taisablog

taisa's engineer blog

Go

Go言語 ORMライブラリ GORMの使い方

投稿日:


Go言語 ORMライブラリのGORMの簡単な使い方を確認してみました。また、公式ドキュメントにしっかりと使い方が書いてありますので基本的にはそちらを参考にしてもらえればと思います(すべてではないですが日本語訳もされています)。その上でクイックスタートを元に簡単な使い方と挙動を確認してみます。
http://gorm.io/ja_JP/docs/

インストール

以下のコマンドでインストールできます。

go get -u github.com/jinzhu/gorm

クイックスタート

公式ドキュメントにあるクイックスタートを実行してみました。DBだけsqliteではなくmysqlに変更しています。

package main
import (
  "github.com/jinzhu/gorm"
  // _ "github.com/jinzhu/gorm/dialects/sqlite"
  _ "github.com/jinzhu/gorm/dialects/mysql"
)
type Product struct {
  gorm.Model
  Code string
  Price uint
}
func main() {
  // db, err := gorm.Open("sqlite3", "test.db")
  db, err := gorm.Open("mysql", "gorm:gorm@/sandbox?charset=utf8mb4&parseTime=True&loc=Local")
  if err != nil {
    panic("データベースへの接続に失敗しました")
  }
  defer db.Close()
  // スキーマのマイグレーション
  db.AutoMigrate(&Product{})
  // Create
  db.Create(&Product{Code: "L1212", Price: 1000})
  // Read
  var product Product
  db.First(&product, 1) // idが1の製品を探します
  db.First(&product, "code = ?", "L1212") // codeがL1212の製品を探します
  // Update - 製品価格を2,000に更新します
  db.Model(&product).Update("Price", 2000)
  // Delete - 製品を削除します
  db.Delete(&product)
}

実行してみるとproductsテーブルが作成され、以下のカラムとレコードができました。structでは宣言していない、idcreated_atupdated_atdeleted_atカラムができ、deleted_atに日付が入りソフトデリートが行われています。

go run main.go

gorm.Model

gorm.Modelを宣言するとidcreated_atupdated_atdeleted_atカラムが自動的に注入されます。また,deleted_atカラムがある場合、Deleteはソフトデリートになります。
参考:
http://gorm.io/ja_JP/docs/conventions.html

SQL実行ログ出力

先ほど実行したプログラムでどんなSQLが実行されたか確認してみます。db.LogMode(true)を設定するとSQLの実行ログが確認できます。

func main() {
  // db, err := gorm.Open("sqlite3", "test.db")
  db, err := gorm.Open("mysql", "gorm:gorm@/sandbox?charset=utf8mb4&parseTime=True&loc=Local")
  if err != nil {
    panic("データベースへの接続に失敗しました")
  }
  defer db.Close()
  // ログを出力する
  db.LogMode(true)
  // スキーマのマイグレーション
  db.AutoMigrate(&Product{})
  // Create
  db.Create(&Product{Code: "L1212", Price: 1000})
  // Read
  var product Product
  db.First(&product, 1)                   // idが1の製品を探します
  db.First(&product, "code = ?", "L1212") // codeがL1212の製品を探します
  // Update - 製品価格を2,000に更新します
  db.Model(&product).Update("Price", 2000)
  // Delete - 製品を削除します
  db.Delete(&product)
}

実行SQLや実行時間が確認できるようになりました。

(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:28)
[2019-06-26 19:57:23]  [16.23ms]  CREATE TABLE `product` (`id` int unsigned AUTO_INCREMENT,`created_at` timestamp NULL,`updated_at` timestamp NULL,`deleted_at` timestamp NULL,`code` varchar(255),`price` int unsigned , PRIMARY KEY (`id`))
[0 rows affected or returned ]
(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:28)
[2019-06-26 19:57:23]  [1.58ms]  CREATE INDEX idx_product_deleted_at ON `product`(deleted_at)
[0 rows affected or returned ]
(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:31)
[2019-06-26 19:57:23]  [0.30ms]  INSERT  INTO `product` (`created_at`,`updated_at`,`deleted_at`,`code`,`price`) VALUES ('2019-06-26 19:57:23','2019-06-26 19:57:23',NULL,'L1212',1000)
[1 rows affected or returned ]
(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:35)
[2019-06-26 19:57:23]  [0.32ms]  SELECT * FROM `product`  WHERE `product`.`deleted_at` IS NULL AND ((`product`.`id` = 1)) ORDER BY `product`.`id` ASC LIMIT 1
[1 rows affected or returned ]
(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:36)
[2019-06-26 19:57:23]  [0.33ms]  SELECT * FROM `product`  WHERE `product`.`deleted_at` IS NULL AND `product`.`id` = 1 AND ((code = 'L1212')) ORDER BY `product`.`id` ASC LIMIT 1
[1 rows affected or returned ]
(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:39)
[2019-06-26 19:57:23]  [0.24ms]  UPDATE `product` SET `price` = 2000, `updated_at` = '2019-06-26 19:57:23'  WHERE `product`.`deleted_at` IS NULL AND `product`.`id` = 1
[1 rows affected or returned ]
(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:42)
[2019-06-26 19:57:23]  [0.22ms]  UPDATE `product` SET `deleted_at`='2019-06-26 19:57:23'  WHERE `product`.`deleted_at` IS NULL AND `product`.`id` = 1
[1 rows affected or returned ]

