Skip to content

Go-DDDマーケットプレイスアプリケーション システム機能の実装手順

1. 概要

この文書では、Go-DDDマーケットプレイスアプリケーションにおけるシステム機能の実装手順を説明します。システム機能は、アプリケーション全体に関わる横断的な関心事(クロスカッティングコンサーン)を扱います。実装はドメイン駆動設計(DDD)の原則に従い、レイヤードアーキテクチャを使用します。

1.1 リクエスト処理フロー

以下はAPIリクエスト処理のフローを表すステート図です:

uml diagram

2. 実装するシステム機能

Go-DDDマーケットプレイスアプリケーションでは、以下のシステム機能を実装します:

  1. RESTful API: 標準的なRESTful APIエンドポイントの提供
  2. エラーハンドリング: アプリケーション全体での一貫したエラー処理
  3. タイムスタンプ管理: エンティティの作成日時と更新日時の管理
  4. ソフトデリート: エンティティの論理削除機能
  5. データベースマイグレーション: データベーススキーマの自動作成と更新

3. 実装ステップ

以下はシステム機能の実装ステップの概要を表すフロー図です:

uml diagram

3.1 RESTful API

Echoフレームワークを使用したRESTful APIの実装については、Echoフレームワーク実装の詳細解説を参照してください。

3.1.1 APIルーティングの設定

APIルーティングの設定では、Echoフレームワークを使用して各エンドポイントを定義します。詳細な実装はEchoフレームワーク実装の詳細解説を参照してください。

3.1.2 コントローラーの修正

商品コントローラーと出品者コントローラーは、APIグループを受け取るように修正されています。詳細な実装は以下のページを参照してください:

3.2 認証・認可

JWT(JSON Web Token)を使用した認証・認可の実装については、Echo JWT認証の詳細解説を参照してください。

JWT認証は以下の機能を提供します:

  1. ユーザー認証とトークン発行
  2. 保護されたリソースへのアクセス制御
  3. ステートレスな認証メカニズム
  4. セキュリティ対策(トークンの有効期限、署名検証など)

3.3 エラーハンドリング

3.3.1 カスタムエラータイプの定義

  1. internal/application/common/errors.go にカスタムエラータイプを定義します。
package common

import (
    "fmt"
)

// ErrorType はエラーの種類を表す型
type ErrorType string

const (
    // NotFound はエンティティが見つからない場合のエラー
    NotFound ErrorType = "NOT_FOUND"

    // ValidationError はバリデーションエラー
    ValidationError ErrorType = "VALIDATION_ERROR"

    // DatabaseError はデータベース操作に関するエラー
    DatabaseError ErrorType = "DATABASE_ERROR"

    // UnknownError は未分類のエラー
    UnknownError ErrorType = "UNKNOWN_ERROR"
)

// AppError はアプリケーション固有のエラー型
type AppError struct {
    Type    ErrorType
    Message string
    Err     error
}

// Error はエラーインターフェースを実装
func (e AppError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("%s: %s (%s)", e.Type, e.Message, e.Err.Error())
    }
    return fmt.Sprintf("%s: %s", e.Type, e.Message)
}

// NewNotFoundError は新しいNotFoundエラーを作成
func NewNotFoundError(message string, err error) AppError {
    return AppError{
        Type:    NotFound,
        Message: message,
        Err:     err,
    }
}

// NewValidationError は新しいValidationErrorを作成
func NewValidationError(message string, err error) AppError {
    return AppError{
        Type:    ValidationError,
        Message: message,
        Err:     err,
    }
}

// NewDatabaseError は新しいDatabaseErrorを作成
func NewDatabaseError(message string, err error) AppError {
    return AppError{
        Type:    DatabaseError,
        Message: message,
        Err:     err,
    }
}

// NewUnknownError は新しいUnknownErrorを作成
func NewUnknownError(message string, err error) AppError {
    return AppError{
        Type:    UnknownError,
        Message: message,
        Err:     err,
    }
}

3.3.2 エラーハンドリングミドルウェアの実装

エラーハンドリングミドルウェアは、アプリケーション全体で一貫したエラーレスポンスを提供するためのミドルウェアです。詳細な実装はEchoフレームワーク実装の詳細解説を参照してください。

