Go-DDDマーケットプレイスアプリケーション ORMマッパー実装の詳細解説¶
1. 概要¶
この文書では、Go-DDDマーケットプレイスアプリケーションにおけるインフラストラクチャ層のORMマッパー実装について詳しく解説します。ORMマッパーは、ドメインエンティティとデータベースの間の変換を担当し、リポジトリインターフェースの実装を提供します。
1.1 ORMマッパーの役割¶
ORMマッパーは以下の役割を担います:
- ドメインエンティティとデータベースモデルの変換: ドメインエンティティをデータベースに保存可能な形式に変換し、データベースから取得したデータをドメインエンティティに変換します。
- リポジトリインターフェースの実装: ドメイン層で定義されたリポジトリインターフェースの実装を提供します。
- データベース操作の抽象化: SQLクエリやデータベース固有の操作を抽象化し、ドメイン層がデータベースの詳細を知る必要がないようにします。
1.2 アーキテクチャにおける位置づけ¶
以下の図は、ORMマッパーのアーキテクチャにおける位置づけを示しています:
2. ORMマッパーの実装¶
2.1 データベースモデルの定義¶
まず、データベースモデルを定義します。これらのモデルは、GORMのタグを使用してデータベーステーブルとのマッピングを定義します。
2.1.1 商品データベースモデル¶
package postgres
import (
"time"
)
// DBProduct はデータベースの商品テーブルに対応する構造体
type DBProduct struct {
ID string `gorm:"primaryKey"`
Name string
Description string
Price float64
SellerID string
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
}
// TableName はテーブル名を指定するGORMのインターフェースメソッド
func (DBProduct) TableName() string {
return "products"
}
2.1.2 出品者データベースモデル¶
package postgres
import (
"time"
)
// DBSeller はデータベースの出品者テーブルに対応する構造体
type DBSeller struct {
ID string `gorm:"primaryKey"`
Name string
Email string
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
}
// TableName はテーブル名を指定するGORMのインターフェースメソッド
func (DBSeller) TableName() string {
return "sellers"
}
2.2 マッパー関数の実装¶
次に、ドメインエンティティとデータベースモデルの間の変換を行うマッパー関数を実装します。
2.2.1 商品マッパー¶
package postgres
import (
"github.com/sklinkert/go-ddd/internal/domain/entities"
)
// toDBProduct はドメインの商品エンティティをデータベースモデルに変換する
func toDBProduct(product entities.Product) DBProduct {
return DBProduct{
ID: product.Id,
Name: product.Name,
Description: product.Description,
Price: product.Price,
SellerID: product.SellerId,
CreatedAt: product.CreatedAt,
UpdatedAt: product.UpdatedAt,
DeletedAt: product.DeletedAt,
}
}
// toDomainProduct はデータベースモデルをドメインの商品エンティティに変換する
func toDomainProduct(dbProduct DBProduct) entities.Product {
return entities.Product{
Id: dbProduct.ID,
Name: dbProduct.Name,
Description: dbProduct.Description,
Price: dbProduct.Price,
SellerId: dbProduct.SellerID,
CreatedAt: dbProduct.CreatedAt,
UpdatedAt: dbProduct.UpdatedAt,
DeletedAt: dbProduct.DeletedAt,
}
}
2.2.2 出品者マッパー¶
package postgres
import (
"github.com/sklinkert/go-ddd/internal/domain/entities"
)
// toDBSeller はドメインの出品者エンティティをデータベースモデルに変換する
func toDBSeller(seller entities.Seller) DBSeller {
return DBSeller{
ID: seller.Id,
Name: seller.Name,
Email: seller.Email,
CreatedAt: seller.CreatedAt,
UpdatedAt: seller.UpdatedAt,
DeletedAt: seller.DeletedAt,
}
}
// toDomainSeller はデータベースモデルをドメインの出品者エンティティに変換する
func toDomainSeller(dbSeller DBSeller) entities.Seller {
return entities.Seller{
Id: dbSeller.ID,
Name: dbSeller.Name,
Email: dbSeller.Email,
CreatedAt: dbSeller.CreatedAt,
UpdatedAt: dbSeller.UpdatedAt,
DeletedAt: dbSeller.DeletedAt,
}
}
2.3 リポジトリ実装¶
最後に、ドメイン層で定義されたリポジトリインターフェースの実装を提供します。これらの実装は、GORMを使用してデータベース操作を行い、マッパー関数を使用してドメインエンティティとデータベースモデルの変換を行います。
2.3.1 商品リポジトリ実装¶
package postgres
import (
"errors"
"gorm.io/gorm"
"github.com/sklinkert/go-ddd/internal/domain/entities"
"github.com/sklinkert/go-ddd/internal/application/common"
)
// GormProductRepository はGORMを使用した商品リポジトリの実装
type GormProductRepository struct {
db *gorm.DB
}
// NewGormProductRepository は新しいGormProductRepositoryを作成する
func NewGormProductRepository(db *gorm.DB) *GormProductRepository {
return &GormProductRepository{db: db}
}
// Save は商品を保存する
func (r *GormProductRepository) Save(product entities.Product) error {
dbProduct := toDBProduct(product)
result := r.db.Create(&dbProduct)
if result.Error != nil {
return common.NewDatabaseError("Failed to save product", result.Error)
}
return nil
}
// FindAll は全ての商品を取得する
func (r *GormProductRepository) FindAll() ([]entities.Product, error) {
var dbProducts []DBProduct
// 削除されていないレコードのみを取得
result := r.db.Where("deleted_at IS NULL").Find(&dbProducts)
if result.Error != nil {
return nil, common.NewDatabaseError("Failed to find products", result.Error)
}
products := make([]entities.Product, len(dbProducts))
for i, dbProduct := range dbProducts {
products[i] = toDomainProduct(dbProduct)
}
return products, nil
}
// ById はIDで商品を取得する
func (r *GormProductRepository) ById(id string) (entities.Product, error) {
var dbProduct DBProduct
result := r.db.First(&dbProduct, "id = ? AND deleted_at IS NULL", id)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return entities.Product{}, common.NewNotFoundError("Product not found", nil)
}
return entities.Product{}, common.NewDatabaseError("Failed to find product", result.Error)
}
return toDomainProduct(dbProduct), nil
}
// Update は商品を更新する
func (r *GormProductRepository) Update(product entities.Product) error {
dbProduct := toDBProduct(product)
result := r.db.Save(&dbProduct)
if result.Error != nil {
return common.NewDatabaseError("Failed to update product", result.Error)
}
if result.RowsAffected == 0 {
return common.NewNotFoundError("Product not found", nil)
}
return nil
}
// Delete は商品を論理削除する
func (r *GormProductRepository) Delete(id string) error {
result := r.db.Model(&DBProduct{}).Where("id = ?", id).Update("deleted_at", gorm.Expr("NOW()"))
if result.Error != nil {
return common.NewDatabaseError("Failed to delete product", result.Error)
}
if result.RowsAffected == 0 {
return common.NewNotFoundError("Product not found", nil)
}
return nil
}
2.3.2 出品者リポジトリ実装¶
package postgres
import (
"errors"
"gorm.io/gorm"
"github.com/sklinkert/go-ddd/internal/domain/entities"
"github.com/sklinkert/go-ddd/internal/application/common"
)
// GormSellerRepository はGORMを使用した出品者リポジトリの実装
type GormSellerRepository struct {
db *gorm.DB
}
// NewGormSellerRepository は新しいGormSellerRepositoryを作成する
func NewGormSellerRepository(db *gorm.DB) *GormSellerRepository {
return &GormSellerRepository{db: db}
}
// Save は出品者を保存する
func (r *GormSellerRepository) Save(seller entities.Seller) error {
dbSeller := toDBSeller(seller)
result := r.db.Create(&dbSeller)
if result.Error != nil {
return common.NewDatabaseError("Failed to save seller", result.Error)
}
return nil
}
// FindAll は全ての出品者を取得する
func (r *GormSellerRepository) FindAll() ([]entities.Seller, error) {
var dbSellers []DBSeller
// 削除されていないレコードのみを取得
result := r.db.Where("deleted_at IS NULL").Find(&dbSellers)
if result.Error != nil {
return nil, common.NewDatabaseError("Failed to find sellers", result.Error)
}
sellers := make([]entities.Seller, len(dbSellers))
for i, dbSeller := range dbSellers {
sellers[i] = toDomainSeller(dbSeller)
}
return sellers, nil
}
// ById はIDで出品者を取得する
func (r *GormSellerRepository) ById(id string) (entities.Seller, error) {
var dbSeller DBSeller
result := r.db.First(&dbSeller, "id = ? AND deleted_at IS NULL", id)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return entities.Seller{}, common.NewNotFoundError("Seller not found", nil)
}
return entities.Seller{}, common.NewDatabaseError("Failed to find seller", result.Error)
}
return toDomainSeller(dbSeller), nil
}
// Update は出品者を更新する
func (r *GormSellerRepository) Update(seller entities.Seller) error {
dbSeller := toDBSeller(seller)
result := r.db.Save(&dbSeller)
if result.Error != nil {
return common.NewDatabaseError("Failed to update seller", result.Error)
}
if result.RowsAffected == 0 {
return common.NewNotFoundError("Seller not found", nil)
}
return nil
}
// Delete は出品者を論理削除する
func (r *GormSellerRepository) Delete(id string) error {
result := r.db.Model(&DBSeller{}).Where("id = ?", id).Update("deleted_at", gorm.Expr("NOW()"))
if result.Error != nil {
return common.NewDatabaseError("Failed to delete seller", result.Error)
}
if result.RowsAffected == 0 {
return common.NewNotFoundError("Seller not found", nil)
}
return nil
}
3. ORMマッパーの動作フロー¶
以下の図は、ORMマッパーを使用したデータの保存と取得の流れを示しています:
3.1 データ保存フロー¶
3.2 データ取得フロー¶
3.3 データ更新フロー¶
3.4 データ削除フロー(ソフトデリート)¶
4. ORMマッパーの利点と注意点¶
4.1 利点¶
-
ドメインロジックとデータアクセスの分離: ORMマッパーを使用することで、ドメインロジックとデータアクセスの関心事を明確に分離できます。ドメインエンティティはデータベースの詳細を知る必要がなく、純粋なビジネスロジックに集中できます。
-
リポジトリパターンの実装: リポジトリパターンを使用することで、データアクセスのインターフェースを抽象化し、実装の詳細を隠蔽できます。これにより、テストが容易になり、将来的なデータストアの変更にも柔軟に対応できます。
-
型安全性: Goの静的型システムを活用して、データベースとのマッピングを型安全に行うことができます。
-
ソフトデリート: GORMの機能を利用して、エンティティの論理削除(ソフトデリート)を簡単に実装できます。
4.2 注意点¶
-
パフォーマンス: ORMマッパーを使用すると、エンティティとデータベースモデルの変換によるオーバーヘッドが発生します。パフォーマンスが重要な場合は、クエリの最適化や、場合によっては生のSQLを使用することを検討する必要があります。
-
複雑なクエリ: 複雑なクエリや集計クエリなどは、ORMでは表現しにくい場合があります。そのような場合は、リポジトリに専用のメソッドを追加し、生のSQLやGORMの高度な機能を使用することを検討してください。
-
N+1問題: リレーションを持つエンティティを取得する際に、N+1問題が発生する可能性があります。GORMのPreloadやJoinsなどの機能を適切に使用して、この問題を回避する必要があります。
-
トランザクション管理: 複数のリポジトリにまたがる操作を行う場合は、トランザクション管理が必要になります。GORMのトランザクション機能を使用して、一貫性を保つようにしてください。
5. まとめ¶
ORMマッパーは、ドメイン駆動設計においてドメインレイヤーとインフラストラクチャレイヤーを橋渡しする重要な役割を果たします。適切に実装することで、ドメインロジックとデータアクセスの関心事を分離し、テスト可能で保守性の高いコードを作成することができます。
Go言語とGORMを使用したORMマッパーの実装では、以下のポイントに注意することが重要です:
- ドメインエンティティとデータベースモデルを明確に分離する
- マッパー関数を使用して、両者の変換を行う
- リポジトリインターフェースを通じてドメインレイヤーとやり取りする
- エラーハンドリングを適切に行い、ドメインレイヤーに意味のあるエラーを返す
- ソフトデリートなどの横断的な関心事を適切に実装する
これらのポイントを押さえることで、クリーンで保守性の高いORMマッパーを実装することができます。