Go言語のデータベース用のライブラリを比較する
GoでCloud SQLを使うことになったのですがライブラリが複数ありどのライブラリを選んでいいか迷ったので比較してみます。
ライブラリを導入する上でほしい機能は以下です。
- migration: Railsみたいにマイグレーションファイルがかけてマイグレーションやロールバックがしたい
- transaction: トランザクションが書きやすく直感的
- orm: データベースへのクエリを抽象化でき、Goのstructへのマッピングも可能
- connection pool: データベースへのコネクション数を制限できる
- multiple environment support: 本番・ステージング・開発環境など複数の環境で設定Sを使い分けられる
TL;DR
- スキーマを一度だけ書き、Goのstructもそれから生成してほしい場合は、マイグレーションをk0kubun/sqldef, ORMをvolatiletech/sqlboilerにするとよさそう。
- Rails的にやりたいなら、マイグレーションとORMが一緒になっているgobuffalo/pop。マイグレーションにもSQLをかかない。
- よく使われていて安心感を得たい場合は、マイグレーションはliamstask/goose、ORMはjinzhu/gorm
- パフォーマンスを気にする場合は、マイグレーションツールはliamstask/goose、ORMはvolatiletech/sqlboiler
環境設定
今回、docker-composeでMySQL 5.7とPostgreSQL 9.6を立ち上げて実際に、コードを実行しながら、ライブラリごとにどのような利点や課題があるのかみていきます。docker-composeでデータベースの保存先をmacOS側にしなければ立ち上げごとにデータベースが初期化されるので楽です。
使ったコードはhttps://github.com/wapa5pow/go-database-library-how-toにあげました
基本、PostgreSQLでやっていきますが、MySQLにしかないライブラリの場合は、MySQLでやります。
migrationライブラリの調査
まずはmigration用のライブラリを試してみます。(だいたいGo製マイグレーションツールまとめと同じようになってしまった)
liamstask/goose
gooseはBitbucket上のものとGithub上のものがありソースコードも違います。migrationファイルがタイムスタンプになっているBitbucket上のものを使います。
対応しているデータベース
- MySQL
- PostgreSQL
- SQLite
インストール
go get bitbucket.org/liamstask/goose/cmd/goose
設定ファイル
development:
driver: postgres
open: user=postgres dbname=postgres sslmode=disable password=password
マイグレーションファイルの作成
goose create create_users_table sql
コマンドラインでsqlを指定しないとGoのコードでmigrationファイルがかけます
実行するとdb/migrations/20180904123223_create_users_table.sql
というファイルができるので、以下のように編集します。
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TABLE users
(id BIGINT, name varchar(256));
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back
DROP TABLE users;
マイグレーション
goose up
実行するとデータベースに、goose_db_versionというテーブルができ以下のようなレコードができます。
ロールバック
goose down
所感
- マイグレーションのファイル名にタイムスタンプがついているので複数人で開発していてもマイグレーションファイルのコンフリクトがなさそう
- 環境ごとの設定ファイルを作れるのはいい
- Railsのときみたいに抽象化されたマイグレーションのテーブル定義はかけない
rubenv/sql-migrate
対応しているデータベース
- SQLite
- PostgreSQL
- MySQL
- MSSQL
- Oracle
インストール
go get -v github.com/rubenv/sql-migrate/...
設定ファイル
development:
dialect: postgres
datasource: user=postgres dbname=postgres sslmode=disable password=password
dir: migrations/postgres
table: migrations
production:
dialect: postgres
datasource: user=postgres dbname=postgres sslmode=disable password=password
dir: migrations/postgres
table: migrations
マイグレーションファイルの作成
mkdir -p migrations/postgres
sql-migrate new create_users_table
migrations/postgres/20180904140112-create_users_table.sql
のようなタイムスタンプのついたファイルが作られるので以下のように編集します。
-- +migrate Up
CREATE TABLE users
(id BIGINT, name varchar(256));
-- +migrate Down
DROP TABLE users;
マイグレーション
sql-migrate up
migrationsというテーブルに以下のようなデータが入ります。
ロールバック
sql-migrate down
上記を実行すると、ロールバック後に、migrationsテーブルの該当レコードが消される
所感
- gooseとだいたい一緒
- gooseより対応ドライバが多い
- transaction内で実行できないSQL(PostgreSQLのindex作成など)の場合でもnotransactionをマイグレーションファイルにつけることにより対応できる
naoina/migu
miguはGoのstructからマイグレーションします。
対応しているデータベース
- MySQL
インストール
go get -u github.com/naoina/migu/cmd/migu
マイグレーション
まずGoのstructでUserを定義します。
package main
//+migu
type User struct {
Id int64
name string
}
miguコマンドを実行するとuser.goのstructを読み取ってテーブルを作ってくれます。
migrationファイルは作られずに、そのままcreate文が発行されて適用されます。
2回実行してもcreate文は発行されず冪等性が保たれています。
$ migu sync -uusername -ppassword sample user.go
--------applying--------
CREATE TABLE `user` (
`id` BIGINT NOT NULL
)
--------done 0.011s--------
primaryキーなどを設定したいときは、以下のようにアノテーションをつければ大丈夫です。
Id int64 `migu:"pk"`
所感
- Goのstructをテーブル定義としているのはいちいちマイグレーションファイルをかかなくていいので楽
- マイグレーションするときにSQLを実行したいなどのニーズがみたせない
- ロールバックもできないのでつらい
gobuffalo/pop
対応しているデータベース
- PostgreSQL (>= 9.3)
- MySQL (>= 5.7)
- SQLite (>= 3.x)
- CockroachDB (>= 1.1.1)
インストール
go get github.com/gobuffalo/pop/...
go install github.com/gobuffalo/pop/soda
設定ファイル
soda g config
と実行するとdatabase.yml
の設定ファイルができるので以下のように編集します。
development:
dialect: postgres
database: postgres
user: postgres
password: password
host: 127.0.0.1
pool: 5
マイグレーションファイルの作成
$ soda generate model user id:int name:text
[POP] 2018/09/04 14:42:51 info - Loading config file from /Users/koichi.ishida/go/src/github.com/wapa5pow/go-database-library-how-to/pop/database.yml
v4.7.2
[POP] 2018/09/04 14:42:51 info - Loading config file from /Users/koichi.ishida/go/src/github.com/wapa5pow/go-database-library-how-to/pop/database.yml
create models/user.go
create models/user_test.go
run goimports -w models/user.go models/user_test.go
create migrations/20180904054251_create_users.up.fizz
create migrations/20180904054251_create_users.down.fizz
作成するとGoのUser structとマイグレーションファイルができます。マイグレーションファイルは、markbates/fizz形式になってます。
マイグレーション
$ soda migrate
[POP] 2018/09/04 16:05:37 info - Loading config file from /Users/koichi.ishida/go/src/github.com/wapa5pow/go-database-library-how-to/pop/database.yml
v4.7.2
[POP] 2018/09/04 16:05:37 info - Loading config file from /Users/koichi.ishida/go/src/github.com/wapa5pow/go-database-library-how-to/pop/database.yml
[POP] 2018/09/04 16:05:37 info - > create_users
[POP] 2018/09/04 16:05:37 info - 0.1038 seconds
[POP] 2018/09/04 16:05:37 info - dumped schema for postgres
schema_migrationというテーブルができ、以下のレコードが入っている。
ロールバック
$ soda migrate down
[POP] 2018/09/04 16:09:35 info - Loading config file from /Users/koichi.ishida/go/src/github.com/wapa5pow/go-database-library-how-to/pop/database.yml
v4.7.2
[POP] 2018/09/04 16:09:35 info - Loading config file from /Users/koichi.ishida/go/src/github.com/wapa5pow/go-database-library-how-to/pop/database.yml
[POP] 2018/09/04 16:09:35 info - < create_users
[POP] 2018/09/04 16:09:35 info - 0.0271 seconds
[POP] 2018/09/04 16:09:35 info - dumped schema for postgres
schema_migrationからはレコードがなくなる
既存のデータベースのスキーマをダンプする
popには既存のデータベースのスキーマをダンプして、新しいデータベースに適用することができます。
既存のテーブルがある状態からマイグレーションを導入したときにテストデータベースを作りたいなどのときに有用そうです。
スキーマのダンプは以下でできます。実行されると、migrations/schema.sql
にスキーマファイルができます。
soda schema dump
既存のschema.sqlを適用するには以下です。
soda schema load
所感
- Railsっぽい
- 2つのファイルにUP/DOWNをわけてかかなければいけないのがめんどくさい
- fuzzは覚えれば生のSQLを書くよりもやりやすそう
- コマンドでデータベースも作れる
- 既存のデータベースがあって、マイグレーションを適用したいときにも使える
- コネクションプールの数も設定ファイルにかける
created_at
とupdated_at
が自動的につく。popのORMを使えば自動で更新される。- マイグレーションの適用日時がレコードにはいっていない。まあ、困ることはなさそう。
- 個人的に一番使いがってがよさそう
k0kubun/sqldef
データベースのスキーマファイル(schema.sql
)を作っておき、それとデータベースとの差分を修正するSQL(例えばALERT文)を出力してそれをマイグレーションとして適用する形です。
最近コミットされていませんが、KAYACでも同じような思想の(schemalex/schemalex)を使っているようです。ここで実際の使い方がのっていました。
sqldefの作者の方のこちらのブログにsqldefとschemalexとの違いがのっています。
PostgreSQLはsslmode=disable
をつけてビルドしてからやらないと動かなかったので、MySQLのほうで今回ためします。
インストール
こちらにあるようにバイナリをとってきて適当なところに置きます。
スキーマの定義
まずは何も定義されていないデータベースにusersテーブルを定義するためにschema.sqlを作ります。
CREATE TABLE `users` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`deleted_at` timestamp NULL DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_users_deleted_at` (`deleted_at`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
スキーマの適用
mysqldef -u username -p password sample -h 127.0.0.1 < schema.sql
MySQLを確認するとusersテーブルがつくられています。
次に、emailカラムをschema.sql
に足し、再度mysqldefコマンドを実行すると、ALTER文が発行されてemailカラムが追加されています。
スキーマのエクスポート
以下のコマンドで既存のデータベースのスキーマをエクスポートすることができます。
所感
- まだ作られて間もないがいままでの重厚なマイグレーションと比べてスキーマだけをかけばいいシンプルさがある
- 複雑なケースがでないかぎりこれで十分なきもする。万が一複雑なケースがでても、
--dry-run
でALTER文などを発行しておき、Goでコードでそれを実行しつつマイグレーションで必要なことをやればようさそう
ORMライブラリの調査
各種ORMライブラリをみていきます。生のSQLを書くライブラリは使わないつもりなので調べていないです。
gobuffalo/pop
popの設定はマイグレーションで説明したやり方と同じです。
Insert
トランザクションなし
package main
import (
"github.com/gobuffalo/pop"
"github.com/wapa5pow/go-database-library-how-to/pop/models"
"log"
)
func main() {
conn, err := pop.Connect("development")
if err != nil {
log.Fatal(err)
}
user := models.User{Name: "Ishida"}
conn.Create(&user)
}
トランザクションあり
package main
import (
"github.com/gobuffalo/pop"
"github.com/wapa5pow/go-database-library-how-to/pop/models"
"log"
)
func main() {
conn, err := pop.Connect("development")
if err != nil {
log.Fatal(err)
}
conn, err = conn.NewTransaction()
if err != nil {
log.Fatal(err)
}
user := models.User{Name: "Ishida"}
conn.Create(&user)
conn.TX.Commit()
}
Select
ページネーションは以下でできる。
package main
import (
"fmt"
"github.com/gin-gonic/gin/json"
"github.com/gobuffalo/pop"
"github.com/wapa5pow/go-database-library-how-to/pop/models"
"log"
)
func main() {
conn, err := pop.Connect("development")
if err != nil {
log.Fatal(err)
}
users := []*models.User{}
err = conn.Paginate(0, 2).All(&users)
if err != nil {
log.Fatal(err)
}
j, _ := json.Marshal(users)
fmt.Println(string(j))
}
その他のクエリは、READMEやThe Unofficial pop Bookに書き方がのっています。
所感
- マイグレーションのときと同じくRailsみたい
- has_manyなど設定できデータベースへの問い合わせが楽にかけそう
- callbackもある
jinzhu/gorm
インストール
go get -u github.com/jinzhu/gorm
コード
package main
import (
"encoding/json"
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
)
type User struct {
gorm.Model
Name string
}
func main() {
db, err := gorm.Open("postgres", "user=postgres dbname=postgres sslmode=disable password=password")
if err != nil {
panic(err)
}
defer db.Close()
db.LogMode(true)
var newUser = &User{Name: "Ishida"}
newDB := db.AutoMigrate(&User{})
if newDB.Error != nil {
panic(newDB.Error)
}
newDB = db.Create(newUser)
if newDB.Error != nil {
panic(newDB.Error)
}
j, _ := json.Marshal(newUser)
fmt.Println(string(j))
var user User
newDB = db.First(&user, 1)
if newDB.Error != nil {
panic(newDB.Error)
}
j, _ = json.Marshal(user)
fmt.Println(string(j))
newDB = db.Delete(&user)
if newDB.Error != nil {
panic(newDB.Error)
}
}
所感
- 機能が豊富。ドキュメントも充実している。
- PostgreSQLのjsonbなどにも対応できる。(Write new Dialect)
- gorm.Modelをstructのフィールドとして追加すると、ID, CreatedAt, UpdatedAt, DeletedAtのフィールとが追加され、論理削除になる。
- MigrationはAuto Migrationというのがあり、マイグレーションの定義をかかなくてもGoのstructを見て、不足があれば追加してくれる。明示的にIndexを足したりもできる。ただし、Auto Migrationはカラムを削除したりができないので使いづらそう。
- エラーが返り値でかえってくるのではなく、DBオブジェクトのフィールドとして入っているので若干扱いずらい。(Error Handling)
- メソッドチェーンでクエリをかける
- Connection Poolの設定がコードでかける
- ロガーにどのようなクエリがサーバ側から発行されたのか出力できる。Logger
- Gorm用のマイグレーションヘルパーのgo-gormigrate/gormigrateというのもあるが、求めている形ではない
go-xorm/xorm
Gormからフォークされて拡張されている。Pairsでも採用されているみたい
インストール
go get -u github.com/go-xorm/xorm
コード
省略
所感
- xorm toolsがある。既存のデータベースをGoのコードとして落とせたりする
- 楽観的ロックの仕組みがある
- Go的なエラーの返し方をしてくれる
- マイグレーション的な機能もある。(3.4.Synchronize database schema: なぜか直リンクがつくれない。。。)
- ドキュメントがHTTPSでない。そして検索ができない。
- コネクションプールがある。
- 全体的になんか若干不安。
volatiletech/sqlboiler
sqlboilerは先にデータベースにテーブルが作られている状態で、その状態をsqlboilerコマンドでGoのstructで定義されたmodelsに落とすことでORMを実現しています。sqlboiler自体にはマイグレーションツールが付属していないので、gooseなど別途マイグレーションツールを使用する必要があります。
インストール
go get -u -t github.com/volatiletech/sqlboiler
go get -u github.com/volatiletech/null
go get -u github.com/volatiletech/sqlboiler/drivers/sqlboiler-psql
マイグレーションの作成と実行
gooseを使って行っていきます。gooseのインストールなどに関しては前半部分にかいたので参照してください。
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TABLE pilots (
id SERIAL NOT NULL,
name text NOT NULL
);
ALTER TABLE pilots ADD CONSTRAINT pilot_pkey PRIMARY KEY (id);
CREATE TABLE jets (
id integer NOT NULL,
pilot_id integer NOT NULL,
age integer NOT NULL,
name text NOT NULL,
color text NOT NULL
);
ALTER TABLE jets ADD CONSTRAINT jet_pkey PRIMARY KEY (id);
ALTER TABLE jets ADD CONSTRAINT jet_pilots_fkey FOREIGN KEY (pilot_id) REFERENCES pilots(id);
CREATE TABLE languages (
id integer NOT NULL,
language text NOT NULL
);
ALTER TABLE languages ADD CONSTRAINT language_pkey PRIMARY KEY (id);
-- Join table
CREATE TABLE pilot_languages (
pilot_id integer NOT NULL,
language_id integer NOT NULL
);
-- Composite primary key
ALTER TABLE pilot_languages ADD CONSTRAINT pilot_language_pkey PRIMARY KEY (pilot_id, language_id);
ALTER TABLE pilot_languages ADD CONSTRAINT pilot_language_pilots_fkey FOREIGN KEY (pilot_id) REFERENCES pilots(id);
ALTER TABLE pilot_languages ADD CONSTRAINT pilot_language_languages_fkey FOREIGN KEY (language_id) REFERENCES languages(id);
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back
DROP TABLE IF EXISTS pilot_languages;
DROP TABLE IF EXISTS languages;
DROP TABLE IF EXISTS jets;
DROP TABLE IF EXISTS pilots;
マイグレーションを実行します。
goose up
コード
sqlboilerの設定ファイルsqlboiler.toml
を書きテーブル定義からGoのコードにおとします。
[psql]
dbname="postgres"
host="127.0.0.1"
user="postgres"
pass="password"
sslmode="disable"
sqlboiler psql
実行すると、models以下に各種Goのファイルが作られます。
次にレコードを挿入するコードをかいて実行するとAuto Incrementがきいた状態でレコードが追加されています。
package main
import (
"context"
"database/sql"
"encoding/json"
"fmt"
_ "github.com/volatiletech/sqlboiler/drivers/sqlboiler-psql/driver"
"github.com/volatiletech/sqlboiler/boil"
"github.com/wapa5pow/go-database-library-how-to/sqlboiler/models"
)
func main() {
// Open handle to database like normal
db, err := sql.Open("postgres", "user=postgres dbname=postgres sslmode=disable password=password")
if err != nil {
panic(err)
}
ctx := context.Background()
tx, err := db.BeginTx(ctx, nil)
if err != nil {
panic(err)
}
defer tx.Commit()
var p1 models.Pilot
p1.Name = "Larry"
err = p1.Insert(ctx, tx, boil.Infer())
if err != nil {
panic(err)
}
j, _ := json.Marshal(p1)
fmt.Println(string(j))
}
所感
- マイグレーションツールは別途必要だがよさそう
- グローバルなデータベースを定義でき、いちいちデータベースのインスタンスをわたさなくても実行できるようにもできる。ツール用とかでは便利そう。
- Benchmarksも早い