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.ExprTypeinterfaceの条件を満たす任意の型を使用し、その上で同一の型でのみ比較可能、などの制約が設定されています。 これにより、不要な型変換を行う必要がなく、また、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を実行できます。

results matching ""

    No results matching ""