为了账号安全,请及时绑定邮箱和手机立即绑定

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

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

Go
素胚勾勒不出你 2022-06-06 15:47:50
我目前正在尝试围绕领域驱动设计、实体、服务、存储库构建我的应用程序...所有基本的 crud 操作都很简单,基本上 1 个实体 => 1 个表 => 1 个存储库 => 1 个服务但我想不出处理两个实体之间连接表的最干净的方法。可以通过联接内部的表进行 1 次查询,这将是“干净的”(可以这么说),但效率不高,因为简单的联接会导致一次查询。在这种模式下,表连接在哪里?我一直在考虑现在构建可以封装答案的实体,但这会有效地为 1 个查询创建 1 个实体 + 存储库......我还认为将多个实体合并到一个界面中可能会部分解决它,但它会导致我的实体出现许多空参数(在执行连接时很少需要来自所有标签的 ALL 字段)解决这个问题的正确方法/模式是什么,适合 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 连接会在查询中为我提供所有数据。
查看完整描述

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 还是您可以部分模仿它是一个哲学问题。如果您想要灵活性,请选择一次数据库并使用纯数据库句柄进行操作


查看完整回答
反对 回复 2022-06-06
?
繁花如伊

TA贡献2012条经验 获得超12个赞

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

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

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

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


查看完整回答
反对 回复 2022-06-06
  • 2 回答
  • 0 关注
  • 99 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信