エラーハンドリングミドルウェアは以下の機能を提供します:

  1. アプリケーションエラー(AppError)を適切なHTTPステータスコードとメッセージに変換
  2. Echoフレームワークの標準HTTPエラーの処理
  3. 予期しないエラーの適切な処理

このミドルウェアにより、クライアントには常に一貫した形式のエラーレスポンスが返されます。

3.3.3 ミドルウェアの適用

エラーハンドリングミドルウェアは、Echoフレームワークのルーター設定時に適用されます。詳細な実装はEchoフレームワーク実装の詳細解説を参照してください。

ミドルウェアの適用により、アプリケーション全体で一貫したエラー処理が可能になります。これにより、開発者はビジネスロジックに集中し、エラー処理の詳細を気にする必要がなくなります。

3.4 タイムスタンプ管理

3.4.1 タイムスタンプを持つベースエンティティの作成

  1. internal/domain/entities/base_entity.go にベースエンティティを作成します。
package entities

import (
    "time"
)

// BaseEntity はすべてのエンティティに共通するフィールドを持つ構造体
type BaseEntity struct {
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt *time.Time
}

// IsDeleted はエンティティが削除されているかどうかを返す
func (e *BaseEntity) IsDeleted() bool {
    return e.DeletedAt != nil
}

// SetCreatedAt は作成日時を設定する
func (e *BaseEntity) SetCreatedAt(t time.Time) {
    e.CreatedAt = t
}

// SetUpdatedAt は更新日時を設定する
func (e *BaseEntity) SetUpdatedAt(t time.Time) {
    e.UpdatedAt = t
}

// SoftDelete はエンティティを論理削除する
func (e *BaseEntity) SoftDelete() {
    now := time.Now()
    e.DeletedAt = &now
}

3.4.2 既存エンティティの修正

  1. 商品エンティティと出品者エンティティを修正して、ベースエンティティを埋め込みます。
// Product エンティティ
type Product struct {
    Id          string
    Name        string
    Description string
    Price       float64
    SellerId    string
    BaseEntity  // ベースエンティティを埋め込む
}

// Seller エンティティ
type Seller struct {
    Id         string
    Name       string
    Email      string
    BaseEntity // ベースエンティティを埋め込む
}

3.5 ソフトデリート

3.5.1 リポジトリの修正

  1. リポジトリの実装を修正して、ソフトデリートをサポートします。
// GormProductRepository
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
}

