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

Posted on

Firebase Cloud Firestore 使い方の勘ドコロ

FirebaseのCloud Firestoreを扱う際、本家ドキュメントが充実していて検索すればすぐに出てくるのでそれほど困ることはありません。ただ、最初の入りとしてFiresotreのDocumentって何? となったり、docdocRefcityRefcityDocRefsnapShotdocs など、本家ドキュメントの中でも若干変数名の書き方が異なっていたりします。その為、慣れてくれば大丈夫ですが、慣れるまで毎回どう書くんだっけ?と検索することが多かったので、自分なりに解釈して整理してみました。Firestoreを扱える言語は様々用意されていますが、ここでは、JavaScript(Nuxt.js)を例に進めていきます。(セットアップは省きます)

参照元:https://firebase.google.com/docs/firestore/manage-data/add-data?hl=ja

Firestoreの基本的な考え方

基本的な考え方は、正にこの図にあるように「フォルダ(collection)」「ドキュメント(document)」「データ」という構成になっています。

参照元:https://firebase.google.com/docs/firestore/data-model
これをFirestoreの管理画面でみるとこのようになります。

これを「フォルダ(collection)」「ドキュメント(document)」「データ」に当てはめてみるとこのようになります。イメージとしては、本棚にフォルダが並んでいてフォルダを選択するとフォルダの中のファイル一覧がみることができ、あるファイルを選択するとそのファイルに書かれている内容をみることができるという感じです。

◯◯Refとは

本家ドキュメントをみると、◯◯Refという変数がよく出てきます。これはその名の通り「参照」です。つまり本棚にあるフォルダ(collection)やフォルダ内のドキュメントを選択した状態です。この時点ではまだ参照なので取得はしていません。usersコレクションを選択した状態であればuserRefusersコレクションのドキュメントを選択した状態であればuserDocRefといった変数をつけると分かりやすいと思います。(本家ドキュメントではそのようなルール決めはありません。)

userRef = db.collection('users')
userDocRef = db.collection('users').doc('YLu7bG7PcRNYhAg3F6MY')

これらを踏まえて、実際にデータの取得、追加、更新、削除のコードを書いてみます。ここではあえて◯◯Refのように変数を使っています。

コレクション配下のドキュメントデータをすべて取得する

usersコレクションを参照した状態で、get().then()を呼び出すとその時点のドキュメント一覧(snapShot)が取得できます。ドキュメントは1つしか存在しない可能性もありますが、ドキュメント一覧なのでforEachでループすることで全データを取得することができます。ユーザIDはデフォルトでドキュメントに振られるIDです。

const db = firebase.firestore();
let userRef = db.collection('users')
userRef.get().then((snapShot) => {
  snapShot.forEach((doc) => {
    this.users.push({
      user: doc.data(),
      user_id: doc.id
    })
  })
})
ユーザID ユーザ名 ユーザメールアドレス
{{user.user_id}} {{user.user.name}} {{user.user.email}}

参考:https://firebase.google.com/docs/firestore/query-data/get-data?hl=ja

コレクション配下のドキュメントデータをフィルタして取得する

フィルタするのにwhereを利用することができます。whereで対象を絞った後、同様にget().then()を呼び出すとフィルタしたドキュメント一覧が取得できます。

const db = firebase.firestore();
let userRef = db.collection('users')
userRef.where('name', '==', '山田太郎1').get().then((snapShot) => {
  snapShot.forEach((doc) => {
    this.filteredUsers.push({
      user: doc.data(),
      user_id: doc.id
    })
  })
})
ユーザID ユーザ名 ユーザメールアドレス
{{user.user_id}} {{user.user.name}} {{user.user.email}}

参考:https://firebase.google.com/docs/firestore/query-data/get-data?hl=ja

単一のドキュメントデータを取得する

単一のドキュメントデータを取得するには、doc()IDを渡し、get().then()を呼び出すと指定したドキュメントのデータが取得できます。この場合、doc()で単一のドキュメントを指定しているので、取得できるのは一覧ではなく指定したドキュメントの値となります。

const db = firebase.firestore();
let userRef = db.collection('users')
userRef.doc('YLu7bG7PcRNYhAg3F6MY').get().then((doc) => {
  this.user = doc.data()
  this.user.user_id = doc.id
})

これは以下のようにも書くことができます。

const db = firebase.firestore();
let userDocRef = db.collection('users').doc('YLu7bG7PcRNYhAg3F6MY')
userDocRef.get().then((doc) => {
  this.user = doc.data()
  this.user.user_id = doc.id
})
ユーザID ユーザ名 ユーザメールアドレス
{{user.user_id}} {{user.name}} {{user.email}}

参考:https://firebase.google.com/docs/firestore/query-data/get-data?hl=ja

ドキュメントを新規追加する

ドキュメントを新規追加するには、add()またはset()を使います。ドキュメントIDをランダムに完全に新規で作成する場合はadd()を利用します。ドキュメントIDを任意の値に指定したい場合はset()を指定します。set()は、既存のドキュメントIDを指定するとドキュメント全体を更新する処理となります。

const db = firebase.firestore();
let userRef = db.collection('users')
// 完全新規で追加する場合
userRef.add({
  name: '山田花子1',
  email: 'sample1@sample.com'
})
// ドキュメントIDを指定して新規追加する場合
userRef.doc('new-user-id').set({
  name: '山田花子2',
  email: 'sample2@sample.com'
})
// 既存のIDを指定してドキュメントの値を更新する場合
userRef.doc('YLu7bG7PcRNYhAg3F6MY').set({
  name: '山田花子3',
  email: 'sample3@sample.com'
})

参考:https://firebase.google.com/docs/firestore/manage-data/add-data?hl=ja

ドキュメントを更新する

ドキュメント全体を上書きせずにドキュメントの一部のフィールドを更新するには、update()を使います。

const db = firebase.firestore();
let userRef = db.collection('users')
userRef.doc('YLu7bG7PcRNYhAg3F6MY').update({
  name: '山田花子1'
})

参考:https://firebase.google.com/docs/firestore/manage-data/add-data?hl=ja

ドキュメントを削除する

ドキュメントを削除するには、特定のドキュメントIDをしていしてdelete()を呼び出します。

const db = firebase.firestore();
let userRef = db.collection('users')
userRef.doc('2YkjKlEoEQtH0tBJnByr').delete()

参考:https://firebase.google.com/docs/firestore/manage-data/delete-data?hl=ja

まとめ

ある程度がっつり書いていけばすぐに慣れますが、ちょこちょこ書いていたので書くたびにドキュメントを参照するというのを何回もやっていたのでまとめてみました。自分なりにまとめたものなので誤っている解釈などがあれば指摘して頂ければ幸いです。