TuneCoreJapan 新卒エンジニアの skfvr です。
この記事は WanoグループAdvent Calendar 2019 3日目の記事です。
今回はPerlの軽量ORMである「Teng」をGolangで同じように使いたく自作しようとした話になります。
諸注意
- 本記事はPerl/Golangのいずれかを持ち上げる/持ち下げる目的の記事でないことを予めお断りしておきます。
- 業務とは関係ありません。
- 初投稿故、誤字/脱字/知識不足を暖かく見守っていただけると幸いです。知識不足の祭は指摘していただけると嬉しいです。
1.Teng とは
まずはTengから。TengとはPerlでの軽量のORMです。例えば次のように書けるわけです(一部簡略化してます)。
my $hoge = $teng->single('hoge_table',{
id => 1,
});
# => select * from hoge_table where id = 1
簡単なクエリなら簡潔な表現で記述できるのでそういったクエリを叩きたいときには使いやすいです。また直接クエリを書いて叩くことも出来ます。
my $hoge = $teng->search_by_sql("select * from hoge_table where id = 1");
初めて使ったときの僕は「GolangでもTengみたいにクエリ書けたら最強じゃん」みたいな短絡的な思考をしていました(呆れ)。
ちなみにこの時点でもGo言語には「gorm」や「sqlx」など使いやすくORMが色々あるので「あまり作るメリットは実務的にはないのでは?」という感じです。
2.先に紹介
先にどういうものが出来上がってしまったのかを紹介します(簡略化して書いてるので汲んでもらえると幸いです)。
sql, args, err := dao.Select(model.TableNameHogeTable, model.HogeTableParam{
DeleteFlg: st.Statement{"=", false},
Name: st.Statement{"=", "hoge"},
}).SQL()
// => select * from hoge_table where delete_flg = 0 and name = "hoge";
sql, args, err := dao.Update(model.TableNameHogeTable, model.HogeTableSetter{
Name: st.Setter{"hoge"},
}, model.HogeTableParam{
ID: st.Statement{"=", 225},
})
// => update hoge_table set name = "hoge" where id = 225;
オレオレ感あるものが出来てしまった...。
ちなみにGolangで自分がよく見るクエリジェネレーターとしてはこういうやつだと思います。改行を怠らなければこっちのほうが分かりやすいですね。
sql, args, err := generator.Select(TableNameHogeTable)
.Where("delete_flg", "=", 0)
.Where("name", "=", "hoge")
3. しんどいポイントの紹介
作るに当たってしんどかったなぁという点を紹介します。
圧倒的にしんどい箇所としては「型」です。
型
一番しんどかったです。
MySQLの型からGolangの型に変換しなければなりません。
またPerlでは基本的にはスカラ・ハッシュ・配列を抑えておけばなんとかなりますので例えば次のように書けました。
my $hoge = $teng->single('hoge_table',{
number => 25,
name => "tutugo"
});
この時 number と name では「数値」と「文字列」ではありますがPerl的にはそんなもん関係ありません。
Golangでやる場合は interface{}
に頼ることになります。今回の例では st.Statement.Value
が interface{}
になっていることで1つの引数でもスライスでも受け取ってそこからクエリを作ろうという目論見でした。
今回の実装だといちいち st.Statement
という構造体を挟まないといけないのでいまいちしっくりこない感じですね...。
sql, args, err := dao.Select(model.TableNameHogeTable, model.HogeTableParam{
Number: st.Statement{Operation:"=", Value:25},
Name: st.Statement{Operation:"=", Value:"tutugo"},
}).SQL()
理想としてはやはりこんな感じでしょうか(処理がとてつもなく面倒くさそう)。
sql, args, err := dao.Select(model.TableNameHogeTable, model.HogeTableParam{
Number: 25,
Name:"tutugo",
}).SQL()
ゼロ値処理
ここはGolangのなかなかしんどい箇所だと思います。簡単に言えば
- 数値の0
- 空文字列
""
- boolean の
false
などはゼロ値として処理される悲しい運命が有ります。例えば json タグの omitempty を入れている場合は json.encode 時に要素そのものが消滅します。有名な gorm でもゼロ値を扱いたい場合は NullInt
を使えと推奨される感じです。
複雑になるとしんどい
ここまでのやつでは全てこんな感じのコードでした。単純なクエリばかり紹介してきました。
sql, args, err := dao.Select(model.TableNameHogeTable, model.HogeTableParam{
Number: st.Statement{Operation:"=", Value:25},
Name: st.Statement{Operation:"=", Value:"tutugo"},
}).SQL()
// => select * from hoge_table were number = 25 and name = "tutugo"
ここで例えば number between 10 and 30 and number not between 20 and 25
みたいに書きたくなったらどうするんだという話ですね...。
sql, args, err = dao.Select(model.TableNameHogeTable, &model.HogeTableParam{
ID: st.StatementAnd{
st.Statement{"between", []int{10, 30}},
st.Statement{"not between", []int{20, 25}},
},
}).SQL()
// => select * from hoge_table where
// ( ( id between ? and ? and id not between ? and ? ) )
// [10 30 20 25]
//
/ || ̄ ̄|| ∧_∧
|.....||__|| ( ^ω^ ) ...?
| ̄ ̄\三⊂/ ̄ ̄ ̄/
| | ( ./ /
だんだん無茶苦茶感出てきてしまいました...。特に statementAnd
と言ったクサいものが出現してきてしまいました。
4. 課題点
or が十分に使えない
結構ヤバいポイントかもしれません。例えば
select * from hoge_table
where ( id = 1 and name = "kuwahara")
or
( id = 0 and name = "nakai")
のようなクエリはまだ書けない状態です。後先考えずにこういう実装をしてしまったことが原因です。
func dao.Select(tableName string, model.Param)
type Hoge struct{
ID int
Name string
Age int
}
type HogeParam struct{
ID StatementInterface
Name StatementInterface
Age StatementInterface
}
要は Param 「s」ができないわけですね...。一回クエリを作る際に1つの model.param
しか選択できません。
こういう細かいところはまだまだ改善点だと思います。
Joinができない
流石にここまで来ると顔が (・_・) になります。
Joinを考えるとなると2つのしんどそうなポイントとしては
-
table_name.id
のような表記にする必要がある -
join hoge h on ごにょごにょ
の「ごにょごにょ」を書けるようにする必要がある。- (おおよそは where 以下の部分と同じだし案外何とかなると思っている)
と言ったところでしょうか。全てを自作ジェネレーターで賄うにはまだまだ時間がかかりそうです...。
5.感想
- reflect すごい。
- 様々な言語で orm や クエリジェネレーター に関わっている方すごい。
- 落ち着けば仮完成させて放出してみたい(謎の自信)。