🦜

Go製CGOフリーなSQLiteドライバーでentを使う

2021/09/08に公開

CGOフリーなSQLiteドライバーmodernc.org/sqlite

以下のC記述をGoにトランスレートする仕組みで作られたPure-GoなSQLite3ドライバーです。

これを利用して型安全なORM「ent」でつかう方法をまとめてみます。

ドライバーラッパー

entがsqliteドライバーに要求するForeignKeysフラグがmodernc.org/sqlite では標準でスイッチできないのでそこをサポートするコードを差し込みます。幸い、ドライバー名はsqlite3ではなくsqliteで重複していないのでsqlite3で登録することでentがサポートするドライバー名に合わせることができます。

sqlite_driver.go

package main

import (
	"database/sql"
	"database/sql/driver"
	"fmt"

	"modernc.org/sqlite"
)

type sqliteDriver struct {
	*sqlite.Driver
}

func (d sqliteDriver) Open(name string) (driver.Conn, error) {
	conn, err := d.Driver.Open(name)
	if err != nil {
		return conn, err
	}
	c := conn.(interface {
		Exec(stmt string, args []driver.Value) (driver.Result, error)
	})
	if _, err := c.Exec("PRAGMA foreign_keys = on;", nil); err != nil {
		conn.Close()
		return nil, fmt.Errorf("failed to enable enable foreign keys: %w", err)
	}
	return conn, nil
}

func init() {
	sql.Register("sqlite3", sqliteDriver{Driver: &sqlite.Driver{}})
}

上記のファイルをimportできれば、modernc.org/sqlitegithub.com/mattn/go-sqlite3 の代わりにつかうことができるようになります。

あとはentのチュートリアル通り。

go install entgo.io/ent/cmd/ent@latest
mkdir sample
cd sample
go mod init sample
ent init User

生成されたコードent/schema/user.goのFieldsメソッドにフィールド宣言を書き足します。

// Fields of the User.
func (User) Fields() []ent.Field {
	return []ent.Field{
		field.String("name").Default("unknown"),
		field.Int("age").Positive(),
	}
}

上記のスキーマでコード生成を実行します。

go generate ent

以下のようなコードでマイグレーションとユーザーのINSERTを行えます。

package main

import (
	"context"
	"log"
	"modernc-sqlite/ent"

	"entgo.io/ent/dialect"
	_ "modernc.org/sqlite"
)

func main() {
	entOptions := []ent.Option{}
	entOptions = append(entOptions, ent.Debug())
	client, err := ent.Open(dialect.SQLite, "file:ent.sqlite?cache=shared", entOptions...)
	if err != nil {
		log.Fatalf("failed opening connection to sqlite: %v", err)
	}
	defer client.Close()
	if err := client.Schema.Create(context.Background()); err != nil {
		log.Fatalf("failed creating schema resources: %v", err)
	}
	ctx := context.Background()
	u, err := client.User.
		Create().
		SetAge(32).
		SetName("inoue").
		Save(ctx)
	if err != nil {
		log.Fatalf("failed creating user: %v", err)
	}
	log.Println("user was created: ", u)
}

ベンチマーク

コード

func BenchmarkInsert(b *testing.B) {
	ctx := context.Background()
	for i := 0; i < b.N; i++ {
		_, err := client.User.
			Create().
			SetAge(20 + i%32).
			SetName(fmt.Sprintf("hoge%08d", i)).
			Save(ctx)
		if err != nil {
			b.Log(err)
			b.Fail()
		}
	}
}

mattn/go-sqlite3

2021/09/08 13:23:22 Do stuff BEFORE the tests!
goos: darwin
goarch: amd64
pkg: mattn-sqlite3
cpu: Intel(R) Core(TM) i5-8210Y CPU @ 1.60GHz
BenchmarkInsert-4   	   21843	     53604 ns/op	    3137 B/op	     117 allocs/op
PASS
2021/09/08 13:23:24 Do stuff AFTER the tests!
ok  	mattn-sqlite3	1.978s

modernc.org/sqlite

2021/09/08 13:25:24 Do stuff BEFORE the tests!
goos: darwin
goarch: amd64
pkg: modernc-sqlite
cpu: Intel(R) Core(TM) i5-8210Y CPU @ 1.60GHz
BenchmarkInsert-4   	   14449	     84427 ns/op	    2363 B/op	      77 allocs/op
PASS
2021/09/08 13:25:26 Do stuff AFTER the tests!
ok  	modernc-sqlite	2.426s

ER図の生成

https://twitter.com/mattn_jp/status/1435451348829433860

以下のコマンドでer.htmlが出力されます。

go install github.com/a8m/enter@latest
enter ./ent/schema

er.htmlをブラウザで開くとER図がみれます!

まとめ

  • entは型安全でORMが扱えるし、オーバーヘッドも少なめで良い
  • CGO依存が残ることはGoの利点のいくつかを損なう
    • CGO依存は複数の処理系の依存管理が必要になる
    • 環境構築の手間もコンパイル時間も実行時メモリ使用量も悪化する
    • クロスコンパイル環境も難解な作業を伴う
  • 以上のデメリットから解放されることはとっても嬉しい
  • 実行速度だけは若干落ちる(moderncの仕組み上、C実装のエミュレーションをする関係)

Discussion