// GormSellerRepository
func (r *GormSellerRepository) Delete(id string) error {
    // ソフトデリート
    result := r.db.Model(&DBSeller{}).Where("id = ?", id).Update("deleted_at", time.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.6 環境変数管理

環境変数管理の詳細については、環境変数管理(godotenv)を参照してください。

環境変数を使用することで、以下のメリットが得られます:

  1. 異なる環境(開発、テスト、本番)で異なる設定を使用可能
  2. 機密情報(データベースパスワード、APIキーなど)をコードから分離
  3. アプリケーションの再ビルドなしに設定を変更可能

3.7 データベースマイグレーション

3.7.1 マイグレーション機能の実装

  1. internal/infrastructure/db/postgres/migration.go にマイグレーション機能を実装します。
package postgres

import (
    "gorm.io/gorm"
)

// RunMigrations はデータベースマイグレーションを実行する
func RunMigrations(db *gorm.DB) error {
    // 自動マイグレーション
    err := db.AutoMigrate(
        &DBProduct{},
        &DBSeller{},
    )
    return err
}

3.6.2 アプリケーション起動時にマイグレーションを実行

  1. cmd/marketplace/main.go を修正して、アプリケーション起動時にマイグレーションを実行します。
func main() {
    dsn := "host=localhost user=root password=password dbname=mydb port=9920 sslmode=disable TimeZone=Asia/Shanghai"
    port := ":9090"

    gormDB, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatalf("Failed to connect to database: %v", err)
    }

    // データベースマイグレーションを実行
    if err := postgres2.RunMigrations(gormDB); err != nil {
        log.Fatalf("Failed to run migrations: %v", err)
    }

    // リポジトリの初期化
    productRepo := postgres2.NewGormProductRepository(gormDB)
    sellerRepo := postgres2.NewGormSellerRepository(gormDB)

    // サービスの初期化
    productService := services.NewProductService(productRepo, sellerRepo)
    sellerService := services.NewSellerService(sellerRepo)

    // Echoサーバーの初期化
    e := echo.New()

    // ルーターの設定
    rest.SetupRouter(e, productService, sellerService)

    // サーバーの起動
    if err := e.Start(port); err != nil {
        log.Fatalf("Failed to start server: %v", err)
    }
}

4. テスト

4.1 エラーハンドリングのテスト

  1. internal/application/common/errors_test.go にエラーハンドリングのテストを実装します。
package common

import (
    "errors"
    "testing"
)

func TestAppError_Error(t *testing.T) {
    // 内部エラーがある場合
    innerErr := errors.New("inner error")
    appErr := AppError{
        Type:    ValidationError,
        Message: "validation failed",
        Err:     innerErr,
    }
    expected := "VALIDATION_ERROR: validation failed (inner error)"
    if appErr.Error() != expected {
        t.Errorf("AppError.Error() = %v, want %v", appErr.Error(), expected)
    }

    // 内部エラーがない場合
    appErr = AppError{
        Type:    NotFound,
        Message: "entity not found",
        Err:     nil,
    }
    expected = "NOT_FOUND: entity not found"
    if appErr.Error() != expected {
        t.Errorf("AppError.Error() = %v, want %v", appErr.Error(), expected)
    }
}

func TestNewErrors(t *testing.T) {
    innerErr := errors.New("inner error")

    // NotFoundError
    err := NewNotFoundError("not found", innerErr)
    if err.Type != NotFound {
        t.Errorf("NewNotFoundError().Type = %v, want %v", err.Type, NotFound)
    }
    if err.Message != "not found" {
        t.Errorf("NewNotFoundError().Message = %v, want %v", err.Message, "not found")
    }
    if err.Err != innerErr {
        t.Errorf("NewNotFoundError().Err = %v, want %v", err.Err, innerErr)
    }

    // ValidationError
    err = NewValidationError("validation failed", innerErr)
    if err.Type != ValidationError {
        t.Errorf("NewValidationError().Type = %v, want %v", err.Type, ValidationError)
    }

    // DatabaseError
    err = NewDatabaseError("database error", innerErr)
    if err.Type != DatabaseError {
        t.Errorf("NewDatabaseError().Type = %v, want %v", err.Type, DatabaseError)
    }

    // UnknownError
    err = NewUnknownError("unknown error", innerErr)
    if err.Type != UnknownError {
        t.Errorf("NewUnknownError().Type = %v, want %v", err.Type, UnknownError)
    }
}

4.2 ミドルウェアのテスト

  1. internal/interface/api/rest/middleware_test.go にミドルウェアのテストを実装します。
package rest

import (
    "errors"
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"

    "github.com/labstack/echo/v4"
    "github.com/sklinkert/go-ddd/internal/application/common"
)

func TestErrorHandlerMiddleware(t *testing.T) {
    e := echo.New()

    // テストケース
    testCases := []struct {
        name           string
        handlerError   error
        expectedStatus int
        expectedError  string
    }{
        {
            name:           "No error",
            handlerError:   nil,
            expectedStatus: http.StatusOK,
            expectedError:  "",
        },
        {
            name:           "NotFound error",
            handlerError:   common.NewNotFoundError("entity not found", nil),
            expectedStatus: http.StatusNotFound,
            expectedError:  "NotFound",
        },
        {
            name:           "Validation error",
            handlerError:   common.NewValidationError("validation failed", nil),
            expectedStatus: http.StatusBadRequest,
            expectedError:  "ValidationError",
        },
        {
            name:           "Database error",
            handlerError:   common.NewDatabaseError("database error", nil),
            expectedStatus: http.StatusInternalServerError,
            expectedError:  "DatabaseError",
        },
        {
            name:           "Unknown error",
            handlerError:   common.NewUnknownError("unknown error", nil),
            expectedStatus: http.StatusInternalServerError,
            expectedError:  "InternalServerError",
        },
        {
            name:           "Generic error",
            handlerError:   errors.New("generic error"),
            expectedStatus: http.StatusInternalServerError,
            expectedError:  "InternalServerError",
        },
    }

    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            // テストハンドラー
            handler := func(c echo.Context) error {
                return tc.handlerError
            }

            // ミドルウェアを適用
            middlewareHandler := ErrorHandlerMiddleware(handler)

            // リクエスト作成
            req := httptest.NewRequest(http.MethodGet, "/", nil)
            rec := httptest.NewRecorder()
            c := e.NewContext(req, rec)

            // ハンドラー実行
            middlewareHandler(c)

            // レスポンス検証
            if tc.handlerError == nil {
                if rec.Code != http.StatusOK {
                    t.Errorf("Expected status code %d, got %d", http.StatusOK, rec.Code)
                }
            } else {
                if rec.Code != tc.expectedStatus {
                    t.Errorf("Expected status code %d, got %d", tc.expectedStatus, rec.Code)
                }

                if tc.expectedError != "" && !strings.Contains(rec.Body.String(), tc.expectedError) {
                    t.Errorf("Expected error %s, got %s", tc.expectedError, rec.Body.String())
                }
            }
        })
    }
}

