Go言語のデータベース用のライブラリを比較する

by

@wapa5pow

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

設定ファイル

title=db/dbconf.yml
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というファイルができるので、以下のように編集します。

title=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

ロールバック

goose down

所感

  • マイグレーションのファイル名にタイムスタンプがついているので複数人で開発していてもマイグレーションファイルのコンフリクトがなさそう
  • 環境ごとの設定ファイルを作れるのはいい
  • Railsのときみたいに抽象化されたマイグレーションのテーブル定義はかけない

rubenv/sql-migrate

対応しているデータベース

  • SQLite
  • PostgreSQL
  • MySQL
  • MSSQL
  • Oracle

インストール

go get -v github.com/rubenv/sql-migrate/...

設定ファイル

title=dbconfig.yml
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のようなタイムスタンプのついたファイルが作られるので以下のように編集します。

title=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

ロールバック

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を定義します。

title=user.go
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の設定ファイルができるので以下のように編集します。

title=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というテーブルができ、以下のレコードが入っている。

pop

ロールバック

$ 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_atupdated_atが自動的につく。popのORMを使えば自動で更新される。
  • マイグレーションの適用日時がレコードにはいっていない。まあ、困ることはなさそう。
  • 個人的に一番使いがってがよさそう

k0kubun/sqldef

データベースのスキーマファイル(schema.sql)を作っておき、それとデータベースとの差分を修正するSQL(例えばALERT文)を出力してそれをマイグレーションとして適用する形です。

最近コミットされていませんが、KAYACでも同じような思想の(schemalex/schemalex)を使っているようです。ここで実際の使い方がのっていました。

sqldefの作者の方のこちらのブログにsqldefとschemalexとの違いがのっています。

PostgreSQLはsslmode=disableをつけてビルドしてからやらないと動かなかったので、MySQLのほうで今回ためします。

インストール

こちらにあるようにバイナリをとってきて適当なところに置きます。

スキーマの定義

まずは何も定義されていないデータベースにusersテーブルを定義するためにschema.sqlを作ります。

title=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))
}

その他のクエリは、READMEThe 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のインストールなどに関しては前半部分にかいたので参照してください。

title=db/migrations/20180905083427_CreateTables.sql

-- +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のコードにおとします。

title=sqlboiler.toml
[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も早い

参考にしたサイト