// 実行可能
// SELECT * FROM users WHERE users.id="{{userID}}"
userValues, err := genorm.
Select(orm.User()).
Where(genorm.EqLit(user.IDExpr, userID)).
GetAll(db)
// コンパイルエラー(messagesテーブルのカラム使用)
// SELECT * FROM users WHERE messages.id="{{messageID}}"
userValues, err := genorm.
Select(orm.User()).
Where(genorm.EqLit(message.IDExpr, messageID)).
GetAll(db)
SQLのExpressionに型パラメーターで
の情報を持たせる
func EqLit[T Table, S ExprType](
expr TypedTableExpr[T, S],
literal S,
) TypedTableExpr[T, WrappedPrimitive[bool]] {
// 省略
}
CLIとライブラリで構成される。
リリース前のバージョンのコードを生成
→go/ast
をそのまま使って生成
go/format
のformat.Node
でコード生成→golang.org/x/tools/imports
imports.Process
を呼ぶだけでimportを削れる
// codeBytes: import整理前
// newCodeBytes: import整理後
newCodeBytes, err := imports.Process("", codeBytes, nil)
ミスが発生しやすい部分
→ライブラリのinterfaceを関数で表現
func typedTableExpr(tableType ast.Expr, exprType ast.Expr) ast.Expr {
return &ast.IndexListExpr{
X: &ast.SelectorExpr{
X: genormIdent,
Sel: ast.NewIdent("TypedTableExpr"),
},
Indices: []ast.Expr{
tableType,
exprType,
},
}
}
プロトタイプ
messageUserValues, err := orm.User().
Message().Join(genorm.Eq(userIDExpr, messageUserID))
Select().
Fields(userName, messageContent).
GetAll(db)
現在
table := orm.User().
Message().Join(genorm.Eq(userIDExpr, messageUserID))
messageUserValues, err := genorm.
Select(table).
Fields(userName, messageContent).
GetAll(db)
GoのGenericsではMethodに型パラメーターを持たせられない
参考: Type Parameters Proposal
/* ref: https://go.dev/ref/spec#Function_declarations */
FunctionDecl = "func" FunctionName [ TypeParameters ] Signature [ FunctionBody ] .
/* ref: https://go.dev/ref/spec#Method_declarations */
MethodDecl = "func" Receiver MethodName Signature [ FunctionBody ] .
Expression指定でSELECTする
結果としてExpressionのGoの型の値を返す
→Pluckには型パラメーターが必須
// SELECT id FROM users
// userIDs: []uuid.UUID
userIDs, err := genorm.
Pluck(orm.User(), user.IDExpr).
GetAll(db)
初期: T
がポインターなので、無理やりメモリ確保
func (c *SelectContext[T]) GetCtx(ctx context.Context, db DB) (T, error) {
var table T
// 省略
iTable := table.New() // Table型を返す
switch v := iTable.(type) {
case T:
table = v
default:
return table, fmt.Errorf("invalid table type: %T", iTable)
}
// 省略
return table, nil
}
現在: 型パラメーターS
でT
の先の型取得
type SelectContext[S any, T TablePointer[S]] struct {
//省略
}
func (c *SelectContext[S, T]) GetCtx(ctx context.Context, db DB) (T, error) {
//省略
var table S
//省略
return &table, nil
}
使う時にS
指定しないでいいの?
→制約型推論
Go 1.18時点のGenericsの型推論は2種類
func hoge[T any](t T)
でfunc hoge(1)
T → int
型制約を利用した型推論
T → *orm.UserTable
(関数引数型推論)T → *S
(型制約)→S → orm.UserTable
type TablePointer[T any] interface {
Table
*T
}
func Select[S any, T TablePointer[S]](table T) *SelectContext[S, T]{
// 省略
}
詳しくは
現在、gomockで型パラメーターを含むmockは生成できない
interfaceが型パラメーターを持たなくても、
生成できない場合がある
以下のいずれかで落ちる
Expr自体は型パラメーターがないが、
TableExprに方パラメーターがあるのでmockできない
type Expr interface {
Expr() (string, []ExprType, []error)
}
type TableExpr[T Table] interface {
Expr
TableExpr(T) (string, []ExprType, []error)
}
内部実装が変わると使えなくなる可能性があるのに注意
genorm_test
パッケージに置いているtype Expr interface {
genorm.Expr
}
Scanのみ型ごとに処理が分かれる
type ExprPrimitive interface {
bool |
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64 |
string | time.Time
}
type WrappedPrimitive[T ExprPrimitive] struct {
valid bool
val T
}
できるならそもそも型を分けて対応するべき
GenORMの場合、分けると使い勝手が悪くなるので分けたくない
書きたいやつ(できない)
func (wp *WrappedPrimitive[T]) Scan(src any) error {
switch T {
case bool:
//省略
}
// 省略
}
やらないで良いならやらない方が良い
力技対応
func (wp *WrappedPrimitive[T]) Scan(src any) error {
var dest any = wp.val // wp.valの型はT
switch dest.(type) {
case bool:
//省略
}
// 省略
}
database/sql
のNullInt16
,etcジェネリクスの使い道として真っ先に考えたんじゃないかと思う
本当にジェネリクスの使いどころなのか?と言う疑問が出てくる
こうなる?
func (n *NullValue[T]) Scan(value any) error {
var dest any = wp.val
switch dest.(type) {
case bool:
//省略
}
// 省略
}
database/sql
のNull~
現在でもany
に変換して内部でswitchしていた
func (n *NullInt16) Scan(value any) error {
// 省略
err := convertAssign(&n.Int16, value)
n.Valid = err == nil
return err
}
func convertAssign(dest, src any) error {
// 省略
}
GenORMはほぼ標準ライブラリしか使っていない
ライブラリの更新の手間が軽くて楽
これまでまとめられなかった関数、etcをまとめる用途としては十分すぎる
GoらしいGenericsになっているという印象
ただ、GenORMのように「ライブラリで型を強力につける」という用途だと辛いことも多い
→ Avoid boilerplate
Goの構文中に全SQLが現れる
→Goの構文とSQLの構文を組み合わせた解析が可能
userValues, err = genorm.
Select(orm.User()).
Fields(user.Name).
GroupBy(user.Name).
Having(genorm.GtLit(genorm.Count(user.IDExpr, false), genorm.Wrap(int64(10)))).
GetAll(db)
性質上、対応可能なSQLが非常の多い
カラム名以外でのSELECT,etcもできるようにしたい
tupleValues, err = genorm.
Find(orm.User(), genorm.Tuple2(user.NameExpr, genorm.Count(user.IDExpr, false))).
GroupBy(user.Name).
GetAll(db)
プロトタイプを実際に使用感を教えたくれた@oribeさん、
英語版ドキュメントのチェックをしてくれた@hosshii、@sappi_red、
など、本当にありがとうございました