Go-DDDマーケットプレイスアプリケーション システム機能の実装手順¶
1. 概要¶
この文書では、Go-DDDマーケットプレイスアプリケーションにおけるシステム機能の実装手順を説明します。システム機能は、アプリケーション全体に関わる横断的な関心事(クロスカッティングコンサーン)を扱います。実装はドメイン駆動設計(DDD)の原則に従い、レイヤードアーキテクチャを使用します。
1.1 リクエスト処理フロー¶
以下はAPIリクエスト処理のフローを表すステート図です:
2. 実装するシステム機能¶
Go-DDDマーケットプレイスアプリケーションでは、以下のシステム機能を実装します:
- RESTful API: 標準的なRESTful APIエンドポイントの提供
- エラーハンドリング: アプリケーション全体での一貫したエラー処理
- タイムスタンプ管理: エンティティの作成日時と更新日時の管理
- ソフトデリート: エンティティの論理削除機能
- データベースマイグレーション: データベーススキーマの自動作成と更新
3. 実装ステップ¶
以下はシステム機能の実装ステップの概要を表すフロー図です:
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認証は以下の機能を提供します:
- ユーザー認証とトークン発行
- 保護されたリソースへのアクセス制御
- ステートレスな認証メカニズム
- セキュリティ対策(トークンの有効期限、署名検証など)
3.3 エラーハンドリング¶
3.3.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フレームワーク実装の詳細解説を参照してください。
エラーハンドリングミドルウェアは以下の機能を提供します:
- アプリケーションエラー(AppError)を適切なHTTPステータスコードとメッセージに変換
- Echoフレームワークの標準HTTPエラーの処理
- 予期しないエラーの適切な処理
このミドルウェアにより、クライアントには常に一貫した形式のエラーレスポンスが返されます。
3.3.3 ミドルウェアの適用¶
エラーハンドリングミドルウェアは、Echoフレームワークのルーター設定時に適用されます。詳細な実装はEchoフレームワーク実装の詳細解説を参照してください。
ミドルウェアの適用により、アプリケーション全体で一貫したエラー処理が可能になります。これにより、開発者はビジネスロジックに集中し、エラー処理の詳細を気にする必要がなくなります。
3.4 タイムスタンプ管理¶
3.4.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 既存エンティティの修正¶
- 商品エンティティと出品者エンティティを修正して、ベースエンティティを埋め込みます。
// 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 リポジトリの修正¶
- リポジトリの実装を修正して、ソフトデリートをサポートします。
// 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)を参照してください。
環境変数を使用することで、以下のメリットが得られます:
- 異なる環境(開発、テスト、本番)で異なる設定を使用可能
- 機密情報(データベースパスワード、APIキーなど)をコードから分離
- アプリケーションの再ビルドなしに設定を変更可能
3.7 データベースマイグレーション¶
3.7.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 アプリケーション起動時にマイグレーションを実行¶
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 エラーハンドリングのテスト¶
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 ミドルウェアのテスト¶
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 ベースエンティティのテスト¶
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マーケットプレイスアプリケーションのシステム機能を、ドメイン駆動設計の原則に従って実装する方法を説明しました。システム機能は、アプリケーション全体に関わる横断的な関心事を扱い、以下の機能を提供します:
- RESTful API: 標準的なRESTful APIエンドポイントを提供し、APIバージョニングを実装
- エラーハンドリング: カスタムエラータイプとミドルウェアを使用して、一貫したエラー処理を実現
- タイムスタンプ管理: ベースエンティティを使用して、エンティティの作成日時と更新日時を管理
- ソフトデリート: エンティティを物理的に削除せず、論理的に削除する機能を実装
- データベースマイグレーション: アプリケーション起動時にデータベーススキーマを自動的に作成・更新
これらのシステム機能を実装することで、アプリケーションの保守性、拡張性、および堅牢性が向上します。また、ドメインロジックとシステム機能を明確に分離することで、コードの可読性と再利用性も向上します。