フューチャー技術ブログ

Go言語で2WaySQL

はじめに

この記事はORMとクエリビルダー連載の7記事目です。

こんにちは。2021年4月入社の本田です。私が当社でアルバイトをしていたときに作成したtwowaysqlというツールについて紹介します。

JavaではuroboroSQLDOMAなど広く使われている2WaySQLライブラリが存在しますが、Goでこれがデファクトスタンダートというものは現在のところ存在しません。

そのためGo言語でtwowaysqlという2WaySQLライブラリを実装することにしました。

2WaySQLとは

2WaySQLとは2つの実行方法を持つSQL文のことです。コメントとして/* IF */, /* ELIF */, /* ELSE */, /* END */という制御構造や、/* value */のようにバインドしたい変数を記述します。

2WaySQLはtwowaysqlで処理してから実行することもできますし、そのままSQLクライアントツールで実行することもできます。

使用例

package main

import (
"context"
"fmt"
"log"

"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
"gitlab.com/osaki-lab/twowaysql"
)

type Person struct {
EmpNo int `db:"employee_no"`
DeptNo int `db:"dept_no"`
FirstName string `db:"first_name"`
LastName string `db:"last_name"`
Email string `db:"email"`
}

type Params struct {
Name string `twowaysql:"name"`
EmpNo int `twowaysql:"EmpNo"`
MaxEmpNo int `twowaysql:"maxEmpNo"`
DeptNo int `twowaysql:"deptNo"`
}

func main() {
ctx := context.Background()

db, err := sqlx.Open("postgres", "user=postgres password=postgres dbname=postgres sslmode=disable")

if err != nil {
log.Fatal(err)
}
defer db.Close()

tw := twowaysql.New(db)

var people []Person
var params = Params{
MaxEmpNo: 2000,
DeptNo: 15,
}

err = tw.Select(ctx, &people, `SELECT * FROM persons WHERE employee_no < /*maxEmpNo*/1000 /* IF deptNo */ AND dept_no < /*deptNo*/1 /* END */`, &params)
if err != nil {
log.Fatalf("select failed: %v", err)
}

fmt.Printf("%#v\n%#v\n%#v", people[0], people[1], people[2])
//Person{EmpNo:1, DeptNo:10, FirstName:"Evan", LastName:"MacMans", Email:"evanmacmans@example.com"}
//Person{EmpNo:3, DeptNo:12, FirstName:"Jimmie", LastName:"Bruce", Email:"jimmiebruce@example.com"}
//Person{EmpNo:2, DeptNo:11, FirstName:"Malvina", LastName:"FitzSimons", Email:"malvinafitzsimons@example.com"}
}

twowaysqlの現在の公開関数

Eval

Evalは2WaySQLクエリとパラメータを受け取ってプリペアードステートメントとして実行できるクエリとパラメータの組を返します。

Evalのシグネチャは以下のようになっています。inputParamsにはタグ付き構造体を渡します。twowaysql:"tag_name"という形式でなくてはいけません。タグがついていない場合はプロパティの名前がキーとして使われます。

func Eval(inputQuery string, inputParams interface{}) (string, []interface{}, error) {}

使用例

コード

type Params {
Name string `twowaysql:"name"`
MaxEmpNo int `twowaysql:"maxEmpNo"`
DeptNo int `twowaysql:"deptNo"`
GenderList []string `twowaysql:"gender_list"`
IntList []int `twowaysql:"int_list"`
}

var query = `SELECT * FROM person WHERE employee_no = /*maxEmpNo*/1000 AND /* IF int_list !== null */ person.gender in /*int_list*/(3,5,7) /* END */`
var params = Params{
Name: "Jeff",
MaxEmpNo: 3,
DeptNo: 12,
GenderList: []string{"M", "F"},
IntList: []int{1,2,3},
}
resultQuery, resultParams, err := Eval(query, params)

期待出力

resultQuery: `SELECT * FROM person WHERE employee_no = ?/*maxEmpNo*/ AND person.gender in (?, ?, ?)/*int_list*/`
resultParams: []interface{}{3, 1, 2, 3}

Exec

ExecはsqlパッケージのExecContext関数の薄いラッパーです。

検索結果を取得しない場合(INSERT, UPDATE, DELETE等)に使われます。関数内部で、Evalの呼び出し、適切なプレースホルダーへの変換を行い、sqlパッケージのExecにクエリとパラメータを渡しています。

Select

SelectはsqlxパッケージのSelectContext関数の薄いラッパーです。検索結果を取得する場合(SELECT)に使われます。

twowaysqlの機能

バインドパラメータ

SQLにバインドするパラメータを/* parameter name */の形式で使用できます。

SELECT * from person where dept_no = /* deptNo */10 AND first_name = /* firstName */"Jeff"

上の例では/* deptNo *//* firstName */がバインドパラメータです。Generate関数にかけられると、後ろに続く10、”Jeff”が削除され、?が追加されます。

SELECT * from person where dept_no = ?/* deptNo */ AND first_name = ?/* firstName */

IN句

スライス型の値をIN句にバインドパラメータとして指定できます。現在対応しているの型は[]stringと[]intです。

SELECT * FROM person /* IF gender_list !== null */ where person.gender in /*gender_list*/("M") /* END */

gender_listとして(“M”, “F”)を指定すると以下のように変換されます。

SELECT * FROM person where person.gender in (?, ?)/*gender_list*/

条件分岐(/* IF */, /* ELIF */, /* ELSE */, /* END */)

/* IF */, /* ELIF */, /* ELSE */, /* END */を使ってSQLを動的に制御できます。

記述方法

/*IF [評価式]*/
-- IFの評価式が真の場合に適用されるSQL
/*ELIF [評価式]*/
-- ELIFの評価式が真の場合に適用されるSQL
/*ELSE*/
-- IF,ELIFの評価式が偽の場合に適用されるSQL
/*END*/

IF、ELIFの条件はGo言語で実装されたJavaScriptインタプリタであるottoに渡されて評価されます。したがって、条件部にはJavaScriptの式を記述することが可能です。

書いてみた所感

Go言語で最低限のコア機能を持った2WaySQL処理ツールを実装してみました。

初めに2WaySQL処理ツールを実装してほしいと言われたときは、ソースコードが一行もない状態で、Goの経験と言えばA Tour Of Goをやったことがある程度の自分が書けるのだろうかと不安だったのですが、先輩方の手厚いコードレビューや、低レイヤを知りたい人のためのCコンパイラ作成入門を途中まで読んでいて、字句解析や構文解析の知識が少しだけあったという幸運もあり、何とか形にできました。

開発の経験を積んでみたい学生の方は、当社でアルバイトをすると、つよつよな技術部隊の方にコードレビューをしてもらって開発ができ、かつ給料ももらえる(私の時は時給1500円でした)ので、とてもおススメです!

次は辻さんのSQLファイルから型安全なコードを生成するsqlcです。