型安全なSQLビルダーJetの解説
Goで型安全なSQLビルダーを実現できるライブラリJetの解説
Table of contents
author: qwerty2501
はじめに
Goには様々なSQLをサポートするライブラリがありますが、その中で型安全なSQLビルダーであるJetを解説します。
利用方法については公式のREADMEを読んでいただければ大体わかるのでまずそちらを参照してください。
コンパイル時にシンタックスエラーを検知できる嬉しさ
例えば以下はJetのwikiにあるSELECTのサンプルコードです。
SELECT(
AVG(Payment.Amount).OVER(),
MINf(Payment.Amount).OVER(PARTITION_BY(Payment.CustomerID).ORDER_BY(Payment.PaymentDate.DESC())),
ROW_NUMBER().OVER(Window("w1")),
RANK().OVER(
Window("w2").
ORDER_BY(Payment.CustomerID).
RANGE(PRECEDING(UNBOUNDED), FOLLOWING(UNBOUNDED)),
),
AVG(Payment.Amount).OVER(Window("w3").ROWS(PRECEDING(1), FOLLOWING(2))),
).
FROM(Payment).
WINDOW("w1").AS(PARTITION_BY(Payment.PaymentDate)).
WINDOW("w2").AS(Window("w1")).
WINDOW("w3").AS(Window("w2").ORDER_BY(Payment.CustomerID)).
これを生SQLにすると以下のようになります。
SELECT AVG(payment.amount) OVER (),
MIN(payment.amount) OVER (PARTITION BY payment.customer_id ORDER BY payment.payment_date DESC),
ROW_NUMBER() OVER (w1),
RANK() OVER (w2 ORDER BY payment.customer_id RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING),
AVG(payment.amount) OVER (w3 ROWS BETWEEN 1 PRECEDING AND 2 FOLLOWING)
FROM dvds.payment
WINDOW w1 AS (PARTITION BY payment.payment_date), w2 AS (w1), w3 AS (w2 ORDER BY payment.customer_id);
この量のSQLを間違いなく記述する自信が筆者にはありません。
これを書き間違えたらコンパイル時に検出してくれるのは非常に嬉しいです。
テーブル定義はSQLで書く
これは好みが分かれるところですが、テーブル定義はSQLで書いて定義します。
ORMライブラリではコード上の定義と実際に実行されているテーブル定義がどのようになっているか想起しにくいと筆者は感じているのでこっちのほうが好みです。
テーブル定義を書き間違えていたとしてもDBを起動できないことで気づけるのでデメリットもそこまでないと考えています。
他のSQLビルダーと比べて
他のSQLビルダーと比べて嬉しいのはJetはテーブル定義からGoのコードを生成してくれることです。
例えばexampleにある以下のようなcategoryテーブルがあるとします。
CREATE TABLE dvds.category (
category_id integer DEFAULT nextval('dvds.category_category_id_seq'::regclass) NOT NULL,
name character varying(25) NOT NULL,
last_update timestamp without time zone DEFAULT now() NOT NULL
);
これをJetでコード生成するとmodelパッケージとtableパッケージにcategoryテーブルのコードが生成されます。
例えばcategoryテーブルに対して問い合わせたい場合は以下のようにフィールド名も含めてGoのコードで記述できます(フィールド名も含めてコンパイル時に間違いを検出できます)
query := SELECT(table.Category.AllColumns).
WHERE(table.Category.Name.LIKE(String("%カテゴリの名前%")))
var categories []model.Category
if err := query.QueryContext(ctx,&categories); err != nil{
return nil,err
}
for _,category := range categories {
fmt.Sprintf("category.name:%s",category.Name)
}
sqlcと比べて
事前にSQLを記述してそこからGoのコードを生成するライブラリとして sqlc が存在しますが、その違いとしてはよりsqlcのほうがより静的であるという点です。
例えば以下のように与えられた引数に応じてクエリを組み立てたい要求があった場合、sqlcでは対応するのが難しくなります。(sqlcには現状 動的にクエリを組み立てる仕組みはない ため)
以下の例はJetのものです。
query := SELECT(table.Category.AllColumns)
var conditions []BoolExpression
if args.Name != nil{
conditions=append(conditions,table.Category.Name.LIKE(String("%"+*args.Name+"%")))
}
if args.StartLastUpdate != nil {
conditions=append(conditions,table.Category.LastUpdate.GT_EQ(TimestampT(*args.StartLastUpdate)))
}
if args.EndLastUpdate != nil {
conditions=append(conditions,table.Category.LastUpdate.LT_EQ(TimestampT(*args.EndLastUpdate)))
}
if len(conditions) > 0{
query = query.WHERE(AND(conditions...))
}
if args.Offset != nil {
query = query.OFFSET(*args.Offset)
}
if args.Limit != nil {
query = query.LIMIT(*args.Limit)
}
var categories []model.Category
if err := query.QueryContext(ctx,&categories); err != nil{
return err
}
for _,category := range categories {
fmt.Sprintf("category.name:%s",category.Name)
}
このようにJetでは動的なクエリの組み立てにも対応できるのが利点です。
まとめ
- テーブル定義からGoのSQLビルダーコードを生成します
- ORMではないですが、テーブル定義をもとにmodelとしてstruct型を生成できます
- 生成したGoコードをもとにSQLを組み立てるので単純なシンタックスエラーはGoコードコンパイル時に検出できます
- 動的なクエリ組み立てにも対応できます
以上です。いかがでしたでしょうか?
GI Cloudは事業の拡大に向けて一緒に夢を追う仲間を募集しています
当社は「クラウドで日本のIT業界を変革し、世の中をもっとハッピーに」をミッションに掲げ、Google Cloudに特化した技術者集団として、お客様にコンサルティングからシステム開発、運用・保守まで一気通貫でサービスを提供しています。
まだ小規模な事業体ですが、スタートアップならではの活気と成長性に加えて、大手総合商社である伊藤忠グループの一員としてやりがいのある案件にもどんどんチャレンジできる環境が整っています。成長意欲の高い仲間と共にスキルを磨きながら、クラウドの力で世の中をもっとハッピーにしたい。そんな我々の想いに共感できる方のエントリーをお待ちしています。
※本記事は、ジーアイクラウド株式会社の見解を述べたものであり、必要な調査・検討は行っているものの必ずしもその正確性や真実性を保証するものではありません。
※リンクを利用する際には、必ず出典がGIC dryaki-blogであることを明記してください。
リンクの利用によりトラブルが発生した場合、リンクを設置した方ご自身の責任で対応してください。
ジーアイクラウド株式会社はユーザーによるリンクの利用につき、如何なる責任を負うものではありません。