created_atupdated_atdeleted_atが不要な場合

テーブルによってはcreated_atupdated_atdeleted_atが不要な場合もあります。その時はgorm.Modelを使わなければカラムは作られません(IDだけはできるよう個別に記述しています)。

type Product struct {
  // gorm.Model
  ID uint `gorm:"primary_key"`
  Code  string
  Price uint
}

この場合、先ほどのSQL実行ログは下記のようになります。deleted_atカラムがないので今回のレコード削除はハードデリートになります。また、DELETE文でwhere product.id = 1となっているのは、db.Create(&Product{Code: "L1212", Price: 1000})実行時のIDを引き継いでいるようで、db.Create(&Product{Code: "L1212", Price: 1000})を削除するとwhere product.id = 1の指定はなくなります。もしくはproduct.ID = 2Delete実行前に記述するとwhere product.id = 2と条件が変わってくれます。

(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:31)
[2019-06-26 20:14:50]  [0.24ms]  INSERT  INTO `product` (`code`,`price`) VALUES ('L1212',1000)
[1 rows affected or returned ]
(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:35)
[2019-06-26 20:14:50]  [0.26ms]  SELECT * FROM `product`  WHERE (`product`.`id` = 1) ORDER BY `product`.`id` ASC LIMIT 1
[1 rows affected or returned ]
(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:36)
[2019-06-26 20:14:50]  [0.25ms]  SELECT * FROM `product`  WHERE `product`.`id` = 1 AND ((code = 'L1212')) ORDER BY `product`.`id` ASC LIMIT 1
[1 rows affected or returned ]
(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:39)
[2019-06-26 20:14:50]  [0.16ms]  UPDATE `product` SET `price` = 2000  WHERE `product`.`id` = 1
[0 rows affected or returned ]
(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:42)
[2019-06-26 20:14:50]  [0.16ms]  DELETE FROM `product`  WHERE `product`.`id` = 1
[1 rows affected or returned ]

テーブル名を単数形にする場合

マイグレーションを実行すると生成されるテーブル名はproductsのように自動的に複数系になりますが、複数形にしたくない場合もあります。その時はdb.SingularTable(true)を宣言するとテーブルが単数形で生成されます。
参考:
http://gorm.io/ja_JP/docs/conventions.html

GORM本体のテストカバレッジ

OSSライブラリを使う時、どのくらいテストが書かれているか気になったりするので確認してみました。GORM本体にはtest_all.shというテストすべてを実行するスクリプトが用意されているのでそれを元にカバレッジを出してみました。(初期実行時はライブラリが足りないなど出るので都度追加しました。)また、mysqlのテストをする為にmain_test.goに書かれている以下の記述に事前に設定を合わせておく必要があります。

case "mysql":
  fmt.Println("testing mysql...")
  if dbDSN == "" {
    dbDSN = "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True"
  }
  db, err = gorm.Open("mysql", dbDSN)

今回は、postgresmssql環境がないのでmysqlsqliteだけに限定しました。go testのあとに-coverprofile=cover.outを追記してカバレッジを出力します。その後go tool cover -html=cover.out -o cover.htmlを実行してhtmlに変換しました。

#dialects=("postgres" "mysql" "mssql" "sqlite")
dialects=("mysql" "sqlite")
for dialect in "${dialects[@]}" ; do
    echo ${dialect}
    DEBUG=false GORM_DIALECT=${dialect} go test -coverprofile=cover.out
done

実行してみると全体ではcoverage: 81.7% of statementsとカバレッジが81.7%でファイル毎にみても以下のようになっていました。

ソース

https://github.com/taisa831/sandbox-gorm

まとめ

非常に簡単なところだけを触っただけで分かりませんが、通して導入しやすく安定したライブラリだと思いました。個人的にORMはあまり積極的に使わない方ですが簡単に扱える、学習コストが低い、実行SQLを確認しながら扱える、などがあると導入障壁が低くてよいですね。

-Go
-

執筆者:

関連記事

Go言語 GORM+GinでTODOリストを作ってみた

