フューチャー技術ブログ

Go言語のDBレイヤーライブラリの評価

自己紹介

小林と申します。アルバイトとして2019年2月からフューチャーで働いている大学生です。

アルバイトとして入社後は、Goを用いてツール開発、Vue.jsでコンポーネントの開発など沢山のプログラミングの機会を頂いており、日々成長を感じています。現在、GoとDBの連携について学んでおり、沢山の知見を得たため、アウトプットとして、筆を走らせています。

本記事ではGoのDBレイヤーライブラリである、GORM,SQLBoiler,xormの3つについて、それぞれの特徴や違いを押さえられる様まとめてみました。DBレイヤーのライブラリ検討の手がかりとなれば幸いです。

O/Rマッパとは?

Object-Relational-Mapping の頭文字を取った略称です。直訳すると オブジェクト関係マッピング でしょうか。名前だけでは少し分かりにくいのでもう簡単に表現すると、オブジェクト(指向のプログラミング言語)と関係(データベース)のデータの対応付けをしてくれるもの となります。

O/Rマッパライブラリが持つ機能はたくさんありますが、以下のような機能を持つことが多いです。

  • DBのRecordとのMapping
  • SQL文の組み立て

それでは、GORM, SQLBoiler, xormについて比較していきます。

結果サマリ

AutoMigrationを始めとした各機能の説明は追って説明していきます。

Name 自動生成の経路 AutoMigration Schemaからのリバース Relation機能のライブラリ提供
GORM Struct -> Schema生成 -
SQLBoiler Schema -> Struct生成 - -
XORM Schema -> Struct生成 - -

バージョン情報

  • Go v1.12.9
  • ライブラリ
    • gorm v1.9.10
    • sqlboiler v3.5.0
    • xorm v0.7.6
  • PostgreSQL 11.5

比較

GORM

  • GORM
  • 特徴
    • GoのDBレイヤーライブラリとしては最もGitHubのスター数が多い (14500+)
    • オートマイグレーション機能がある。 (※後述)
  • 所感
    • 分かりやすく、直感的で非常に使いやすい
    • structは自分で書く必要がある(DBからコードに落としてくれる機能はない)

それではコードレベルで紹介していきます。

テーブル定義

以下のような usersテーブルで存在するとします。

postgres=# \d
List of relations
Schema | Name | Type | Owner
--------+-------+-------+----------
public | users | table | postgres
(1 row)

postgres=# \d users
Table "public.users"
Column | Type | Collation | Nullable | Default
--------+------+-----------+----------+---------
name | text | | |

CRUDサンプル

GORM経由でCRUDを行ってみます。

CURDサンプルコード
package main

import (
"fmt"

"github.com/jinzhu/gorm"
_ "github.com/lib/pq"
)

// User
type User struct {
Name string
}

// Array of User
type Users []User

func main() {
db, err := gorm.Open("postgres", "host=localhost port=15432 user=postgres sslmode=disable")
if err != nil {
// TODO error handling
}
defer db.Close()

// INSERT
db.Create(&User{Name: "hoge"})

// SELECT 1
users := Users{}
db.Find(&users) // SELECT * FROM users

// SELECT 2
user := User{}
db.Take(&user) // SELECT * FROM users LIMIT 1;

// UPDATE
db.Model(&user).Update("Name", "huga")

// DELETE
db.Delete(&user)
}

オートマイグレーション機能

Goの構造体とDBのスキーマを比較して、不足しているものを追加してくれる機能です。DBスキーマを簡単に作成してくれるので非常に有用です。しかし、カラムの削除や変更は出来ません。

例えば、UserテーブルにNameだけでなく年齢(Age)も足したくなった場合、Userの構造体を更新し、db.Automigrate(&User) とするだけで自動的にカラムが追加されます。ではコードを見ていきましょう。

まず、Structに属性を追加します。

usersの更新のためのStruct
type User struct {
Name string
Age uint
}

