Go-DDDマーケットプレイスアプリケーション 出品者管理機能の実装手順¶
1. 概要¶
この文書では、Go-DDDマーケットプレイスアプリケーションにおける出品者管理機能の実装手順を説明します。実装はドメイン駆動設計(DDD)の原則に従い、レイヤードアーキテクチャを使用します。
1.1 出品者のライフサイクル¶
以下は出品者エンティティのライフサイクルを表すステート図です:
2. 実装ステップ¶
以下は出品者管理機能の実装ステップの概要を表すフロー図です:
2.1 ドメインレイヤーの実装¶
2.1.1 出品者エンティティの作成¶
internal/domain/entities/seller.goに出品者エンティティを定義します。
package entities
import (
"time"
"errors"
"regexp"
)
type Seller struct {
Id string
Name string
Email string
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
}
func (s *Seller) Validate() error {
if s.Name == "" {
return errors.New("seller name cannot be empty")
}
if s.Email == "" {
return errors.New("seller email cannot be empty")
}
// メールアドレスの簡易バリデーション
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
if !emailRegex.MatchString(s.Email) {
return errors.New("invalid email format")
}
return nil
}
func (s *Seller) UpdateName(name string) error {
if name == "" {
return errors.New("seller name cannot be empty")
}
s.Name = name
s.UpdatedAt = time.Now()
return nil
}
func (s *Seller) UpdateEmail(email string) error {
if email == "" {
return errors.New("seller email cannot be empty")
}
// メールアドレスの簡易バリデーション
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
if !emailRegex.MatchString(email) {
return errors.New("invalid email format")
}
s.Email = email
s.UpdatedAt = time.Now()
return nil
}
func (s *Seller) SoftDelete() {
now := time.Now()
s.DeletedAt = &now
}
func (s *Seller) IsDeleted() bool {
return s.DeletedAt != nil
}
2.1.2 バリデーション済み出品者エンティティの作成¶
internal/domain/entities/validated_seller.goにバリデーション済み出品者エンティティを定義します。
package entities
type ValidatedSeller struct {
Seller Seller
}
func NewValidatedSeller(seller Seller) (*ValidatedSeller, error) {
if err := seller.Validate(); err != nil {
return nil, err
}
return &ValidatedSeller{Seller: seller}, nil
}
2.1.3 出品者リポジトリインターフェースの定義¶
internal/domain/repositories/seller_repository.goに出品者リポジトリインターフェースを定義します。
package repositories
import (
"github.com/sklinkert/go-ddd/internal/domain/entities"
)
type SellerRepository interface {
Save(seller entities.Seller) error
FindAll() ([]entities.Seller, error)
ById(id string) (entities.Seller, error)
Update(seller entities.Seller) error
Delete(id string) error
}
2.2 アプリケーションレイヤーの実装¶
2.2.1 出品者コマンドの作成¶
internal/application/command/seller_commands.goに出品者コマンドを定義します。
package command
type CreateSellerCommand struct {
Name string
Email string
}
type UpdateSellerNameCommand struct {
Id string
Name string
}
type UpdateSellerEmailCommand struct {
Id string
Email string
}
type DeleteSellerCommand struct {
Id string
}
2.2.2 出品者クエリの作成¶
internal/application/query/seller_queries.goに出品者クエリを定義します。
package query
type FindAllSellersQuery struct {}
type FindSellerByIdQuery struct {
Id string
}
2.2.3 出品者サービスの実装¶
internal/application/services/seller_service.goに出品者サービスを実装します。
package services
import (
"errors"
"time"
"github.com/google/uuid"
"github.com/sklinkert/go-ddd/internal/application/command"
"github.com/sklinkert/go-ddd/internal/domain/entities"
"github.com/sklinkert/go-ddd/internal/domain/repositories"
)
type SellerService struct {
sellerRepo repositories.SellerRepository
}
func NewSellerService(sellerRepo repositories.SellerRepository) *SellerService {
return &SellerService{
sellerRepo: sellerRepo,
}
}
func (s *SellerService) Create(cmd command.CreateSellerCommand) (entities.Seller, error) {
// 出品者エンティティの作成
now := time.Now()
seller := entities.Seller{
Id: uuid.New().String(),
Name: cmd.Name,
Email: cmd.Email,
CreatedAt: now,
UpdatedAt: now,
}
// バリデーション
if _, err := entities.NewValidatedSeller(seller); err != nil {
return entities.Seller{}, err
}
// 保存
if err := s.sellerRepo.Save(seller); err != nil {
return entities.Seller{}, err
}
return seller, nil
}
func (s *SellerService) FindAll() ([]entities.Seller, error) {
return s.sellerRepo.FindAll()
}
func (s *SellerService) ById(id string) (entities.Seller, error) {
return s.sellerRepo.ById(id)
}
func (s *SellerService) UpdateName(cmd command.UpdateSellerNameCommand) (entities.Seller, error) {
// 既存の出品者を取得
seller, err := s.sellerRepo.ById(cmd.Id)
if err != nil {
return entities.Seller{}, err
}
// 名前を更新
if err := seller.UpdateName(cmd.Name); err != nil {
return entities.Seller{}, err
}
// バリデーション
if _, err := entities.NewValidatedSeller(seller); err != nil {
return entities.Seller{}, err
}
// 保存
if err := s.sellerRepo.Update(seller); err != nil {
return entities.Seller{}, err
}
return seller, nil
}
func (s *SellerService) UpdateEmail(cmd command.UpdateSellerEmailCommand) (entities.Seller, error) {
// 既存の出品者を取得
seller, err := s.sellerRepo.ById(cmd.Id)
if err != nil {
return entities.Seller{}, err
}
// メールアドレスを更新
if err := seller.UpdateEmail(cmd.Email); err != nil {
return entities.Seller{}, err
}
// バリデーション
if _, err := entities.NewValidatedSeller(seller); err != nil {
return entities.Seller{}, err
}
// 保存
if err := s.sellerRepo.Update(seller); err != nil {
return entities.Seller{}, err
}
return seller, nil
}
func (s *SellerService) Delete(cmd command.DeleteSellerCommand) error {
// 既存の出品者を取得して存在確認
_, err := s.sellerRepo.ById(cmd.Id)
if err != nil {
return errors.New("seller not found")
}
// 削除
return s.sellerRepo.Delete(cmd.Id)
}
2.3 インフラストラクチャレイヤーの実装¶
2.3.1 出品者リポジトリの実装(PostgreSQL)¶
internal/infrastructure/db/postgres/seller_repository.goに出品者リポジトリの実装を作成します。
package postgres
import (
"errors"
"gorm.io/gorm"
"github.com/sklinkert/go-ddd/internal/domain/entities"
)
type GormSellerRepository struct {
db *gorm.DB
}
func NewGormSellerRepository(db *gorm.DB) *GormSellerRepository {
return &GormSellerRepository{db: db}
}
func (r *GormSellerRepository) Save(seller entities.Seller) error {
dbSeller := toDBSeller(seller)
result := r.db.Create(&dbSeller)
return result.Error
}
func (r *GormSellerRepository) FindAll() ([]entities.Seller, error) {
var dbSellers []DBSeller
result := r.db.Find(&dbSellers)
if result.Error != nil {
return nil, result.Error
}
sellers := make([]entities.Seller, len(dbSellers))
for i, dbSeller := range dbSellers {
sellers[i] = toDomainSeller(dbSeller)
}
return sellers, nil
}
func (r *GormSellerRepository) ById(id string) (entities.Seller, error) {
var dbSeller DBSeller
result := r.db.First(&dbSeller, "id = ?", id)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return entities.Seller{}, errors.New("seller not found")
}
return entities.Seller{}, result.Error
}
return toDomainSeller(dbSeller), nil
}
func (r *GormSellerRepository) Update(seller entities.Seller) error {
dbSeller := toDBSeller(seller)
result := r.db.Save(&dbSeller)
return result.Error
}
func (r *GormSellerRepository) Delete(id string) error {
// ソフトデリート
result := r.db.Delete(&DBSeller{}, "id = ?", id)
return result.Error
}
2.3.2 出品者マッパーの実装¶
internal/infrastructure/db/postgres/seller_mapper.goに出品者マッパーを実装します。
package postgres
import (
"time"
"github.com/sklinkert/go-ddd/internal/domain/entities"
)
type DBSeller struct {
ID string `gorm:"primaryKey"`
Name string
Email string
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
}
func (DBSeller) TableName() string {
return "sellers"
}
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,
}
}
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.4 インターフェースレイヤーの実装¶
2.4.1 出品者コントローラーの実装¶
internal/interface/api/rest/seller_controller.goに出品者コントローラーを実装します。
```go package rest
import ( "net/http" "time" "github.com/labstack/echo/v4" "github.com/sklinkert/go-ddd/internal/application/command" "github.com/sklinkert/go-ddd/internal/application/services" )
type SellerController struct { sellerService *services.SellerService }
func NewSellerController(e echo.Echo, sellerService services.SellerService) { controller := &SellerController{ sellerService: sellerService, }
api := e.Group("/api/v1")
api.POST("/sellers", controller.Create)
api.GET("/sellers", controller.FindAll)
api.GET("/sellers/:id", controller.ById)
api.PUT("/sellers/:id/name", controller.UpdateName)
api.PUT("/sellers/:id/email", controller.UpdateEmail)
api.DELETE("/sellers/:id", controller.Delete)
}
type CreateSellerRequest struct {
Name string json:"name"
Email string json:"email"
}
type UpdateSellerNameRequest struct {
Name string json:"name"
}
type UpdateSellerEmailRequest struct {
Email string json:"email"
}
type SellerResponse struct {
Id string json:"id"
Name string json:"name"
Email string json:"email"
CreatedAt string json:"createdAt"
UpdatedAt string json:"updatedAt"
DeletedAt *string json:"deletedAt,omitempty"
}
func (ctrl *SellerController) Create(c echo.Context) error { var req CreateSellerRequest if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) }
cmd := command.CreateSellerCommand{
Name: req.Name,
Email: req.Email,
}
seller, err := ctrl.sellerService.Create(cmd)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
resp := toSellerResponse(seller)
return c.JSON(http.StatusCreated, resp)
}
func (ctrl *SellerController) FindAll(c echo.Context) error { sellers, err := ctrl.sellerService.FindAll() if err != nil { return c.JSON(http