【问题标题】:Golang - DTOs, Entities and MappingGolang - DTO、实体和映射
【发布时间】:2022-03-12 15:40:32
【问题描述】:

我是来自 C# 背景的 Go 新手,我只是对构建 Go 应用程序感到困惑。

假设我正在构建一个位于数据库之上的 REST API。另外,即使在完成后,考虑到业务的变迁等,此应用程序也可能需要经常更改。

在 C# 中,使用 Entity Framework 和 DTO 等工具,我通过从控制器给出的结果中抽象出数据库,在一定程度上缓解了这个问题。如果我更改数据库中一堆字段的名称,我可能必须更改我的数据库访问逻辑。不过,希望我使用 AutoMapper 映射到实体的 DTO 可以保持不变,因此我不会破坏依赖给定 DTO 结构的前端功能。

我应该用 Go 的结构复制这个结构吗?考虑到结构只是 DTO,这种方法的某些方面似乎是错误的,并且我将有很多与实体结构相同的 DTO 结构。我还必须设置将实体映射到 DTO 的逻辑。不知何故,这一切都感觉很单调,而且我在网上看到的许多示例都只是序列化数据库结构。

简而言之,人们如何避免他们的 API 和 Go 中的数据库之间的过度耦合,以及他们将如何广泛地分离应用程序的不同部分?

如果有什么不同,我打算使用sqlx 将数据库结果编组为结构,如果我不将实体与 DTO 分开,这意味着除了 JSON 之外还有更多标签。

【问题讨论】:

  • 好的方法是通过一些开源项目来获得如何处理这些问题的灵感。尝试通过gobuffalo.io 项目。对于这种特殊情况,github.com/markbates/pop 可能会有所帮助。
  • 我能给出的最好建议是不要尝试模仿其他语言,尤其是面向对象的语言。将 Java 或 C# 习语应用到 Go 将给你带来无穷无尽的痛苦。与其尝试按照你习惯的方式做事,不如做最简单的、在 Go 中看起来很惯用的事情,然后从那里迭代。 KISS+YAGNI 将为您省去很多麻烦。从现有的类似项目中寻找灵感。

标签: c# go


【解决方案1】:

对于 REST API,您通常会处理至少三个不同的实现层:

  • HTTP 处理程序
  • 某种业务逻辑/用例
  • 持久存储/数据库接口

您可以分别处理和构建它们中的每一个,这不仅可以将其解耦,还可以使其更易于测试。然后通过注入必要的位将这些部分组合在一起,因为它们符合您定义的接口。通常这会导致main 或单独的配置机制成为唯一知道what 被组合和注入how 的地方。

文章Applying The Clean Architecture to Go applications 很好地说明了如何分离各个部分。遵循这种方法的严格程度在一定程度上取决于项目的复杂性。

下面是一个非常基本的分解,将处理程序与逻辑和数据库层分开。

HTTP 处理程序

处理程序除了将请求值映射到局部变量或可能的自定义数据结构(如果需要)之外什么都不做。除此之外,它只是运行用例逻辑并在将结果写入响应之前映射结果。这也是将不同错误映射到不同响应对象的好地方。

type Interactor interface {
    Bar(foo string) ([]usecases.Bar, error)
}

type MyHandler struct {
    Interactor Interactor
}

func (handler MyHandler) Bar(w http.ResponseWriter, r *http.Request) {
    foo := r.FormValue("foo")
    res, _ := handler.Interactor.Bar(foo)

    // you may want to map/cast res to a different type that is encoded
    // according to your spec
    json.NewEncoder(w).Encode(res)
}

单元测试是测试 HTTP 响应是否包含针对不同结果和错误的正确数据的好方法。

用例/业务逻辑

由于存储库只是被指定为一个接口,因此很容易为业务逻辑创建单元测试,由同样符合DataRepository 的模拟存储库实现返回不同的结果。

type DataRepository interface {
    Find(f string) (Bar, error)
}

type Bar struct {
    Identifier string
    FooBar     int
}

type Interactor struct {
    DataRepository DataRepository
}

func (interactor *Interactor) Bar(f string) (Bar, error) {
    b := interactor.DataRepository.Find(f)

    // ... custom logic

    return b
}   

数据库接口

与数据库通信的部分实现了DataRepository 接口,但在其他方面完全独立于它将数据转换为预期类型的​​方式。

type Repo {
    db sql.DB
}

func NewDatabaseRepo(db sql.DB) *Repo {
    // config if necessary...

    return &Repo{db: db}
}

func (r Repo)Find(f string) (usecases.Bar, error) {
    rows, err := db.Query("SELECT id, foo_bar FROM bar WHERE foo=?", f)
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    for rows.Next() {
        var id string, fooBar int
        if err := rows.Scan(&id, &fooBar); err != nil {
            log.Fatal(err)
        }
        // map row value to desired structure
        return usecases.Bar{Identifier: id, FooBar: fooBar}
    }

    return errors.New("not found")
}

同样,这允许单独测试数据库操作而无需任何模拟 SQL 语句。

注意:上面的代码是伪代码,不完整。

【讨论】:

  • 这是我已经使用的模型,很高兴得到一些确认这是一个很好的决定 - 它在我的应用程序中确实运行良好,到目前为止,尽管有点代价发展速度。
  • 是的,但这将 API 与数据库耦合在一起。它不会“缩放”(即 API 版本,甚至重构表。)
【解决方案2】:

在使用 Go 开发自己的应用程序之前,我有 .NET MVC 经验。我确实错过了 .NET 中 BLL 和 DTO 之间的映射器,但是当我在 Go 中编写更多代码时,我已经习惯了 Go 中没有太多免费午餐的事实。

Go 中几乎没有框架、NHibernate、Entity 以及 URI 和视图之间的自动映射,这表明您可能需要做很多其他框架会处理的工作。尽管在某些人看来这可能效率低下,但这无疑是一个学习的好机会,也可以以一种效率较低的方式构建高度可定制的应用程序。当然,您必须编写自己的 SQL 来执行 CRUD 操作,读取查询返回的行,将 sql.Rows 映射到模型,应用一些业务逻辑,保存更改,将模型映射到 DTO,并将响应发送回客户端.

关于DTO和模型的区别:DTO是模型对视图的表示,没有行为(方法)。模型是你的业务逻辑的抽象,有很多复杂的行为。我永远不会在 DTO 对象上实现诸如“checkProvisioning()”或“applyCouponCode()”之类的方法,因为它们是业务逻辑。

至于使用您的模型映射数据库,我不会采取任何捷径,因为此时可用的大多数 ORM 都非常原始。如果我必须使用 ORM 构建我的数据库,我什至不会尝试使用外键。最好从 SQL 脚本构建数据库,因为您还可以配置索引、触发器和其他花里胡哨。是的,如果架构发生更改,您将不得不更新相当多的代码以反映该更改,但大多数情况下它只会影响数据访问层代码。

我有一个使用 C# 的 MVC 设计但完全用 Go 编写的项目,我敢打赌,它比您从已出版书籍中找到的任何玩具项目都要复杂。如果阅读代码可以帮助您更好地理解结构,那就去吧:https://github.com/yubing24/das

【讨论】:

    猜你喜欢
    • 2021-10-28
    • 2018-09-13
    • 1970-01-01
    • 2021-07-24
    • 2012-04-03
    • 1970-01-01
    • 2021-12-31
    • 1970-01-01
    相关资源
    最近更新 更多