go中的存储库模式和联接表
我目前正试图围绕领域驱动的设计、实体、服务、回购等构建我的应用程序 所有基本的crud操作都很简单,基本上1个实体=>1个表=>1个存储库=>1个服务 但我想不出处理两个实体之间的联接表的最干净的方法 可以在联接内部按表进行一次查询,而且它是“干净的”(也就是说),但效率不高,因为一个简单的联接会导致一次查询 在这种模式下,表在哪里连接go中的存储库模式和联接表,go,design-patterns,domain-driven-design,ddd-repositories,Go,Design Patterns,Domain Driven Design,Ddd Repositories,我目前正试图围绕领域驱动的设计、实体、服务、回购等构建我的应用程序 所有基本的crud操作都很简单,基本上1个实体=>1个表=>1个存储库=>1个服务 但我想不出处理两个实体之间的联接表的最干净的方法 可以在联接内部按表进行一次查询,而且它是“干净的”(也就是说),但效率不高,因为一个简单的联接会导致一次查询 在这种模式下,表在哪里连接 我一直在考虑现在构建实体来封装答案,但这将有效地为一个查询创建一个实体+存储库 我还认为,将多个实体合并到一个接口中可能会部分解决这个问题,但这会导致我的实体
- 我一直在考虑现在构建实体来封装答案,但这将有效地为一个查询创建一个实体+存储库
- 我还认为,将多个实体合并到一个接口中可能会部分解决这个问题,但这会导致我的实体有许多空参数(很少在执行连接时需要所有选项卡中的所有字段)
type User struct {
ID int `db:"id"`
ProjectID int `db:"project_id"`
RoleID int `db:"role_id"`
Email string `db:"email"`
FirstName string `db:"first_name"`
LastName string `db:"last_name"`
Password string `db:"password"`
}
type UserRepository interface {
FindById(int) (*User, error)
FindByEmail(string) (*User, error)
Create(user *User) error
Update(user *User) error
Delete(int) errorr
}
type Project struct {
ID int `db:"id"``
Name string `db:"name"`
Description string `db:"description"`
}
这里我有一个简单的用户存储库。我在“项目”表中有类似的内容。可以创建表格、获取项目的所有信息、删除等
如您所见,UserID具有它所属的项目ID的外键
我的问题是当我需要从用户那里检索所有信息时,比如说“项目名称”和描述。(i)表/实体具有更多的参数)
我需要在user.project\u id和project.id中做一个简单的连接,并在一个查询中检索用户+项目名称+描述的所有信息
有时会更复杂,因为会有3-4个实体像这样链接。(用户、项目、项目附加信息、角色等)
当然,我可以做N个查询,每个实体一个
user := userRepo.Find(user_id)
project := projectRepo.FindByuser(user.deal_id)
这将“起作用”,但我正试图在一个查询中找到一种方法。由于user.project\u id和project.id上的简单sql连接将为我提供查询中的所有数据。至于连接部分,您的问题很容易回答,但是对于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)
}
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
}
// 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 db实现它,我们将有如下内容:
// 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)
}
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
}
// 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
这同样适用于不同的相关对象。您只需要耐心地为对象与底层数据的映射建模。让我再举一个例子
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实际上(就目前而言)不支持泛型,而且在大多数情况下,人们不鼓励使用反射功能,因为它会使您的程序变慢。为了彻底打乱您的想法,从现在开始,您需要事务性功能
如果您来自其他语言,您可以尝试通过以下方式实现:
// 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)
}
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
}
// 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来说看起来很可怕,应该有它自己的问题(想法是工作单元的存储库版本在引擎盖下用started 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,或者你可以部分模仿它。如果需要灵活性,请选择数据库一次并使用纯db句柄操作,具体取决于要读取的数据,解决方案将有所不同: 如果要联接的表形成单个聚合,则只需在查询中联接它们,并始终返回和存储完整的聚合。在这种情况下,您只有根实体的存储库。这可能不是您的场景,因为您说过您有其他要加入的实体的存储库(除非您有设计问题) 如果要联接的表属于不同的有界上下文,则不应联接它们。更好的方法是在每个有界上下文上提交一个查询,以便它们保持解耦。这些多个查询将来自不同的地方,具体取决于您的体系结构:直接来自客户端、来自API网关、来自某种应用程序服务,等等 如果表属于单个有界上下文,但来自多个聚合,那么最干净的方法是遵循CQR(命令/查询分离)。简单地说,您为您的查询定义了一个特定的接口,其中包含您正在实现的用例所需的输入和输出。这种分离将您从尝试使用Commands基础结构进行查询(您拥有的1对1实体/存储库关系)时遇到的限制中解放出来。这个查询接口的原始实现可以是一个对现有表进行联接的查询。这是快速且易于实现的,但这意味着您的命令和查询在代码中是分开的,而不是在DB级别。理想情况下,您应该在数据库中创建一个(非规范化的)读取模型表,其中包含特定查询所需的所有列,并在每次更新一个源表时进行更新(这通常通过域事件完成)。这允许您使用正确的列、数据格式和索引为查询优化表,但作为一个缺点,它在写模型和读模型之间引入了一些复杂性和最终的一致性