次に、オートマイグレーションのための AutoMigrate を呼び出します。

func main() {
db, err := gorm.Open("postgres", "host=localhost port=15432 user=postgres sslmode=disable")
if err != nil {
// TODO error handling
}
defer db.Close()
// オートマイグレーション
db.AutoMigrate(&User{})
}

次にDBのスキーマを確認します。

postgres=# \d users
Table "public.users"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
name | text | | |
age | integer | | |

テーブルにageカラムが追加されていることが分かりました。簡単ですね。

Relation(Association)

GORMは簡単にRelation(Association)を組むことが出来ます。今回はUserがCreditCardを複数枚持つようなHasManyの関係を作ります。

※Userテーブルなしの状態(コンテナ作り立ての状態)からAutoMigrationします(参考)

構造体を以下のように定義します。

Relationサンプル
type User struct {
Name string
ID uint
CreditCards []CreditCard
}

type CreditCard struct {
UserID uint
Number string
ID uint
}

そしてオートマイグレーションを実施してDBのスキーマを更新します。

func main() {
db, err := gorm.Open("postgres", "host=localhost port=15432 user=postgres sslmode=disable")
if err != nil {
// TODO error handling
}
defer db.Close()
db.AutoMigrate(&User{},&CreditCard{})
}

psqlで結果を確認数と、外部キーは貼られていない気が・・しますね。

postgres=# \d
List of relations
Schema | Name | Type | Owner
--------+---------------------+----------+----------
public | credit_cards | table | postgres
public | credit_cards_id_seq | sequence | postgres
public | users | table | postgres
public | users_id_seq | sequence | postgres
(4 rows)

postgres=# \d credit_cards
Table "public.credit_cards"
Column | Type | Collation | Nullable | Default
---------+---------+-----------+----------+------------------------------------------
user_id | integer | | |
number | text | | |
id | integer | | not null | nextval('credit_cards_id_seq'::regclass)
Indexes:
"credit_cards_pkey" PRIMARY KEY, btree (id)

postgres=# \d users
Table "public.users"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+-----------------------------------
name | text | | |
id | integer | | not null | nextval('users_id_seq'::regclass)
Indexes:
"users_pkey" PRIMARY KEY, btree (id)

リレーションサンプル

ユーザのIDからCreditCardの情報を得てみます。

// INSERT
db.Create(&User{Name: "hoge",
ID:20,
CreditCards: []CreditCard{
{Number:"1x", ID:1},
{Number:"2x", ID:2},
},
})

// SELECT
u := User{ID: 20}
cs := []CreditCard{}
db.Model(&u).Related(&cs)

fmt.Println(cs) // -> [{20 1x 1} {20 2x 2}]

GORMの所感

  • 全体的に分かりやすく、直感的で非常に使いやすい
  • structは自分で書く必要がある(DBからコードに落としてくれる機能はない)
  • 記事には載せていませんが、構造体にgorm.Modelを定義することで、ID,CreatedAt,DeletedAtのカラムが追加され、論理削除となる機能もある
    • O/Rマッパでこういったレイヤーまでサポートしてくれるのは面白いですね

SQLBoiler

  • SQLBoiler
  • 特徴
    • 高速(らしい) 参考:sqlboilerのベンチマーク
    • SQLとの接続部分は自前で実装する必要がある
    • DBからコードを自動生成するためにtomlファイルを書く必要がある。
    • SQL文の自動生成がメイン機能

テーブル定義

サンプルで用いるusersテーブルとシーケンスです。

primary keyがないテーブルにはSQLBoilerは使用できない(Error: unable to initialize tables: primary key missing in tables) のでご注意を。

サンプルで利用するDBテーブル
postgres=# \d
List of relations
Schema | Name | Type | Owner
--------+--------------+----------+----------
public | users | table | postgres
public | users_id_seq | sequence | postgres
(2 rows)