4.3 ベースエンティティのテスト

  1. internal/domain/entities/base_entity_test.go にベースエンティティのテストを実装します。
package entities

import (
    "testing"
    "time"
)

func TestBaseEntity_IsDeleted(t *testing.T) {
    // 削除されていないエンティティ
    entity := BaseEntity{
        CreatedAt: time.Now(),
        UpdatedAt: time.Now(),
        DeletedAt: nil,
    }

    if entity.IsDeleted() {
        t.Error("BaseEntity.IsDeleted() = true, want false")
    }

    // 削除されたエンティティ
    now := time.Now()
    entity.DeletedAt = &now

    if !entity.IsDeleted() {
        t.Error("BaseEntity.IsDeleted() = false, want true")
    }
}

func TestBaseEntity_SetCreatedAt(t *testing.T) {
    entity := BaseEntity{}
    now := time.Now()

    entity.SetCreatedAt(now)

    if !entity.CreatedAt.Equal(now) {
        t.Errorf("BaseEntity.CreatedAt = %v, want %v", entity.CreatedAt, now)
    }
}

func TestBaseEntity_SetUpdatedAt(t *testing.T) {
    entity := BaseEntity{}
    now := time.Now()

    entity.SetUpdatedAt(now)

    if !entity.UpdatedAt.Equal(now) {
        t.Errorf("BaseEntity.UpdatedAt = %v, want %v", entity.UpdatedAt, now)
    }
}

func TestBaseEntity_SoftDelete(t *testing.T) {
    entity := BaseEntity{
        CreatedAt: time.Now(),
        UpdatedAt: time.Now(),
    }

    entity.SoftDelete()

    if entity.DeletedAt == nil {
        t.Error("BaseEntity.DeletedAt is nil, want non-nil")
    }

    if !entity.IsDeleted() {
        t.Error("BaseEntity.IsDeleted() = false, want true")
    }
}

5. まとめ

この実装手順では、Go-DDDマーケットプレイスアプリケーションのシステム機能を、ドメイン駆動設計の原則に従って実装する方法を説明しました。システム機能は、アプリケーション全体に関わる横断的な関心事を扱い、以下の機能を提供します:

  1. RESTful API: 標準的なRESTful APIエンドポイントを提供し、APIバージョニングを実装
  2. エラーハンドリング: カスタムエラータイプとミドルウェアを使用して、一貫したエラー処理を実現
  3. タイムスタンプ管理: ベースエンティティを使用して、エンティティの作成日時と更新日時を管理
  4. ソフトデリート: エンティティを物理的に削除せず、論理的に削除する機能を実装
  5. データベースマイグレーション: アプリケーション起動時にデータベーススキーマを自動的に作成・更新

これらのシステム機能を実装することで、アプリケーションの保守性、拡張性、および堅牢性が向上します。また、ドメインロジックとシステム機能を明確に分離することで、コードの可読性と再利用性も向上します。