Help us understand the problem. What is going on with this article?

GolangでTengを使いたかった

TuneCoreJapan 新卒エンジニアの skfvr です。

この記事は WanoグループAdvent Calendar 2019 3日目の記事です。

今回はPerlの軽量ORMである「Teng」をGolangで同じように使いたく自作しようとした話になります。

諸注意

  • 本記事はPerl/Golangのいずれかを持ち上げる/持ち下げる目的の記事でないことを予めお断りしておきます。
  • 業務とは関係ありません。
  • 初投稿故、誤字/脱字/知識不足を暖かく見守っていただけると幸いです。知識不足の祭は指摘していただけると嬉しいです。

1.Teng とは

まずはTengから。TengとはPerlでの軽量のORMです。例えば次のように書けるわけです(一部簡略化してます)。

example.pl
my $hoge = $teng->single('hoge_table',{
    id => 1,
});
# => select * from hoge_table where id = 1

簡単なクエリなら簡潔な表現で記述できるのでそういったクエリを叩きたいときには使いやすいです。また直接クエリを書いて叩くことも出来ます。

example.pl
my $hoge = $teng->search_by_sql("select * from hoge_table where id = 1");

初めて使ったときの僕は「GolangでもTengみたいにクエリ書けたら最強じゃん」みたいな短絡的な思考をしていました(呆れ)。

ちなみにこの時点でもGo言語には「gorm」や「sqlx」など使いやすくORMが色々あるので「あまり作るメリットは実務的にはないのでは?」という感じです。

2.先に紹介

先にどういうものが出来上がってしまったのかを紹介します(簡略化して書いてるので汲んでもらえると幸いです)。

example_select.go
    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";
example_update.go
    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で自分がよく見るクエリジェネレーターとしてはこういうやつだと思います。改行を怠らなければこっちのほうが分かりやすいですね。

yokuaru_example.go
sql, args, err := generator.Select(TableNameHogeTable)
.Where("delete_flg", "=", 0)
.Where("name", "=", "hoge")

3. しんどいポイントの紹介

作るに当たってしんどかったなぁという点を紹介します。
圧倒的にしんどい箇所としては「型」です。

一番しんどかったです。

MySQLの型からGolangの型に変換しなければなりません。

またPerlでは基本的にはスカラ・ハッシュ・配列を抑えておけばなんとかなりますので例えば次のように書けました。

example.pl
my $hoge = $teng->single('hoge_table',{
    number  => 25,
    name    => "tutugo"
});

この時 number と name では「数値」と「文字列」ではありますがPerl的にはそんなもん関係ありません。
Golangでやる場合は interface{} に頼ることになります。今回の例では st.Statement.Valueinterface{} になっていることで1つの引数でもスライスでも受け取ってそこからクエリを作ろうという目論見でした。

今回の実装だといちいち st.Statement という構造体を挟まないといけないのでいまいちしっくりこない感じですね...。

example_select.go
    sql, args, err := dao.Select(model.TableNameHogeTable, model.HogeTableParam{
        Number: st.Statement{Operation:"=", Value:25},
        Name:     st.Statement{Operation:"=", Value:"tutugo"},
    }).SQL()

理想としてはやはりこんな感じでしょうか(処理がとてつもなく面倒くさそう)。

feture.go
    sql, args, err := dao.Select(model.TableNameHogeTable, model.HogeTableParam{
        Number: 25,
        Name:"tutugo",
    }).SQL()

ゼロ値処理

ここはGolangのなかなかしんどい箇所だと思います。簡単に言えば

  • 数値の0
  • 空文字列 ""
  • boolean の false

などはゼロ値として処理される悲しい運命が有ります。例えば json タグの omitempty を入れている場合は json.encode 時に要素そのものが消滅します。有名な gorm でもゼロ値を扱いたい場合は NullInt を使えと推奨される感じです。

複雑になるとしんどい

ここまでのやつでは全てこんな感じのコードでした。単純なクエリばかり紹介してきました。

example_select.go
    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 みたいに書きたくなったらどうするんだという話ですね...。

hoge.go
    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 が十分に使えない

結構ヤバいポイントかもしれません。例えば

ex.sql
select * from hoge_table
where ( id = 1 and name = "kuwahara")
or
( id = 0 and name = "nakai")

のようなクエリはまだ書けない状態です。後先考えずにこういう実装をしてしまったことが原因です。

example_select.go
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 や クエリジェネレーター に関わっている方すごい。
  • 落ち着けば仮完成させて放出してみたい(謎の自信)。

final. 参考リンク

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした