GORM v1.30.0 から Generics API が導入されました。 Generics APIでは、Go 1.18 で導入されたジェネリクスを活用し、従来のAPI (Traditional API) における型安全性や状態管理の課題を解消しています。 本記事では、主な変更点を3つの観点から解説します
- 型安全なAPI
- Contextの明示的な受け渡し
- メソッドチェーン汚染への対策
Generics APIの基本的な使い方
Generics APIでは、gorm.G[T](db *gorm.DB) を通じて、モデル型 T に対応するDB操作を実行します。
Traditional APIとほぼ同じメソッドが使えますが、Save, FirstOrCreate などの一部のメソッドは削除されています
ctx := context.Background()
user := User{Name: "Alice", Age: 30}
gorm.G[User](db).Create(ctx, &user)
gorm.G[User](db).Where("age > ?", 20).Find(ctx)
gorm.G[User](db).Where("name = ?", "Alice").Update(ctx, "age", 20)
gorm.G[User](db).Where("name = ?", "Alice").First(ctx)
gorm.G[User](db).Where("name = ?", "Alice").Delete(ctx) 型安全なAPI
Generics APIでは、GORMの各種メソッドがジェネリクスを活用して型安全に設計されています。
例えば、Createメソッドの場合、Traditional APIではinterface{}型の引数を受け取るため、誤った型の値を渡してもコンパイルエラーになりません。
Generics APIではモデル型 T に対して型パラメータが指定されるため、誤った型の値を渡すとコンパイルエラーになります
// Traditional API - コンパイルエラーにならない
user := InvalidUser{FirstName: "John", LastName: "Doe", Email: "john@example.com", Age: 30}
if err := db.WithContext(ctx).Create(&user).Error; err != nil {
log.Fatal(err)
}
// Generics API - コンパイルエラーになる
// cannot use &user (value of type *InvalidUser) as *User value in argument to gorm.G[User](db).Create
user := InvalidUser{FirstName: "John", LastName: "Doe", Email: "john@example.com", Age: 30}
if err := gorm.G[User](db).Create(ctx, &user); err != nil {
log.Fatal(err)
} Contextの明示的な受け渡し
Generics APIからは、GORMの各種メソッドに context.Context を明示的に渡すようになりました。
ctxを渡さないとコンパイルエラーになるので、db.WithContext(ctx)を忘れる心配が無くなりました
// Traditional API
db.WithContext(ctx).Where("id = ?", 1).First(&product)
db.WithContext(ctx).Create(&product)
db.WithContext(ctx).Delete(&product)
// Generics API
gorm.G[Product](db).Where("id = ?", 1).First(ctx)
gorm.G[Product](db).Create(ctx, &product)
gorm.G[Product](db).Delete(ctx, &product) メソッドチェーン汚染への対策
Generics APIでは、これから説明するメソッドチェーン汚染(gorm.DB インスタンスの状態が意図せず引き継がれてしまう問題)に対処しています
そもそも、GORMには Chain系のメソッド と Finisher系のメソッド があります
- Chain系のメソッド :
Where, Select, Omit, Joinsなど、クエリを構築するためのメソッド - Finisher系のメソッド :
Find, First, Createなど、実際にデータベース操作を実行するメソッド
Chain系のメソッドは連続してメソッドを呼び出せるように、gorm.DB インスタンスを返します。
gorm.DB インスタンスは内部で状態 (Statement構造体) を保持しており、Chain系のメソッドを呼ぶたびに状態が更新されます。
Finisher系のメソッドが呼ばれると、その時点での状態に基づいてデータベース操作が実行されます
しかし、Finisher系のメソッドを呼んでも gorm.DB インスタンス自体の状態はリセットされません。
なので、状態が変更されたgorm.DB インスタンスを使い回すと、Chain系のメソッドを再度呼ぶ際に、前回の状態が引き継がれてしまい、意図しないクエリが生成されるケースがあります
// Chain系のメソッドにより、queryは状態が更新されています
query := db.Where("name = ?", "John")
// Finisher系のメソッドを呼んでも、queryの状態は更新されたままです
query.Where("age = ?", 18).Find(&users)
// SELECT * FROM users WHERE name = 'John' AND age = 18
// 再度Chain系のメソッドを呼び出すと、状態がさらに更新されたクエリが生成されてしまいます
query.Where("age = ?", 28).Find(&users)
// SELECT * FROM users WHERE name = 'John' AND age = 18 AND age = 28
// ^ 意図しない条件が追加されてしまう Generics APIでは、gorm.DB インスタンスを使いまわしてもメソッドチェーン汚染は発生しません
query := gorm.G[User](db).Where("name = ?", "John")
user1, err1 := query.Where("age = ?", 18).Find(ctx)
// SELECT * FROM users WHERE name = 'John' AND age = 18;
users, err2 := query.Where("age = ?", 28).Find(ctx)
// SELECT * FROM users WHERE name = 'John' AND age = 28; まとめ
本記事ではGORMのGenerics APIで導入された主な変更点について解説しました。 何かの参考になれば幸いです