领域模型设计
核心领域概念
在 Go-ES 项目中,我们采用了领域驱动设计(DDD)方法论来构建产品管理系统的核心模型。以下是本项目中的核心领域概念:
产品(Product)
产品是本系统的核心聚合根,它包含以下属性:
- ID: 产品唯一标识符
- 名称(Name): 产品名称
- 描述(Description): 产品详细描述
- 价格(Price): 产品价格
- 类别(Category): 产品所属类别
- 标签(Tags): 产品关联的标签集合
- 创建时间(CreatedAt): 产品创建时间
- 更新时间(UpdatedAt): 产品最后更新时间
领域实体设计
Product 实体
产品实体是系统的核心,它在 domain/entity/product.go
中定义:
// Product 产品实体
type Product struct {
ID string `json:"id" gorm:"type:varchar(36);primary_key"`
Name string `json:"name" gorm:"type:varchar(255);not null"`
Description string `json:"description" gorm:"type:text"`
Price float64 `json:"price" gorm:"type:decimal(10,2);not null"`
Category string `json:"category" gorm:"type:varchar(100);index"`
Tags []string `json:"tags" gorm:"-"`
TagsString string `json:"-" gorm:"type:text"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
}
实体具有以下特点:
- 标识唯一性:使用 UUID 作为产品的唯一标识符
- 业务完整性:包含产品的所有业务属性
- 持久化映射:通过 GORM 标签定义与数据库的映射关系
- 标签处理:将字符串数组与数据库字符串之间进行转换
实体方法
产品实体提供了以下方法:
1. 实体创建
NewProduct
方法用于创建新产品实例:
// NewProduct 创建新产品
func NewProduct(name, description string, price float64, category string, tags []string) *Product {
return &Product{
ID: uuid.New().String(),
Name: name,
Description: description,
Price: price,
Category: category,
Tags: tags,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
}
2. 持久化钩子
实体提供了 GORM 钩子方法,在保存和加载时处理标签转换:
// BeforeSave 保存前的钩子
func (p *Product) BeforeSave() error {
// 将标签数组转换为字符串存储
if len(p.Tags) > 0 {
// 简单地使用逗号分隔标签
tagsStr := ""
for i, tag := range p.Tags {
if i > 0 {
tagsStr += ","
}
tagsStr += tag
}
p.TagsString = tagsStr
}
return nil
}
// AfterFind 查询后的钩子
func (p *Product) AfterFind() error {
// 将标签字符串转换为数组
if p.TagsString != "" {
p.Tags = splitTags(p.TagsString)
}
return nil
}
3. ES 文档转换
ToMap
方法将产品实体转换为适合 Elasticsearch 存储的映射:
// ToMap 将产品转换为 map,用于 ES 文档
func (p *Product) ToMap() map[string]interface{} {
return map[string]interface{}{
"id": p.ID,
"name": p.Name,
"description": p.Description,
"price": p.Price,
"category": p.Category,
"tags": p.Tags,
"created_at": p.CreatedAt.Format(time.RFC3339),
"updated_at": p.UpdatedAt.Format(time.RFC3339),
}
}
领域服务设计
领域服务封装了跨实体的业务逻辑,在 domain/service/product_service.go
中实现:
// ProductService 产品领域服务
type ProductService struct {
productRepo repository.ProductRepository
}
领域服务提供以下核心功能:
1. 产品创建
// CreateProduct 创建产品
func (s *ProductService) CreateProduct(ctx context.Context, product *entity.Product) error {
// 领域验证
if product.Name == "" || product.Price <= 0 {
return ErrInvalidProduct
}
// 使用仓储保存产品
err := s.productRepo.Create(ctx, product)
if err != nil {
return err
}
// 同步保存到 Elasticsearch
return s.productRepo.IndexProduct(ctx, product)
}
2. 产品更新
// UpdateProduct 更新产品
func (s *ProductService) UpdateProduct(ctx context.Context, product *entity.Product) error {
// 检查产品是否存在
_, err := s.productRepo.FindByID(ctx, product.ID)
if err != nil {
return ErrProductNotFound
}
// 领域验证
if product.Name == "" || product.Price <= 0 {
return ErrInvalidProduct
}
// 更新产品
err = s.productRepo.Update(ctx, product)
if err != nil {
return err
}
// 同步更新到 Elasticsearch
return s.productRepo.IndexProduct(ctx, product)
}
3. 产品搜索
// SearchProducts 搜索产品
func (s *ProductService) SearchProducts(ctx context.Context, keyword string, category string, page, pageSize int) ([]*entity.Product, int64, error) {
return s.productRepo.Search(ctx, keyword, category, page, pageSize)
}
仓储接口设计
仓储接口定义了领域实体的持久化操作,在 domain/repository/product_repository.go
中定义:
// ProductRepository 产品仓储接口
type ProductRepository interface {
// 基本 CRUD 操作
Create(ctx context.Context, product *entity.Product) error
FindByID(ctx context.Context, id string) (*entity.Product, error)
Update(ctx context.Context, product *entity.Product) error
Delete(ctx context.Context, id string) error
// 列表和搜索功能
FindAll(ctx context.Context, page, pageSize int) ([]*entity.Product, int64, error)
FindByCategory(ctx context.Context, category string, page, pageSize int) ([]*entity.Product, int64, error)
// 搜索功能
Search(ctx context.Context, keyword string, category string, page, pageSize int) ([]*entity.Product, int64, error)
// Elasticsearch 相关操作
IndexProduct(ctx context.Context, product *entity.Product) error
DeleteFromIndex(ctx context.Context, id string) error
// 重建索引
ReindexAll(ctx context.Context) error
}
仓储接口的设计体现了以下原则:
- 依赖倒置原则:领域层定义接口,基础设施层负责实现
- 单一职责原则:接口方法专注于单一功能
- 完整性:包含所有必要的数据访问操作
- 领域无关性:不暴露技术实现细节给领域层
领域模型与数据存储的关系
在本项目中,领域模型与数据存储有两种映射关系:
- 关系型数据库(MySQL):通过 GORM 将 Product 实体映射到数据库表
- Elasticsearch:通过 ToMap() 方法将 Product 实体转换为 ES 文档
这种双重存储机制实现了:
- 在关系型数据库中保证数据的完整性和一致性
- 在 Elasticsearch 中提供高效的全文搜索能力
领域模型与存储之间的同步由仓储实现负责,确保两套存储系统的数据一致性。