GORM v1.30.0 でジェネリクス API が導入されました。
ジェネリクス API を用いることで、データベース操作を型安全に実装するとができます。
非ジェネリクス API に影響を与えない形で実装されているため、既存のコードはそのままにジェネリクス API を利用することが可能です。
Go モジュールを作成し、GORM をインストールします。
バージョンは v1.30.0 を指定します。
go mod init gorm-generic-example
go get gorm.io/[email protected]
go get -u gorm.io/driver/mysql
ローカル DB へ接続して動作確認したいので、MySQL コンテナを起動します。
特に拘りはないですが、MySQL のバージョンは 8.0 を指定します。
compose.yaml
volumes:
mysql-data:
services:
mysql:
container_name: gorm_generics_example
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: gorm_generics_example
volumes:
- mysql-data:/var/lib/mysql
ports:
- 3306:3306
テーブルを作成します。
docker compose exec mysql \
mysql -u root -ppassword -e "CREATE TABLE gorm_generics_example.users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, active BOOLEAN NOT NULL)"
取得操作の動作確認のためにレコードを 1 件挿入しておきます。
docker compose exec mysql \
mysql -u root -ppassword -e "INSERT INTO gorm_generics_example.users (name, active) VALUES ('user_name', true)"
main.go を作成し、ジェネリクス API を使ったコードを書いていきます。
main.go
package main
import (
"context"
"fmt"
"log"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type User struct {
ID int
Name string
Active bool
}
func main() {
db, err := gorm.Open(mysql.Open("root:password@tcp(127.0.0.1:3306)/gorm_generics_example"))
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
users, err := gorm.G[User](db).Find(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println(users)
}
関数 gorm.G
は型パラメータを持つ関数です。
この関数を使うことでジェネリクス対応した操作を利用できます。
型引数には操作したいモデルを渡します。(型引数は省略不可)
上記のコードの Find
の戻り値の型を IDE で確認してみましょう。
型引数として渡した User
のスライス型になっています。
その他の操作も実装してみたので、コードを置いておきます。
snip>
result := gorm.WithResult()
if err := gorm.G[User](db, result).Create(ctx, &User{
Name: "user_name_2",
Active: false,
}); err != nil {
log.Fatal(err)
}
lastInsertId, err := result.Result.LastInsertId()
if err != nil {
log.Fatal(err)
}
user, err := gorm.G[User](db).
Where("id = ?", lastInsertId).
Take(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println(user)
if _, err := gorm.G[User](db, result).
Where("id = ?", lastInsertId).
Update(ctx, "name", "updated_user_name_2"); err != nil {
log.Fatal(err)
}
if _, err := gorm.G[User](db, result).
Where("id = ?", lastInsertId).
Delete(ctx); err != nil {
log.Fatal(err)
}
snip>
参照系操作の取得結果が型の付いた戻り値になっているのが嬉しいです。
GORM に限らずですが、ジェネリクスサポート前から存在する ORM は、変数のポインタを引数に渡して関数を実行すると DB からの取得結果でフィールドが埋められるような、取得結果が戻り値になっていない実装が多いです。
引数に渡す変数の型を気にしながら実装やレビューをするのは慣れるまでは辛かった覚えがあります。
snip>
users := []User{}
if err := db.Where("id >= ?", 1).Find(&users).Error; err != nil {
log.Fatal(err)
}
if err := db.Where("id >= ?", 1).Find(users).Error; err != nil {
log.Fatal(err)
}
snip>
また、Find
などの DB 操作系関数(所謂 Finisher Methods)がエラーを返すようになったことで、Go らしいエラーハンドリングができるようになった点も喜ばしいです。
上記のコードでも現れていますが、非ジェネリクスの Finisher Methods はエラーハンドリングが特殊で、Go の慣習とは異なった書き振りです。
関数の戻り値としてエラーは返らないようになっており、発生したエラーは*gorm.DB
のフィールドに代入されます。
そのため .Error
でフィールドアクセスしてエラーハンドリングを行う必要があり、エラーハンドリングの漏れが発生しやすくなっていました。
snip>
users := []User{}
if err := db.Where("id >= ?", 1).Find(&users).Error; err != nil {
log.Fatal(err)
}
if err := db.Where("id >= ?", 1).Find(users); err != nil {
log.Fatal(err)
}
snip>
一点、引っ掛かりそうだなと思ったのは、型引数にポインタ型構造体を渡した際の参照系 Finisher Methods の戻り値です。
筆者は、エラーが発生した際の第一戻り値はポインタ型のゼロ値である nil
が返ってくることを期待していましたが、実際にはゼロ値の構造体のポインタが返ります。
エラーにより値が取得できなかったことが判明している上で第一戻り値を参照することはほとんどないため実害は少ないと思いますが、期待の挙動とは異なっていたため少し気になりました。
snip>
user, err := gorm.G[*User](db).Where("id", 0).Take(ctx)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
log.Fatal(err)
}
}
fmt.Println(user)
snip>
ジェネリクス対応によって、これまでよりも安全に GORM を扱うことができるようになりました。
積極的にジェネリクス API を使っていきたいと思いつつも、ドキュメントもまだ少なく予期しない挙動が隠れている可能性も 0 ではないので十分に確認しつつ導入していきたいと考えています。
Views: 2