はじめに
Goでデータベースを扱う場合、Gorm, Gorp, sqlx といった O/Rマッパ がよく使われます。多くの場合は構造体のフィールドとデータベースのカラム名のマッピングをするタグ付きの構造体を実装します。例えば Gorm
を用いて users
テーブルにアクセスする場合、以下のような構造体が一例として考えられます。
type User struct { |
Gorm
や Xorm
といった O/Rマッパ を用いる場合、スキーマから構造体を生成できず、構造体の生成が面倒という問題があります。複数人で開発していたり、テーブルのカラム数が多かったり、テーブルの数が多い場合、スキーマから構造体を自動生成できるとよいな、と考えることでしょう。 1
また、当社のメンバーと開発するとき SQL が書け適切に index を貼ったり、実行計画からチューニングできるメンバーが多いです。素の SQL を書く or 書きたいケースが多く、メソッドチェーンでクエリを生成する Gorm
などの O/Rマッパ と相性が悪いと感じていました。
- スキーマからテーブルのカラムとマッピングできる構造体を自動生成したい
- 任意のクエリから結果セットのカラムとマッピングできる構造体を自動生成したい
上記を満たすようなライブラリを探しつつ、今回は既に存在するデータベースのスキーマやクエリから構造体を生成できる xo
と gen
を調査してみました。
SQLBoiler や Xorm は O/Rマッパ として使われることが多いので対象から外しました。SQLBoiler
や Xorm
の所感は Go言語のDBレイヤーライブラリの評価 の記事に記載しておりますので、あわせてご確認ください。
概要サマリ
今回調査するライブラリの2020/07/12時点のデータをもとにした概要サマリです。どちらのライブラリも継続的にメンテナンスされています。
Project Name | Stars | Forks | Open Issues | Description | Last Update |
---|---|---|---|---|---|
xo | 2503 | 224 | 91 | Command line tool to generate idiomatic Go code for SQL databases supporting PostgreSQL, MySQL, SQLite, Oracle, and Microsoft SQL Server | 2020-07-10 11:12:42 |
gen | 565 | 105 | 3 | Converts a database into gorm structs and RESTful api | 2020-07-10 14:19:19 |
- バージョン
今回調査したバージョン情報は以下になります。xo
はセマンティクスバージョニングされていなかったため、コミット ID を記載しています。
ライブラリ | バージョン |
---|---|
xo |
7818603ff52bc0b96122715f9c7df7559aeef82d |
gen |
v0.9.24 |
サンプルプロジェクト
本記事では xo
のライブラリにサンプルとして記載されている booktest
をサンプルのプロジェクトとして考えてみます。データベースは PostgreSQL を採用することとし、Docker を用いて以下のようにローカルに起動させておきます。
docker run --rm --name my-postgres -e POSTGRES_USER=booktest -e POSTGRES_PASSWORD=pass -e POSTGRES_DB=testdb -d -p 15432:5432 postgres |
データベースの ER 図は以下のようになります。
スキーマを作成する SQL は以下になります。
-- The MIT License (MIT) |
xo
xo
はデータベースのスキーマや任意の SQL からデータベースにアクセスする Go のコードを生成するライブラリです。生成するコードはテンプレートファイルを使って柔軟にカスタマイズできます。O/Rマッパ を目指したライブラリではなく、静的に型付けされた Go らしいコードを生成することを目的の 1 つとしています。
まずは xo
を試してみます。ディレクトリ構成は以下です。
. |
さっそく xo
をインストールします。2020/07/13現在、セマンティクスバージョニングはされていないため、master ブランチの最新のコミット(7818603ff52bc0b96122715f9c7df7559aeef82d
) を用います。
go get -u github.com/xo/xo |
生成先のディレクトリ名は generated
とします。xo
コマンドを用いて、構造体を生成します。
xo pgsql://booktest:pass@localhost:15432/testdb?sslmode=disable --schema booktest --out generated |
generated
ディレクトリ配下にいくつかファイルが生成されました。
. |
生成されたファイル author.xo.go
を確認します。ファイルの構成は、大きく SELECT を扱うクエリ用の関数と、それ以外の INSERT, UPDATE, DELETE といったクエリを扱うメソッドと 2 種類に大別できます。
SELECT系
SELECT を扱うクエリでは以下の 2 つの関数が生成されています。
AuthorsByName(db XODB, name string) ([]*Author, error)
AuthorByAuthorID(db XODB, authorID int) (*Author, error)
それぞれ関数のドキュメントからデータベースに貼られた index をもとに生成されています。
// AuthorsByName retrieves a row from 'booktest.authors' as a Author. |
関数内に SQL が明示的に記述されていて安心感があります。また index から生成されているので、概ねクエリは高速に動作するでしょう。ただし関数のシグネチャや、データベースアクセスするメソッドのシグネチャから分かるように Context
を引数に受け取ることができません。多くのケースでは Context
を引き回すケースが一般的ですので、Context 対応はテンプレートなどを用いて拡張する必要がありそうです。
// AuthorByAuthorID retrieves a row from 'booktest.authors' as a Author. |
INSERT/UPDATE/DELETE系
INSERT のクエリも SELECT と同様に、INSERT するメソッドが生成されます。
// ... |
UPDATE, UPSERT, DELETE 用のメソッドも生成されます。本記事では割愛します。
その他特徴
xo
で特徴的な機能は、任意のクエリをもとに構造体を生成できる点です。books
テーブルと authors
テーブルを JOIN して books
の title
で文字列検索するクエリの例を考えてみます。以下は xo
のコマンド例です。SQL を記述して構造体を生成できます。オプションの詳細は README.md を参照ください。
xo pgsql://booktest:pass@localhost:15432/testdb?sslmode=disable -N -M -B -T AuthorBookResult --query-type-comment='AuthorBookResult is the result of a search.' -o generated << ENDSQL |
コマンドを実装すると authorbookresult.xo.go
というファイルが生成されます。以下のようにコマンドに記述したクエリとデータベースから取得するカラムのレコードを保持する構造体が生成されます。SQL にパラメータとして渡す変数の型は、xo
のコマンドに記述する SQL の内に int
, string
などとして明示的に記述する必要があります。
// Package generated contains the types for schema 'public'. |
余談ですが、カスタム SQL から構造体を生成する場合、-- hogehoge
というコメントの記述はできません。生成される SQL が壊れます。以下のように、実行時のログを以下のようにして出力すると明らかです。
generated.XOLog = log.Printf |
- 実行結果例
文字列結合した SQL ですが、--
によって意図しない範囲の記述までコメントアウトされてしまいます。/* hogehoge */
とコメントを記述すれば OK です。
2020/07/14 21:59:00 SELECT a.author_id AS author_id, a.name AS author_name, b.book_id AS book_id, b.isbn AS book_isbn, b.title AS book_title, b.tags AS book_tags, -- memo is memo b.memo AS book_memo FROM books b JOIN authors a ON a.author_id = b.author_id WHERE |
所感
- Good
- SQL が文字列として明示されていて、どのようなクエリが発行されるか分かる
- クエリを扱う関数やメソッドが扱う型を明示していて扱いやすい
- Bad
- デフォルトで生成されたクエリは Context を扱うことができず、
Context
を使う場合、テンプレートの実装が必要 - 生成されたクエリを
psql
などで接続してコンソールから直接実行したい場合、文字列結合されたクエリを連結させる必要があり手間 - カスタムクエリでパラメータを使う場合、独自の記述方法が必要。またパラメータ内に Go の型を埋め込む必要があり、煩雑
- カスタムクエリを使う場合、SQL にコメント
-- hogehoge
が記載されていると、生成されるクエリが壊れる
- デフォルトで生成されたクエリは Context を扱うことができず、
gen
続いて gen
です。gen
はデータベースのスキーマメタデータをもとにしてデータベース(特に Gorm
)とマッピング可能な構造体のコードを生成できます。
gen
をインストールします。
go get -u github.com/smallnest/gen@v0.9.24 |
プロジェクト構成は xo
を試したときと同様に以下のようにしておきます。
. |
gen
コマンドで構造体を生成します。
$ gen --connstr="host=localhost port=15432 user=booktest password=pass dbname=testdb sslmode=disable" --out generated --sqltype postgres --database testdb --no-json --overwrite |
generated
ディレクトリ配下に model
というディレクトリが生成され、ディレクトリの中に authors.go
、books.go
と model_base.go
という 3 つのファイルが生成されました。books
テーブルの tags
カラムのデータ型は varchar[]
型でしたが、gen
で生成するとエラーになり、生成された構造体にはフィールドが存在しませんでした。
. |
gen
ではデータベースのデータ型と構造体の型をマッピングする json ファイルを用意し、コマンド実行時にパラメータ --mapping=
でファイルを指定することで任意の型のマッピングが可能になります。お手軽かつ柔軟にカスタマイズできる点は非常に良いと感じています。カラムが NULL を許容するかどうかで NULL を扱える型が生成できる点も嬉しいポイントです。ただし mapping.json をローカルにダウンロードし、以下のマッピング情報を追加して gen
コマンドを実行時にファイルを指定しても
{ |
table: books unable to generate struct field: tags type: _VARCHAR error: unknown sql type: _varchar
というエラーは出力されてしまい、フィールドは生成されませんでした。謎のハックですが、エラーメッセージを確認し、以下のように _varchar
型とするとうまく生成できます。
{ |
さて books.go
は以下のようなファイルです。デフォルトだと guregu/null
や satori/go.uuid
といったモジュールを要求されます。テンプレートで拡張の余地がありそうです。
package model |
Gorm
を意識したライブラリになっており、gen
コマンドに --gorm
というパラメータを付与すると生成する構造体に Gorm
のタグが付与されます。authors.go
ファイルに付与されたタグは以下のようなものです。
// Authors struct is a row record of the authors table in the testdb database |
gorm:"primary_key;column:author_id;type:INT4;"
といったタグが付与されています。構造体のメソッドとして付与されている TableName()
メソッドもおそらく Gorm
を意識したものでしょう。以下のような TableName()
メソッドを実装すると Gorm
のテーブル名として string
の文字列が使用されます。以下の場合テーブル名は authors
です。
// TableName sets the insert table name for this struct type |
ちなみに Gorm
の構造体として生成されるタグですが、外部キーは付与されませんでした。books
テーブルにおける author_id
は authors
の外部キーですが、Gorm
の構造体を見ると、外部キーであるタグは付与されていません。
// Books struct is a row record of the books table in the testdb database |
所感
- Good
Gorm
やdb
といった O/Rマッパ と連携できるタグが付与された構造体を生成できる- 拡張できる点が多く、柔軟にカスタマイズできる
- Bad
- デフォルトだと生成される構造体がやや重厚
varchar[]
型があるテーブルから構造体を生成した場合にエラーが発生するなど、デフォルトでは扱えないデータ型がある
まとめ
スキーマからクエリを扱う関数やメソッド、構造体を生成するライブラリを調査しました
xo
は SQLBoiler
と同じような系統で、スキーマや SQL をからデータベースアクセス可能な関数やメソッドを生成します。gen
はスキーマから構造体を生成するライブラリです。Gorm
や sqlx
といった O/Rマッパ などと一緒に使う必要があるでしょう。
いずれにしてもスキーマから構造体を生成できる点は開発体験として良いと感じています。xo
も gen
も特徴的で、興味深いライブラリです。ただし、プロジェクトの開発に組み込んでいくにはテンプレートの拡張などいくつか工夫は必要だと感じました。
- 1.スキーマを変更した場合、構造体の生成だけでなくマイグレーション管理が必要ですが、本記事ではマイグレーションの話は割愛します。 ↩