前回の「Go言語 ORMライブラリ GORMの使い方」に続いて「GORM+Gin」でTODOリストを作ってみました。使い方は「GitHubのREADME」を参考にしました。できたものは下記URLから確認できます。装飾は別途やれればと。 http://gin.taisablog.com/todo 事前情報 Webフレームワーク:Gin (https://github.com/gin-gonic/gin) ORM:GORM (https://gorm.io/docs) DB:MySQL ルーティングは通常のフォームだとPUT/DELETEが使えないので以下のようにしました。 [GIN-debug] GET /todo –> main.main.func1 (3 handlers) // 一覧表示 [GIN-debug] POST /todo –> main.main.func2 (3 handlers) // 新規作成 [GIN-debug] GET /todo/:id –> main.main.func3 (3 handlers) // 編集画面表示 [GIN-debug] POST /todo/edit/:id –> main.main.func4 (3 handlers) // 編集 [GIN-debug] POST /todo/delete/:id –> main.main.func5 (3 handlers) // 削除 main.goだけで作成した場合 main.goに全ての処理を記述しています。 package main import ( “github.com/gin-gonic/gin” “github.com/jinzhu/gorm” _ “github.com/jinzhu/gorm/dialects/mysql” “net/http” “strconv” ) type Todo struct { gorm.Model Text string Status uint64 } func main() { db, err := gorm.Open(“mysql”, “gorm:gorm@/sandbox_gin?charset=utf8mb4&parseTime=True&loc=Local”) if err != nil { panic(“データベースへの接続に失敗しました”) } defer db.Close() db.LogMode(true) db.AutoMigrate(&Todo{}) r := …

no image

DockerでGoのWebサーバーを起動する

ミニマムにやっておかないと忘れがちなのでメモ サンプル用Goサーバを作成する package main import ( “encoding/json” “net/http” ) type User struct { FirstName string `json:”firstName”` LastName string `json:”lastName”` } func users(w http.ResponseWriter, req *http.Request) { w.Header().Set(“Content-Type”, “application/json”) user := User{ FirstName: “John”, LastName: “Doe”, } var users []User users = append(users, user) json.NewEncoder(w).Encode(users) } func main() { http.HandleFunc(“/users”, users) http.ListenAndServe(“:8002”, nil) } 普通に起動して動作確認をします。 $ go run main.go http://localhost:8002/usersにアクセスするとJSON結果が出力されます。 [ { “firstName”: “John”, “lastName”: “Doe” } ] Dockerfile Dockerfileを作成します。alpineをつけるとよりミニマムなイメージができます。参考(https://hub.docker.com/_/golang?tab=description) FROM golang:1.14 #FROM golang:1.14-alpine # コンテナログイン時のディレクトリ指定 WORKDIR /opt/sandbox-docker-go # ホストのファイルをコンテナの作業ディレクトリにコピー COPY . . # ADD . . # ビルド RUN go build -o app main.go # 起動 CMD [“/opt/sandbox-docker-go/app”] 構成 $ …

Golangを使ってJWTを15分で理解する

