Skip to content

Go-DDDマーケットプレイスアプリケーション ORMマッパー実装の詳細解説

1. 概要

この文書では、Go-DDDマーケットプレイスアプリケーションにおけるインフラストラクチャ層のORMマッパー実装について詳しく解説します。ORMマッパーは、ドメインエンティティとデータベースの間の変換を担当し、リポジトリインターフェースの実装を提供します。

1.1 ORMマッパーの役割

ORMマッパーは以下の役割を担います:

  1. ドメインエンティティとデータベースモデルの変換: ドメインエンティティをデータベースに保存可能な形式に変換し、データベースから取得したデータをドメインエンティティに変換します。
  2. リポジトリインターフェースの実装: ドメイン層で定義されたリポジトリインターフェースの実装を提供します。
  3. データベース操作の抽象化: SQLクエリやデータベース固有の操作を抽象化し、ドメイン層がデータベースの詳細を知る必要がないようにします。

1.2 アーキテクチャにおける位置づけ

以下の図は、ORMマッパーのアーキテクチャにおける位置づけを示しています:

uml diagram

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 データ保存フロー

uml diagram

3.2 データ取得フロー

uml diagram

3.3 データ更新フロー

uml diagram

3.4 データ削除フロー(ソフトデリート)

uml diagram

4. ORMマッパーの利点と注意点

4.1 利点

  1. ドメインロジックとデータアクセスの分離: ORMマッパーを使用することで、ドメインロジックとデータアクセスの関心事を明確に分離できます。ドメインエンティティはデータベースの詳細を知る必要がなく、純粋なビジネスロジックに集中できます。

  2. リポジトリパターンの実装: リポジトリパターンを使用することで、データアクセスのインターフェースを抽象化し、実装の詳細を隠蔽できます。これにより、テストが容易になり、将来的なデータストアの変更にも柔軟に対応できます。

  3. 型安全性: Goの静的型システムを活用して、データベースとのマッピングを型安全に行うことができます。

  4. ソフトデリート: GORMの機能を利用して、エンティティの論理削除(ソフトデリート)を簡単に実装できます。

4.2 注意点

  1. パフォーマンス: ORMマッパーを使用すると、エンティティとデータベースモデルの変換によるオーバーヘッドが発生します。パフォーマンスが重要な場合は、クエリの最適化や、場合によっては生のSQLを使用することを検討する必要があります。

  2. 複雑なクエリ: 複雑なクエリや集計クエリなどは、ORMでは表現しにくい場合があります。そのような場合は、リポジトリに専用のメソッドを追加し、生のSQLやGORMの高度な機能を使用することを検討してください。

  3. N+1問題: リレーションを持つエンティティを取得する際に、N+1問題が発生する可能性があります。GORMのPreloadやJoinsなどの機能を適切に使用して、この問題を回避する必要があります。

  4. トランザクション管理: 複数のリポジトリにまたがる操作を行う場合は、トランザクション管理が必要になります。GORMのトランザクション機能を使用して、一貫性を保つようにしてください。

5. まとめ

ORMマッパーは、ドメイン駆動設計においてドメインレイヤーとインフラストラクチャレイヤーを橋渡しする重要な役割を果たします。適切に実装することで、ドメインロジックとデータアクセスの関心事を分離し、テスト可能で保守性の高いコードを作成することができます。

Go言語とGORMを使用したORMマッパーの実装では、以下のポイントに注意することが重要です:

  1. ドメインエンティティとデータベースモデルを明確に分離する
  2. マッパー関数を使用して、両者の変換を行う
  3. リポジトリインターフェースを通じてドメインレイヤーとやり取りする
  4. エラーハンドリングを適切に行い、ドメインレイヤーに意味のあるエラーを返す
  5. ソフトデリートなどの横断的な関心事を適切に実装する

これらのポイントを押さえることで、クリーンで保守性の高いORMマッパーを実装することができます。