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

huangapple go评论92阅读模式
英文:

Golang - DTOs, Entities and Mapping

问题

我是你的中文翻译助手,以下是你要翻译的内容:

我之前是从C#背景转到Go的,对于如何构建Go应用程序感到困惑。

假设我正在构建一个基于数据库的REST API。而且,假设由于业务等原因,即使完成后,这个应用程序可能需要经常进行一些更改。

在C#中,通过使用Entity Framework和DTO(数据传输对象)等工具,我可以通过将数据库与控制器返回的结果进行抽象来在一定程度上缓解这个问题。如果我更改了数据库中一系列字段的名称,可能需要更改数据库访问逻辑。但是,希望使用AutoMapper将DTO映射到实体时,DTO保持不变,这样就不会破坏依赖于特定DTO结构的前端功能。

在Go中,我应该复制这种结构吗?这种方法似乎有些不对,因为结构体只是DTO,我将有很多与实体结构体相同的DTO结构体。我还需要设置逻辑来将实体映射到DTO。总的来说,这种方式似乎不太符合惯例,我在网上看到的许多示例都只是将数据库结构体序列化。

简而言之,人们如何避免API与数据库之间的过度耦合,并且他们如何广泛地将应用程序的不同部分分离开来?

如果有所区别的话,我计划使用sqlx将数据库结果转换为结构体,这意味着除了JSON标签之外,还会有更多的标签,如果我不将实体与DTO分开的话。

英文:

I am new to Go having come from a C# background, and I am just plain confused about structure a Go application.

Say I am building a REST API that will sit on top of a database. Also, say that, even after it is complete, this application could need to change somewhat frequently given the vicissitudes of the business, etc.

In C#, with tools like Entity Framework and DTOs, I somewhat alleviate this problem by abstracting the database from the results given by the controllers. If I change the name of a bunch of fields in the database, I might have to change my database access logic. Still, hopefully, the DTOs that I map to my entities using AutoMapper can remain the same, so I don't break frontend functionality that relies on a given DTO structure.

Should I replicate this structure with Go's structs? Something about such an approach seems wrong given that structs are just DTOs, and I will have quite a few DTO structs that are identical to the entity structs. I also have to setup logic to map entities to DTOs. This all just feels very unidiomatic somehow, and many of the examples I see on the web just serialize the database structs.

In short, how do people avoid excessive coupling between their API and the database in Go, and how would they broadly go about separating out the different parts of the application?

If it makes any difference, I am planning to use sqlx to marshal database results into structs which will mean more tags in addition to the JSON ones if I don't separate entities from DTOs.

答案1

得分: 39

在REST API的情况下,通常会涉及至少三个不同的实现层:

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

您可以将每个部分单独处理和构建,这不仅可以解耦,还可以更容易进行测试。然后,通过注入您定义的必要部分将它们组合在一起。通常,这样做会使得main函数或单独的配置机制成为唯一知道如何组合和注入的地方。

文章将Clean Architecture应用于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)

    // 可以将res映射/转换为根据规范编码的不同类型
    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)

    // ... 自定义逻辑

    return b
}

数据库接口

与数据库交互的部分实现了DataRepository接口,但在将数据转换为预期类型方面完全独立。

type Repo {
    db sql.DB
}

func NewDatabaseRepo(db sql.DB) *Repo {
    // 如果需要,进行配置...

    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)
        }
        // 将行值映射到所需的结构
        return usecases.Bar{Identifier: id, FooBar: fooBar}
    }

    return errors.New("not found")
}

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

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

英文:

In the case of a REST API, you'll typically deal with at least three different implementation layers:

  • HTTP handler
  • some sort of business logic/use case
  • persistent storage/database interface

You can treat and build each of these separately which does not only decouple it but makes it just a lot more testable, too. These parts then are put together by injecting the necessary bits since they conform to interfaces you define. Usually this ends up leaving the main or a separate configuration mechanism the only place that's aware of what is combined and injected how.

The article Applying The Clean Architecture to Go applications illustrates very well how the various parts can be separated. How strictly you should follow this approach depends a little on the complexity of your project.

Below is a very basic breakdown, separating the handler from logic and database layer.

HTTP handler

The handler does nothing else than mapping the request values into local variables or possibly custom data structures if needed. In addition to that it just runs the use case logic and maps the result before writing it to the response. This is also a good place to map different errors to different response objects.

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)
}

Unit tests are a great way to test that the HTTP response contains the correct data for different results and errors.

Use case / business logic

Since the repository is just specified as an interface it's very easy to create unit tests for the business logic with different results returned by a mock repository implementation that also conforms to 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
}	

Database interface

The part talking to the database implements the DataRepository interface but is otherwise totally independent on how it turns the data into the expected types.

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

Again, this allows testing the database operations separately without the need of any mock SQL statements.

> Note: The code above is very much pseudo code and incomplete.

答案2

得分: 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

英文:

I had .NET MVC experience prior to developing my own application in Go. I did miss that the mapper between BLL and DTO in .NET, but as I write more code in Go, I am getting used to the fact that there is not much free lunch in Go.

The fact that there is almost no framework, NHibernate, Entity, and automatic mapping between URI and views in Go indicates that you probably have to do a lot of work that is taken care of by other frameworks. While this may be inefficient in some people's opinion, it is certainly a great opportunity for learning as well as building a highly customizable application in a less-inefficient manner. Sure, you have to write your own SQL to perform CRUD actions, read the rows returned by the query, map sql.Rows to model, apply some business logic, save changes, map models to DTOs, and send a response back to the client.

As for the difference between DTO and model: DTO is a representation of model for view and has no behaviors (methods). Model is the abstraction of your business logic and has a lot of complex behaviours. I would never implement methods like "checkProvisioning()" or "applyCouponCode()" on a DTO object because they are business logic.

As for mapping database with your model, I won't take any shortcuts because most of the ORM available at this point is pretty primitive. I won't even try to use foreign keys if I have to build my database with ORM. It is better to build your database from a SQL script since you can also configure indexes, triggers, and other bells and whistles. Yes, if the schema changes, you will have to update quite a bit of code to reflect that change, but most of the time it only affects data access layer code.

I have a project that utilizes C#'s MVC design but is completely written in Go, and I bet it's more complex than any toy projects that you can find from published books. If reading the code help you understand the structure better, then go for it: https://github.com/yubing24/das

huangapple
  • 本文由 发表于 2017年5月3日 07:24:45
  • 转载请务必保留本文链接:https://go.coder-hub.com/43748751.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定