postgres=# \d users
Table "public.users"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+-----------------------------------
name | text | | |
id | integer | | not null | nextval('users_id_seq'::regclass)
Indexes:
"users_pkey" PRIMARY KEY, btree (id)

自動生成

SQLBoilerは、DBスキーマとTOMLファイルから生成されたパッケージをインポートして使うのが基本となります。

まずは、プロジェクトのルートにsqlboiler.tomlを置きます。そのtomlファイルにDBの接続先情報やオプションなどの設定を書いていきます。

pkgname="db"
output="app/db"
add-global-variants=true
add-panic-variants=true
[psql]
dbname="postgres"
host="localhost"
port=15432
user="postgres"
sslmode="disable"
  • TOMLファイルにadd-global-variants=trueを記述すると、グローバルに設定したコネクションを用いたDBの操作メソッドが追加されます。
  • add-panic-variants=trueを記述すると、error発生時にerrorを返す代わりにpanicを起こすDBの操作メソッドが追加されます。

TOMLを書いたらコード生成に必要なパッケージをインストールしていきます。

インストール
go get -u github.com/volatiletech/sqlboiler
go get -u github.com/volatiletech/sqlboiler/drivers/sqlboiler-psql

インストールしたらいざコード生成を行います。

sqlboiler --wipe psql

--wipeはコード生成前する前にoutputフォルダがあった際、そのフォルダを削除するフラグです。つけておいて損はほぼなさそうです。

生成後のプロジェクトファイル構造はこのようになります。(main.goは自分で作成したものです)

.
└─main.go
└─sqlboiler.toml
└─app
└─db
└─boil_main_test.go
└─boil_queries.go
└─boil_queries_test.go
└─boil_suites_test.go
└─boil_table_names.go
└─boil_types.go
└─psql_main_test.go
└─psql_suites_test.go
└─psql_upsert.go
└─users.go
└─users_test.go

CRUDサンプルコード

自動生成したコードを用いてCRUDアクセスします。

CRUDサンプルコード
package main

import (
"context"
"database/sql"
"fmt"

"./app/db"
_ "github.com/lib/pq"
"github.com/volatiletech/null"
"github.com/volatiletech/sqlboiler/boil"
)

func main() {
con, err := sql.Open("postgres", "host=localhost port=15432 user=postgres sslmode=disable")
if err != nil {
// TODO error handling
}
boil.SetDB(con)

// INSERT
user := db.User{Name: null.StringFrom("hoge"), ID: 1}
user.InsertGP(context.Background(), boil.Infer())

// SELECT
users := db.Users().AllGP(context.Background())

// UPDATE
user = db.User{ID:1, Name: null.StringFrom("huga")}
user.UpdateGP(context.Background(), boil.Infer())

// DELETE
user.DeleteGP(context.Background())
}

Relation

コード生成の段階でRelationを貼っておく必要があります。本当は検証のためにGORMのAutoMigrationでサクッと外部キーを貼ろうとしたがAutoMigrationでは貼ってくれないようなので自分でテーブルを作ります。

# create table users ( id serial primary key, name text );
# create table credit_card ( id serial primary key, number text, user_id int references users(id) );

上記スキーマでテーブルを作りデータを投入します。

user := db.User{Name: null.StringFrom("hoge"), ID: 3}
c1 := db.CreditCard{Number: null.StringFrom("n"), ID: 21}
c2 := db.CreditCard{Number: null.StringFrom("n2"), ID: 22}

user.InsertGP(context.Background(), boil.Infer())
user.SetCreditCardsGP(context.Background(), true, &c1, &c2)

postgres側でレコードの確認します。

postgres=# select * from credit_card;
id | number | user_id
----+--------+---------
21 | n | 3
22 | n2 | 3
(2 rows)

postgres=# select * from users;
id | name
----+------
3 | hoge
(1 row)

適切にレコードが追加されていますね。

以下はdbからuserのレコードを一件持ってきて、userに結びついているcardを持ってくるコードです。
かなり直感的に書けます。コード読んで何しているか分かりやすいです。

