Golang - DTO(数据传输对象)、实体和映射

41

我之前是使用C#的,现在刚开始接触Go语言,对于如何构建Go应用程序感到困惑。

比方说,我要制作一个REST API来连接数据库。另外,考虑到业务等不确定性因素,即使完成后,这个应用程序也可能需要经常更改。

在C#中,通过使用Entity Framework和DTO等工具,可以将数据库结果与控制器返回的结果进行抽象处理,从而在一定程度上减轻了这个问题。如果我更改数据库中一堆字段的名称,可能需要更改数据库访问逻辑。但是,希望使用AutoMapper映射到实体的DTO仍然可以保持不变,这样就不会破坏依赖于给定DTO结构的前端功能。

那么,在Go中,我是否应该复制这种结构?似乎这种方法有些不对,因为structs只是DTO,并且我将拥有相当多的DTO structs,它们与实体structs相同。我还必须设置实体到DTO的映射逻辑。这一切都感觉非常不惯用,而且我看到网络上的许多示例都将数据库structs序列化。

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

如果有区别的话,我计划使用sqlx将数据库结果转换为structs,这意味着如果不将实体与DTO分开,需要更多的标记,除了JSON。


2
一个好的方法是通过一些开源项目来获得灵感,了解如何处理这些问题。尝试浏览 http://gobuffalo.io 项目。对于这个特定的情况,https://github.com/markbates/pop 可以提供帮助。 - RadekSohlich
8
我能给出的最好建议是不要试图模仿其他语言,特别是面向对象的语言。将Java或C#的概念运用到Go中会在未来带来无尽痛苦。与其试图按照你所熟悉的方式去做事情,不如只做看起来符合Go语言惯例的最简单的事情,然后再从那里迭代改进。保持KISS(保持简单)和YAGNI(你不需要它)原则,这将避免许多麻烦。寻找现有的、类似的项目以获得灵感。 - Adrian
2个回答

40

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

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

您可以将每个部分单独处理和构建,这不仅解耦了它们,而且使其更易于测试。然后通过注入所需的位来将这些部分组合在一起,因为它们符合您定义的接口。通常,这最终会将main或单独的配置机制留给唯一知道已组合和注入了什么如何的地方。

应用清洁架构到Go应用程序这篇文章非常好地说明了如何分离各个部分。您应该严格遵循这种方法的程度取决于项目的复杂性。

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

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语句的情况下单独测试数据库操作。

注意:上面的代码非常类似伪代码且不完整。


1
这是我已经采用的模型,很高兴得到一些确认它是一个好决定 - 迄今为止,在我的应用中它确实运作良好,尽管在开发速度方面付出了一定代价。 - Ben
1
是的,但这将API与数据库耦合在一起。它不会“扩展”(即API版本,甚至重构表格)。 - U Avalos

6

在开始使用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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接