登录
首页 >  Golang >  Go问答

go中的存储库模式和连接表

来源:stackoverflow

时间:2024-03-17 19:24:29 216浏览 收藏

在领域驱动设计中,处理实体之间的连接表时面临着挑战。直接连接表虽然简单,但效率低下。而将实体合并到一个接口或构建封装实体来解决此问题,可能会导致实体包含许多空参数。因此,需要寻找一种干净且有效的解决方案来实现连接表中的查询。

问题内容

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

所有基本的增删改查操作都很简单,基本上 1 个实体 => 1 个表 => 1 个存储库 => 1 个服务

但我无法找出处理两个实体之间的联接表的最干净的方法。

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

在这种模式下,表在哪里连接?

  • 我一直在考虑现在构建可以封装答案的实体,但可以有效地为 1 个查询创建 1 个实体 + 存储库...

  • 我还认为将多个实体合并到一个接口中可能会部分解决这个问题,但这会导致我的实体出现许多空参数(在执行时很少需要所有选项卡中的所有字段)连接)

解决这个问题的正确方法/模式是什么,适合 ddd 或至少是干净的?

-- 编辑示例:

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 的外键。

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

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

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

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

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

这将“工作”,但我正在尝试找到一种方法在一个查询中完成它。因为 user.project_id 和 project.id 上的简单 sql 连接将为我提供查询中的所有数据。


解决方案


对于 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 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 来说看起来很可怕,值得提出自己的问题(这个想法是工作单元的存储库版本在引擎盖下用启动的 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,或者你可以部分模仿它。如果您想要灵活性,请选择数据库一次并使用纯数据库句柄进行操作

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

如果您想要连接的表形成单个聚合,则只需将它们连接到您的查询中,并始终返回并存储完整的聚合。在这种情况下,您只有根实体的存储库。这可能不是您的场景,因为您说过您有想要加入的其他实体的存储库(除非您有设计问题)。

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

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

本篇关于《go中的存储库模式和连接表》的介绍就到此结束啦,但是学无止境,想要了解学习更多关于Golang的相关知识,请关注golang学习网公众号!

声明:本文转载于:stackoverflow 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>