users := db.Users().OneGP(context.Background())
cards := users.CreditCards().AllGP(context.Background())

備考:Tips null.StringFromやnull.IntFromの話

db操作する際にstringを入れる事はできず、null.StringFromを利用する必要がありましたが、なぜそうなっているのか、どのような振る舞いをするのか調査しました。

そこで、定義を見に行きました。

func StringFrom(s string) String

ぱっと見「???」となるのですがよく見ると、string を引数にして String を返しています。

そして String は以下のように定義されています。

type String struct {
sql.NullString
}

sql.NullStringを包んでいますね。sql.NullStringの定義を確認してみます。

type NullString struct {
String string
Valid bool // Valid is true if String is not NULL
}

Validの値を見て値がNULLかどうかを判別しています。

なぜこのような実装になっているかというと、golangにはnilがありますが、pointer型にしか使えないからです。

例えば、stringのゼロ値は””となり、NULLとの区別をつけることが出来ません。

このようにGoの型定義とSQLの型定義には差異があるためその差を埋めるためにnullパッケージが誕生し、それを介することでNULLの表現を可能にしています。

SQLBoiler所感

  • contextを明示的に用いているため非同期処理が比較的簡単に出来る。
  • tomlにすでに接続先情報が書かれているので正直sql.Openで再度接続先を明記するのは二度手間に感じた。
  • Relationは個人的にgormよりも直感的に扱えると感じた。

xorm

  • xorm
  • 特徴
    • 生のSQL実行をサポートしている。
    • コマンドラインツールが提供されている
      • DBからコードを生成する機能などを持つ
    • 生成コードにテスト用コードも付属しているのが良い

テーブル定義

SQLBoilerと同じスキーマを利用します。折角なのでコマンドラインツールを用いてDBから構造体の生成もやります。

まずは go get github.com/go-xorm/cmd/xorm でインストールします。

その後、このコマンドで構造体を作成します。

cd $GOPATH/src/github.com/go-xorm/cmd/xorm
xorm reverse postgres "dbname=postgres host=localhost port=15432 user=postgres sslmode=disable" templates/goxorm

./models/users.go が出来ていると思います。それを自分のプロジェクトフォルダに持ってくればOK。

生成されたusers.goの中身はこちらになりました。

users.go
package models

type Users struct {
Name string `xorm:"TEXT"`
Id int `xorm:"not null pk autoincr INTEGER"`
}

それではこのコードを用いてCRUDしてみます。

CRUDサンプル

xormのCRUDサンプル
package main

import (
"fmt"

model "./models"
"github.com/go-xorm/xorm"
_ "github.com/lib/pq"
)

func main() {
engine, err := xorm.NewEngine("postgres", "dbname=postgres host=localhost port=15432 user=postgres sslmode=disable")
if err != nil {
// TODO error handling
}
defer engine.Close()

// INSERT
user := model.Users{Id: 3, Name: "hoge"}
engine.Insert(&user)

// SELECT
var users []model.Users
engine.Find(&users)

// UPDATE
user.Name = "huga"
engine.ID(3).Update(&user)

// DELETE
engine.ID(3).Delete(&user)
}

xorm所感

まとめ

  • GoのO/Rマッパである、GORM, SQLBoiler, xormについて比較した
  • O/Rマッパと名乗っていても、オートマイグレーション機能や、コードの自動生成機能、論理削除など各ライブラリ特有の差別化要素がある
  • 構造体からSchemaを生成してくれるAutoMigrationはとても魅力的な機能ですが、過信せず生成されたDBスキーマを確認する事が大切

今回わたしのPJでは社内ナレッジが蓄積されている点でGORMを採用しました(CRUD操作が直感的で取っ付きやすいので個人的にも良いと思いました)。実際の開発や運用を通して得られたナレッジなどは別途ブログ化したいと思っています。

作成したコード

https://github.com/reud/blog-orm

参考