GenORM
Go言語のジェネリクスを利用したSQLのミスを防ぐSQL Builder
リポジトリ: https://github.com/mazrean/genorm
特徴
ジェネリクスを用いてSQLの表現に適切なGoの型を対応づけることで
- Goの型が異なる値をSQL内で
=
などでの比較やUPDATEでの値の更新に使用した際、コンパイルエラーとなる - 使用できないテーブルのカラム名を使用した際にコンパイルエラーとなる
など、コンパイルの時点で従来のGo言語のORMやクエリビルダーでは防げなかった多くのSQLのミスを発見できます。
また、SQLの多くのCRUDの構文をサポートしています。
例1
文字列のカラム`users`.`name`
はstring
の値と比較することはできますが、int
の値と比較するとコンパイルエラーとなります。
// correct
userValues, err := genorm.
Select(orm.User()).
Where(genorm.EqLit(user.NameExpr, genorm.Wrap("name"))).
GetAll(db)
// compile error
userValues, err := genorm.
Select(orm.User()).
Where(genorm.EqLit(user.NameExpr, genorm.Wrap(1))).
GetAll(db)
例2
users
テーブルに対するSELECT
文でusers
テーブルのid
カラムは使用できますが、messages
テーブルのid
カラムを使用するとコンパイルエラーとなります。
// correct
userValues, err := genorm.
Select(orm.User()).
Where(genorm.EqLit(user.IDExpr, uuid.New())).
GetAll(db)
// compile error
userValues, err := genorm.
Select(orm.User()).
Where(genorm.EqLit(message.IDExpr, uuid.New())).
GetAll(db)
既存のツールとの違い
既存のGORMやentとの違いについて説明します。
GORM
GORMではinterface{}
を利用して柔軟に引数を受け入れることで、直感的にクエリを組み上げられるようになっています。
反面、型による制約が非常に弱くなっています。
このため、コンパイル時に弾けるバグが少なく、バグが混入しやすくなります。
また、実際に実行されるSQLがわかりづらく、想定したのと異なる挙動をしやすい、という問題もあるように思います。
対して、GenORMは型による制約でコンパイル時にできる限り多くのバグを見つけることができます。 また、Goのコードから実行されるSQLがイメージしやすいようにすることも意識しており、ここまでの例のコードでもコードから実行するSQLがイメージしやすかったのではないかと思います。
このように、GenORMとGORMでは思想、機能ともに大きく異なります。
ent
entとの機能面での最も大きな違いは使用できるGo言語の型と考えています。
entではint、boolなどのプリミティブ型に対応する型にtime.TimeやUUIDなどを加えた、有限個の型のみが使用できます。
対して、GenORMではgenorm.ExprType
interfaceの条件を満たす任意の型を使用し、その上で同一の型でのみ比較可能、などの制約が設定されています。
これにより、不要な型変換を行う必要がなく、また、Defined Typeを用いてより強力な制約を設定できるようになっています。
詳細や例はDefined Typeでみることができます。
また、GenORMではSQLに近いメソッドチェーンでクエリを構築できる点も重要です。 entは「entity framework」であるため、データベースの操作がentityの操作として抽象化されています。 これはメリットもありますが、GORMと同様に実行されるSQLがわかりづらくなるという側面もあります。 これは、意図せずパフォーマンスに問題のある処理を書いてしまう確率を上昇させてしまいます。 この点、GenORMでは実行されるSQLがわかりやすいため、このような問題は起こりづらいでしょう。
仕組み
コード例の動作の仕組みを解説します。
genorm.EqLit
の定義は
func EqLit[T Table, S ExprType](
expr TypedTableExpr[T, S],
literal S,
) TypedTableExpr[T, WrappedPrimitive[bool]] {
// 省略
}
のようになっています。
注目してほしいのがTypedTableExpr[T, S]
の部分です。
[T Table, S ExprType]
の部分からもわかるように、T
はSQLのexpressionが使用しているテーブルを表す型、S
はSQLのexpressionに対応するGo言語の型となっています。
このように、GenORMではSQLのexpressionに使用しているテーブルとGo言語の対応する型を型パラメーターとして持たせることで、使用可能なカラムや比較して良い値に制限をかけています。
このように、SQLのexpressionに型をつけているため、>
や<
、AND
などの演算子、COUNT()
などの関数、さらに将来的にはデータベース独自の関数にも制限をかけてSQLを実行できます。