2 回答
TA贡献1752条经验 获得超4个赞
至于加入部分,您的问题很容易回答,但是对于 DDD,当前的语言可能性存在很多障碍。不过我会试一试的。。
好的,假设我们正在开发一个支持多语言的教育课程后端,我们需要连接两个表并随后映射到对象。我们有两个表(第一个包含与语言无关的数据,第二个包含与语言相关的数据)如果您是存储库倡导者,那么您将拥有类似的内容:
// Course represents e.g. calculus, combinatorics, etc.
type Course struct {
ID uint `json:"id" db:"id"`
Name string `json:"name" db:"name"`
Poster string `json:"poster" db:"poster"`
}
type CourseRepository interface {
List(ctx context.Context, localeID uint) ([]Course, error)
}
然后为 sql db 实现它,我们将得到类似的东西:
type courseRepository struct {
db *sqlx.DB
}
func NewCourseRepository(db *sqlx.DB) (CourseRepository, error) {
if db == nil {
return nil, errors.New("provided db handle to course repository is nil")
}
return &courseRepository{db:db}, nil
}
func (r *courseRepository) List(ctx context.Context, localeID uint) ([]Course, error) {
const query = `SELECT c.id, c.poster, ct.name FROM courses AS c JOIN courses_t AS ct ON c.id = ct.id WHERE ct.locale = $1`
var courses []Course
if err := r.db.SelectContext(ctx, &courses, query, localeID); err != nil {
return nil, fmt.Errorf("courses repostory/problem while trying to retrieve courses from database: %w", err)
}
return courses, nil
}
这同样适用于不同的相关对象。您只需要耐心地为您的对象与基础数据的映射建模。让我再举一个例子。
type City struct {
ID uint `db:"id"`
Country Country `db:"country"`
}
type Country struct {
ID uint `db:"id"`
Name string `db:"name"`
}
// CityRepository provides access to city store.
type CityRepository interface {
Get(ctx context.Context, cityID uint) (*City, error)
}
// Get retrieve city from database by specified id
func (r *cityRepository) Get(ctx context.Context, cityID uint) (*City, error) {
const query = `SELECT
city.id, country.id AS 'country.id', country.name AS 'country.name',
FROM city JOIN country ON city.country_id = country.id WHERE city.id = ?`
city := City{}
if err := r.db.GetContext(ctx, &city, query, cityID); err != nil {
if err == sql.ErrNoRows {
return nil, ErrNoCityEntity
}
return nil, fmt.Errorf("city repository / problem occurred while trying to retrieve city from database: %w", err)
}
return &city, nil
}
现在,一切看起来都很干净,直到您意识到 Go 实际上(就目前而言)不支持泛型,此外在大多数情况下人们不鼓励使用反射功能,因为它会使您的程序变慢。为了完全打乱你的想象,从这一刻起你需要交易功能......
如果您来自其他语言,您可以尝试通过以下方式实现它:
// UnitOfWork is the interface that any UnitOfWork has to follow
// the only methods it as are to return Repositories that work
// together to achieve a common purpose/work.
type UnitOfWork interface {
Entities() EntityRepository
OtherEntities() OtherEntityRepository
}
// StartUnitOfWork it's the way to initialize a typed UoW, it has a uowFn
// which is the callback where all the work should be done, it also has the
// repositories, which are all the Repositories that belong to this UoW
type StartUnitOfWork func(ctx context.Context, t Type, uowFn UnitOfWorkFn, repositories ...interface{}) error
// UnitOfWorkFn is the signature of the function
// that is the callback of the StartUnitOfWork
type UnitOfWorkFn func(ctx context.Context, uw UnitOfWork) error
我故意错过了一个实现,因为它对于 sql 来说看起来很可怕并且值得提出自己的问题(这个想法是工作单元的存储库版本在引擎盖下用 start tx 装饰),在你解决了这个问题之后,你或多或少会有
err = svc.startUnitOfWork(ctx, uow.Write, func(ctx context.Context, uw uow.UnitOfWork) error {
// _ = uw.Entities().Store(entity)
// _ = uw.OtherEntities().Store(otherEntity)
return nil
}, svc.entityRepository, svc.otherEntityRepository)
所以在这里你到达了决赛,在大多数情况下,人们开始说你编写的代码似乎不习惯引用类似的东西。关键是概念写得太抽象了,物化 DDD 是否适用于 Golang 还是您可以部分模仿它是一个哲学问题。如果您想要灵活性,请选择一次数据库并使用纯数据库句柄进行操作
TA贡献2012条经验 获得超12个赞
根据您要读取的数据,解决方案会有所不同:
如果您要连接的表形成一个单一的聚合,那么只需将它们连接到您的查询中并始终返回并存储完整的聚合。在这种情况下,您只有根实体的存储库。这可能不是您的情况,因为您说您有要加入的其他实体的存储库(除非您有设计问题)。
如果要加入的表属于不同的有界上下文,则不应加入它们。更好的方法是在每个有界上下文上提交一个查询,以便它们保持解耦。这些多个查询将来自不同的地方,具体取决于您的架构:直接来自客户端、来自 API 网关、来自某种应用程序服务等。
如果表属于单个有界上下文,但来自多个聚合,那么最简洁的方法是遵循 CQRS(命令/查询隔离)。简单来说,您为查询定义了一个特定接口,其中包含您正在实现的用例所需的输入和输出。这种分离使您摆脱了在尝试使用 Commands 基础结构进行查询时发现的限制(您拥有的 1 对 1 实体/存储库关系)。此查询接口的简单实现可能是对现有表进行连接的查询。这快速且易于实现,但这意味着您的命令和查询在代码中是分开的,而不是在数据库级别。理想情况下,您会在数据库中创建一个(非规范化的)读取模型表,包含该特定查询所需的所有列,并在每次更新源表之一时更新(这通常通过域事件完成)。这允许您使用正确的列、数据格式和索引来优化您的查询表,但作为一个缺点,它会在写入和读取模型之间引入一些复杂性和最终一致性。
- 2 回答
- 0 关注
- 99 浏览
添加回答
举报