Posted on

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を確認しながら扱える、などがあると導入障壁が低くてよいですね。