ORMからGenへの移行と使い方¶
1. 概要¶
この文書では、Go-DDDマーケットプレイスアプリケーションにおけるデータアクセス層の実装を、従来のORMマッパーからGORMのGenに移行した詳細について解説します。Genは、GORMの上に構築された強力なコード生成ツールで、型安全なデータアクセスAPIを提供します。
1.1 Genの特徴¶
Genは以下の特徴を持っています:
- 型安全なAPI:
interface{}を使わない100%型安全なデータアクセスAPIを提供します。 - 動的なSQLからの使いやすく再利用可能なAPI: SQLをGoの文法で扱えるようにします。
- データベースからの自動生成: データベーススキーマからGORMの規約に従ったコードを自動生成します。
- GORMの機能をすべてサポート: GORMの下で動作するため、GORMのすべての機能、プラグイン、サポートするDBMSを利用できます。
1.2 ORMからGenへの移行理由¶
従来のORMマッパーでは、以下のような課題がありました:
- SQLとGoの混在: 複雑なクエリを書く際に、GoのコードとSQL文字列が混在し、コンテキストスイッチのコストが発生していました。
- 型安全性の欠如: SQL文字列を使用する場合、型安全性が失われる場合がありました。
- カラム名の手動入力: テーブルのカラム名を手動で入力する必要があり、ミスの原因となっていました。
Genを使用することで、これらの課題を解決し、より保守性の高いコードを実現できます。
2. Genの実装¶
2.1 モデル生成の設定¶
Genを使用するには、まずモデル生成のための設定を行います。以下は、generate_models.goファイルの例です:
package main
import (
"github.com/sklinkert/go-ddd/internal/infrastructure/db/postgres"
"gorm.io/gen"
)
func main() {
g := gen.NewGenerator(gen.Config{
OutPath: "./internal/infrastructure/db/postgres/gen/query", // 出力パス
Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface,
FieldWithIndexTag: true,
FieldWithTypeTag: true,
FieldNullable: true,
})
// データベース接続の取得
db, err := postgres.NewConnection()
if err != nil {
panic(err)
}
g.UseDB(db)
// すべてのテーブルを生成
all := g.GenerateAllTable()
g.ApplyBasic(all...)
// コードの生成
g.Execute()
}
このスクリプトを実行すると、データベーススキーマから以下のファイルが生成されます:
- モデルファイル:
gen/model/ディレクトリに、各テーブルに対応するGoの構造体が生成されます。 - クエリファイル:
gen/query/ディレクトリに、各テーブルに対する型安全なクエリAPIが生成されます。
2.2 生成されるモデルファイル¶
生成されるモデルファイルは、データベーステーブルの構造を反映したGoの構造体です。以下は、Productモデルの例です:
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
const TableNameProduct = "products"
// Product mapped from table <products>
type Product struct {
Id uuid.UUID `gorm:"column:id;type:uuid;primaryKey" json:"id"`
Name string `gorm:"column:name;type:varchar(255);not null" json:"name"`
Price float64 `gorm:"column:price;type:float;not null" json:"price"`
SellerId uuid.UUID `gorm:"column:seller_id;type:uuid;index" json:"seller_id"`
Seller Seller `gorm:"foreignKey:SellerId" json:"seller"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp" json:"deleted_at"`
}
// TableName Product's table name
func (*Product) TableName() string {
return TableNameProduct
}
2.3 生成されるクエリファイル¶
生成されるクエリファイルは、型安全なデータアクセスAPIを提供します。以下は、Productテーブルに対するクエリAPIの一部です:
// Code generated by gorm.io/gen. DO NOT EDIT.
package query
// ...
type productDo struct{ gen.DO }
// Create creates new product records
func (p productDo) Create(values ...*model.Product) error {
if len(values) == 0 {
return nil
}
return p.DO.Create(values)
}
// First finds the first product record
func (p productDo) First() (*model.Product, error) {
if result, err := p.DO.First(); err != nil {
return nil, err
} else {
return result.(*model.Product), nil
}
}
// Find finds product records
func (p productDo) Find() ([]*model.Product, error) {
result, err := p.DO.Find()
return result.([]*model.Product), err
}
// Where adds conditions to query
func (p productDo) Where(conds ...gen.Condition) *productDo {
return p.withDO(p.DO.Where(conds...))
}
// ...
3. Genの使い方¶
3.1 基本的な使い方¶
Genを使用するには、まず生成されたクエリAPIをインポートし、データベース接続を渡してクエリオブジェクトを初期化します:
import (
"context"
"github.com/sklinkert/go-ddd/internal/infrastructure/db/postgres"
"github.com/sklinkert/go-ddd/internal/infrastructure/db/postgres/gen/model"
"github.com/sklinkert/go-ddd/internal/infrastructure/db/postgres/gen/query"
)
func main() {
// データベース接続の取得
db, err := postgres.NewConnection()
if err != nil {
panic(err)
}
// クエリオブジェクトの初期化
q := query.Use(db)
productQuery := q.Product
sellerQuery := q.Seller
ctx := context.Background()
// 以下、クエリの実行
// ...
}
3.2 データの作成¶
// 新しい商品の作成
product := &model.Product{
Name: "テスト商品",
Price: 9.99,
SellerId: sellerId,
}
err = q.Transaction(func(tx *query.Query) error {
return tx.Product.WithContext(ctx).Create(product)
})
3.3 データの取得¶
// IDによる商品の取得
product, err := productQuery.WithContext(ctx).Where(productQuery.Id.Eq(productId)).First()
// 全商品の取得
products, err := productQuery.WithContext(ctx).Find()
// 条件付き検索
products, err := productQuery.WithContext(ctx).
Where(productQuery.Price.Gt(10.0)).
Order(productQuery.CreatedAt.Desc()).
Find()
3.4 データの更新¶
// 商品の更新
product.Price = 19.99
err = productQuery.WithContext(ctx).Save(product)
// または特定のフィールドのみ更新
err = productQuery.WithContext(ctx).
Where(productQuery.Id.Eq(productId)).
UpdateSimple(productQuery.Price.Value(19.99))
3.5 データの削除¶
// 商品の削除
_, err = productQuery.WithContext(ctx).Where(productQuery.Id.Eq(productId)).Delete()
3.6 リレーションを使用したクエリ¶
// 出品者とその商品を取得
seller, err := sellerQuery.WithContext(ctx).
Where(sellerQuery.Id.Eq(sellerId)).
Preload(sellerQuery.Products).
First()
// 商品と出品者を結合して取得
products, err := productQuery.WithContext(ctx).
Join(sellerQuery, productQuery.SellerId.EqCol(sellerQuery.Id)).
Where(sellerQuery.Name.Eq("テスト出品者")).
Find()
3.7 トランザクション¶
err = q.Transaction(func(tx *query.Query) error {
// トランザクション内の処理
product := &model.Product{
Name: "トランザクションテスト",
Price: 9.99,
SellerId: sellerId,
}
if err := tx.Product.WithContext(ctx).Create(product); err != nil {
return err
}
// 他の処理...
return nil
})
4. ORMとGenの比較¶
4.1 基本的なクエリの比較¶
4.1.1 データの取得¶
ORM(GORM):
var product Product
db.Where("id = ?", productId).First(&product)
Gen:
product, err := productQuery.WithContext(ctx).Where(productQuery.Id.Eq(productId)).First()
4.1.2 条件付き検索¶
ORM(GORM):
var products []Product
db.Where("price > ?", 10.0).Order("created_at DESC").Find(&products)
Gen:
products, err := productQuery.WithContext(ctx).
Where(productQuery.Price.Gt(10.0)).
Order(productQuery.CreatedAt.Desc()).
Find()
4.1.3 結合クエリ¶
ORM(GORM):
var products []Product
db.Joins("JOIN sellers ON sellers.id = products.seller_id").
Where("sellers.name = ?", "テスト出品者").
Find(&products)
Gen:
products, err := productQuery.WithContext(ctx).
Join(sellerQuery, productQuery.SellerId.EqCol(sellerQuery.Id)).
Where(sellerQuery.Name.Eq("テスト出品者")).
Find()
4.2 Genの利点¶
- 型安全性: カラム名やテーブル名が型として表現されるため、タイプミスによるエラーを防ぐことができます。
- コード補完: IDEのコード補完機能を活用できるため、開発効率が向上します。
- リファクタリングのしやすさ: カラム名が変更された場合、コンパイル時にエラーが検出されるため、安全にリファクタリングできます。
- SQLの知識が少なくても使える: SQLの文法を覚える必要がなく、Goの文法だけで複雑なクエリを構築できます。
- 自動生成: データベーススキーマからコードが自動生成されるため、モデルとデータベースの同期が容易です。
5. まとめ¶
GORMのGenを使用することで、型安全で保守性の高いデータアクセスコードを実現できます。従来のORMマッパーと比較して、以下のような利点があります:
- 型安全性の向上: カラム名やテーブル名が型として表現されるため、タイプミスによるエラーを防ぐことができます。
- 開発効率の向上: IDEのコード補完機能を活用できるため、開発効率が向上します。
- コードの可読性向上: SQLの文字列とGoのコードが混在せず、一貫したGoの文法でクエリを記述できます。
- 保守性の向上: データベーススキーマの変更があった場合、コードを再生成するだけで対応できます。
Genは、GORMの上に構築された強力なツールであり、GORMのすべての機能を利用しながら、より型安全で使いやすいAPIを提供します。特に大規模なプロジェクトや、複雑なクエリを多用するプロジェクトでは、Genの導入を検討する価値があります。