存储库模式和 Joining table in go

Repository pattern and Joining table in go

我目前正在尝试围绕域驱动设计、实体、服务、存储库构建我的应用程序...

所有基本的 crud 操作都很简单,基本上 1 个实体 => 1 table => 1 个存储库 => 1 个服务

但我想不出处理两个实体之间连接 table 的最简洁方法。

可以在联接内通过 table 进行 1 次查询,这将是“干净的”(可以这么说),但效率不高,因为简单的联接会导致一个查询。

table在这种模式下在哪里加入直播?

什么是适合 DDD 或至少是干净的解决此问题的正确 way/pattern?

-- 编辑示例:

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"`
}

这里我有一个简单的用户存储库。 "Project" table 我有类似的东西。可以创建 table 、获取项目的所有信息、删除等等

可以看到,UserID有所属项目ID的外键。

我的问题是当我需要从用户那里检索所有信息并说出“项目名称”和描述时。 (我实际上 table/entity 有更多的参数)

我需要在user.project_id和project.id中做一个简单的连接,并在一个查询中检索用户+项目名称+描述的所有信息。

有时会更复杂,因为会有 3-4 个实体像这样链接。 (用户、项目、project_additional_information、角色等)

当然我可以进行 N 个查询,每个实体一个。

user := userRepo.Find(user_id)
project := projectRepo.FindByuser(user.deal_id)

那会 "Work " 但我正在尝试找到一种在一个查询中完成它的方法。因为一个简单的 sql 加入 user.project_id 和 project.id 会给我查询中的所有数据。

至于 join 部分,您的问题很容易回答,但是对于 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 数据库实现它,我们将得到类似的东西:

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 = `
    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 来说看起来很可怕并且值得自己的问题(这个想法是工作单元的存储库版本在引擎盖下用启动的 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)

所以你到了决赛,在大多数情况下,人们开始说你写的代码似乎不符合惯用语,指的是像 that 这样的东西。关键是概念写得太抽象了,物化 DDD 是否适用于 Golang 还是你可以部分模仿它是一个哲学问题。如果要灵活,选一次数据库,用纯db handle操作

根据您要阅读的数据,解决方案会有所不同:

如果您要加入的 table 形成一个聚合,则只需将它们加入您的查询并始终 return 并存储完整的聚合。在这种情况下,您只有根实体的存储库。这可能不是您的方案,因为您说过您拥有要加入的其他实体的存储库(除非您有设计问题)。

如果您要加入的 table 属于不同的限界上下文,则不应加入它们。一种更好的方法是对每个限界上下文提交一个查询,以便它们保持解耦。这些多个查询将来自不同的地方,具体取决于您的体系结构:直接来自客户端、来自 API 网关、来自某种应用程序服务等。

如果 table 属于单个限界上下文,但来自多个聚合,那么最干净的方法是遵循 CQRS(Command/Query 隔离)。简而言之,您为查询定义了一个特定的接口,其中包含您正在实施的用例所需的输入和输出。这种分离使您摆脱了在尝试使用命令基础结构进行查询时发现的限制(您拥有的 1 对 1 entity/repository 关系)。这个查询接口的简单实现可能是一个查询,它连接你现有的 tables。这实现起来既快又容易,但这意味着您的命令和查询在代码中是分开的,而不是在数据库级别。理想情况下,您将在数据库中创建一个(非规范化的)读取模型 table,其中包含该特定查询所需的所有列,并在每次更新其中一个源 table 时更新(这是通常通过领域事件完成)。这允许您使用正确的列、数据格式和索引为您的查询优化 tables,但不利的是,它在写入和读取模型之间引入了一些复杂性和最终一致性。