JWTとは JWT(ジョットと言うらしい)はJSON Web Tokenの略で、JSONをベースとしたアクセストークンのためのオープン標準 (RFC 7519) です。色々記事を見ましたが、最終的にWikipediaが分かりやすく一番参考にしました。https://ja.wikipedia.org/wiki/JSON_Web_Token JWTの構造 JWTは以下の3つの要素をピリオドで区切った文字列で構成されます。 ヘッダー 署名生成に使用するアルゴリズムを格納します。下記のHS256は、このトークンがHMAC-SHA256で署名されていることを示しています。署名アルゴリズムとしては、SHA-256を使用したHMAC (HS256) や、SHA-256を使用したRSA署名 (RS256) がよく用いられます。 { “alg” : “HS256”, “typ” : “JWT” } ペイロード 認証情報などのクレームを格納します。クレームとはペイロードに含める以下のような標準フィールド(クレーム)を指します。JWTの仕様では、トークンに一般的に含まれる7つの標準フィールドが定義されています。また用途に応じた独自のカスタムフィールドを含むこともできます。下記の例では、トークン発行日時を示す標準のクレーム (iat) と、カスタムクレーム (loggedInAs) を格納しています。 { “loggedInAs” : “admin”, “iat” : 1422779638 } 7つのペイロードの標準クレーム 署名 トークン検証用の署名です。署名はヘッダーとペイロードをBase64urlエンコーディングしてピリオドで結合したものから生成します。署名はヘッダーで指定された暗号化アルゴリズムにより生成されます。下記はHMAC-SHA256形式でのコード例です。 HMAC-SHA256(base64urlEncoding(header) + ‘.’ + base64urlEncoding(payload), ‘secret key’) JWTを使用するにあたって JWTはトークンが返され、それをローカルに保存して利用します(主にlocal storageやsession storageが用いられますが、セッションIDのようにCookieを用いる場合もあります。) 認証時にはAuthorizationヘッダーでBearerスキーマを利用します。またサーバー上に認証状態を保持しないステートレスな認証方式です。その為JWT単体ではトークンを無効にすることが出来ません。サーバーに状態を保持すれば可能ですが、その場合ステートレスの利点は失われることになります。さて、ここまではほぼ Wikipedia に書いてある内容そのままです。ここから実際にGo/GinのJWT Middlewareを使って実際の動作を確認してみます。 Go/GinのJWT Middlewareを使った動作確認 利用するJWT Middlewareについて ここでは、「https://github.com/gin-gonic/gin」 を使う前提で、次のMiddlewareを利用します。「https://github.com/appleboy/gin-jwt」。このMiddlewareは、auth_jwt.goの1ファイルでで構成されていて、「https://github.com/dgrijalva/jwt-go」 をGin用に薄くラップしたものです。jwt-goはトークンを作成したりパースしたり様々な機能が用意されています。 サンプルソース サンプルソースは、https://github.com/appleboy/gin-jwt/blob/master/README.md に載っているのでこれを元に確認します。処理は大きく「ログイン時にToken発行する」と「トークン認証&処理実行する」の2種類あります。 ログイン時にToken発行する ログイン時にTokenを発行する処理は、LoginHandlerです。Routerでは次のように定義しています。LoginHandlerではAuthenticatorとPayloadFuncが呼ばれる為、Middlewareにてこれらを実装する必要があります。 r.POST(“/login”, authMiddleware.LoginHandler) Authenticatorはログイン認証の為の関数です。例では固定値が設定されていますが、実際は主にDBから値を取得することになると思います。PayloadFuncはペイロードに含めるクレームを設定します。ペイロードには任意のクレームを追加可能なので、ログインIDとなるuserIDをセットしています。 // ログインに基づいたユーザの認証振る舞いをするコールバック Authenticator: func(c *gin.Context) (interface{}, error) { var loginVals login if err := c.ShouldBind(&loginVals); err != nil { return “”, jwt.ErrMissingLoginValues } userID := loginVals.Username password := loginVals.Password // …

GORMでよく使うSQLの書き方

GORMでよく使うSQLの書き方をメモとして残しておきます。詳細は記事最後の参考リンクの公式ドキュメントで確認できます。 CREATE insertするだけであれば create を利用します。 db.Create(&models.User{Name: “user”}) # 実行SQL INSERT INTO “users” (“created_at”,”updated_at”,”name”,”email”) VALUES (‘2020-04-25 11:22:00′,’2020-04-25 11:22:00′,’user’,0) UPDATE 特定のフィールドだけ更新したい場合、 Update と Updates を使います。 update 一つのフィールドだけアップデートします。 user := &models.User{Name: “user”} db.Create(user) db.Model(user).Update(“name”, “user2”) # 実行SQL INSERT INTO “users” (“created_at”,”updated_at”,”name”,”email”) VALUES (‘2020-04-25 11:34:39′,’2020-04-25 11:34:39′,’user’,”) UPDATE “users” SET “name” = ‘user2’, “updated_at” = ‘2020-04-25 11:34:39’ WHERE “users”.”id” = 5 updates updates は map を利用して複数のフィールドをアップデートします。 user := &models.User{Name: “user”} db.Create(user) db.Model(user).Updates(map[string]interface{}{“name”: “user3”, “email”: “g5.taisa831@gmail.com”}) # 実行SQL INSERT INTO “users” (“created_at”,”updated_at”,”name”,”email”) VALUES (‘2020-04-25 11:39:52′,’2020-04-25 11:39:52′,’user’,”) UPDATE “users” SET “email” = ‘g5.taisa831@gmail.com’, “name” = ‘user3’, “updated_at” = ‘2020-04-25 11:39:52’ WHERE “users”.”id” = 6 SAVE Save は SQL を実行する際にすべてのフィールドを含みます。フィールドを指定しなくても空にはなりません。 user := &models.User{Name: …

Golang 1.13 released! The difference from 1.12 to 1.13

Golang 1.13 was released in 3 September 2019. This post has difference from 1.12 to 1.13. I check the changes at the release notes and difference of src. Release Notes Go 1.13 Release Notes The latest Go release, version 1.13, arrives six months after Go 1.12. Most of its changes are in the implementation of the toolchain, runtime, and libraries. As always, the release maintains the Go 1 promise of compatibility. We expect almost all Go programs to continue to compile and run as before. Diff on GitHub Release branch.go1.13 by taisa831 · Pull Request #1 · taisa831/go This PR …