11
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Go6Advent Calendar 2019

Day 16

sqlxでJOINした時に複数のstructをScanするためのライブラリを作った

Last updated at Posted at 2019-12-15

TL;DR

  • sqlxでJOINで複数のstructのSELECTをするクエリを生成するライブラリを作った
  • sqlx-selector

何を作ったか

GoでSQLを使う時、標準ライブラリに加えていくつかのサードパーティライブラリを使う選択肢があるかと思いますが、私はsqlxがGoらしさもあり、気に入って使っています。ご存知の方が多いかと思いますが軽く説明すると、ORMのようなゴツさはなく、クエリ自体は自分で書きSELECTの結果をstructに突っ込む時にdbタグを参照していい感じに代入してくれるものです。

非常に便利なのですが、複数のテーブルをJOINしてSELECTしたい場合少し面倒なことになります。例として以下のようなテーブルを考えます。

CREATE TABLE users (
    id INTEGER,
    name VARCHAR(128),
    org_id INTEGER
);
CREATE TABLE organizations (
    id INTEGER,
    name VARCHAR(128)
);

これをsqlxで扱う際は以下のようなstructを用意すると思います。

type User struct {
    ID string `db:"id"`
    Name string `db:"name"`
    OrganizationID string `db:"org_id"`
}
type Organization struct {
    ID string `db:"id"`
    Name string `db:"name"`
}

それぞれのテーブルをSELECTする場合は問題ありませんが、JOINを行う以下のようなクエリ場合を考えます。

SELECT * FROM users AS u INNER JOIN organizations AS org ON u.org_id = org.id;

これを実行すると、以下のようなデータが返ってきますが、id カラムはどちらのテーブルであるかを判断することができないため、sqlxで正常にスキャンすることができません。

+------+-----------+--------+------+----------+
| id   | name      | org_id | id   | name     |
+------+-----------+--------+------+----------+
|    0 | user name |      0 |    0 | org name |
+------+-----------+--------+------+----------+

sqlxのREADMEのissue を見るとASを使って個別に名前を与えてやればScanできるとあります。別途structを用意することもできますが、以下のようなstructとクエリを用意することでも正常にScanできるようになります。

var v struct {
    User *User `db:"u"`
    Org *Organization `db:"org"`
}
SELECT 
    u.id AS "u.id",
    u.name AS "u.name",
    u.org_id AS "u.org_id",
    org.id AS "org.id",
    org.name AS "org.name"
FROM users AS u INNER JOIN organizations AS org ON u.org_id = org.id;

sqlxは再帰的にstructを辿って値を入れてくれるようです。しかしこれを毎度書くのは非常に骨が折れるので代替策を考えます。本当はu.* AS "u.*" みたいなことがしたいですがこれはできないようです。

sqlx-selector

以上を踏まえて今回作ったライブラリが、sqlx-selectorです。これを使うと、

var joined struct {
    User          *User         `db:"u"`
    Organization  *Organization `db:"org"`
}

fmt.Println(
    `SELECT ` +
        sqlxselect.New(&joined).
            SelectStructAs("u.*", "u.*", "id", "name"). // select only id and name
            SelectStructAs("org.*", "org.*").
            String() +
        ` FROM users AS u INNER JOIN organizations AS org ON u.org_id = org.id LIMIT 1`,
)

のように書くことができます。SelectStructAsでは第一引数にSQLのテーブル名、第二引数にstructのパスを渡し、特定のカラムのみselectする場合は可変長引数でその後に渡せます。今回はあえてSelectStructAsを利用していますが、第一引数と第二引数が同じであればSelectStruct("org.*")でも大丈夫です。

これを実行すると以下のようなクエリが生成されます。

SELECT `u`.`updated_at` AS "updated_at",`u`.`id` AS "u.id",`u`.`name` AS "u.name",`org`.`id` AS "org.id",`org`.`name` AS "org.name" FROM users AS u INNER JOIN organizations AS org ON u.org_id = org.id LIMIT 1

Go Playgroundでの実行結果

実装など

sqlxが内部でdb tagをトラバースするためのライブラリとしてgithub.com/jmoiron/sqlx/reflectxがあります。
これを利用するとstructをトラバースしてフィールド一覧を、必要ならば特定のタグ(今回はdb)で指定された値で置き換えて取得することができます。例えば上の例では、User.IDu.idになるような感じです。この機能を利用して実装しました。reflectxの内部的には名前の通りreflectバリバリのようです。

まとめ

sqlxで唯一気になっていた点を補うことができるライブラリを作れて個人的には満足しています。とはいえ少しGoっぽくないライブラリになってしまったかもしれない、という思いもあります。今後も更に改善をしていきたいです。

お読みいただきありがとうございました。

11
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?