别再混淆了!一文搞懂GoFrame的四层数据模型entity、do、dao、pb

2025年10月21日 · 2001 字 · 4 分钟 · GoFrame ORM Go 数据库 架构设计 后端开发

写在前面

GoFrame四层数据模型

在使用 GoFrame(特别是 v2.3.3 及之后版本)进行项目开发时,你可能会注意到 ORM 相关代码中总是出现几组文件:dao/entity/do/pb/

这些文件大多是由命令自动生成的,但初学者常常搞不清它们的关系与用途。我刚接触 GoFrame 时也有同样的困惑:为什么要分这么多层?它们之间是什么关系?

今天就来系统地梳理一下它们的职责边界、典型使用方式,以及最佳实践。


一、GoFrame ORM 架构总览

GoFrame ORM 架构总览

GoFrame 的 ORM 模型主要由四个层次构成:

模块 主要作用 典型命名 是否自动生成
entity 数据表字段的结构体映射 entity/user.go
do (Data Object) 数据操作对象(主要用于写入) do/user.go
dao (Data Access Object) 数据访问层(封装 CRUD 逻辑) dao/user.go
pb (protobuf) 用于 RPC 或 API 层的数据结构 pb/user.protopb/user.pb.go ⚙️(gRPC 项目中生成)

这种分层设计乍一看有些复杂,但理解之后你会发现它的优雅之处。


二、entity:数据库表的镜子

entity:数据库表的镜子

位置: /internal/model/entity/

生成命令:

gf gen dao

entity 是数据库表的结构体映射,一行记录对应一个结构体实例。它主要用于查询返回结果的承载,以及提供字段名自动补全(避免写错表字段)。

字段标签包含了 ORM 映射信息,例如:

type User struct {
    Id        uint64      `json:"id"        orm:"id,primary"   description:"用户ID"`
    Name      string      `json:"name"      orm:"name"         description:"用户名"`
    CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"`
}

关键点:

  • 不建议直接修改(下次自动生成会被覆盖)
  • 主要用于读取操作
  • 字段类型与数据库表字段类型严格对应

三、do:写入操作的专用容器

位置: /internal/model/do/

do 全称 Data Object,是专门用于**写入操作(Insert/Update)**的结构体。

entity 不同,do 的字段类型大多为 interface{},因为在写入时可能有条件更新、批量更新等不定值场景。

示例:

type User struct {
    Id    interface{} `orm:"id"    description:"用户ID"`
    Name  interface{} `orm:"name"  description:"用户名"`
    Age   interface{} `orm:"age"   description:"年龄"`
}

使用时:

dao.User.Ctx(ctx).Data(do.User{
    Name: "Tom",
    Age:  25,
}).Insert()

这种设计巧妙地解决了字段空值和部分更新的问题。比如你只想更新用户的名字,不需要传入所有字段,用 interface{} 类型就能灵活处理。

关键点:

  • 用于 Insert / Update 等写操作
  • 字段类型为 interface{},支持灵活赋值
  • 解决了空值和部分更新的问题

四、dao:数据访问的统一入口

位置: /internal/dao/

dao(Data Access Object)是操作数据库的统一入口,封装了底层的表名、上下文和基础操作方法。

示例:

var User = userDao{
    group: "default",
    table: "user",
}

type userDao struct {
    group string
    table string
}

func (d *userDao) Ctx(ctx context.Context) *gdb.Model {
    return g.DB(d.group).Model(d.table).Safe()
}

实际使用:

// 查询
var user *entity.User
dao.User.Ctx(ctx).Where("id", 1).Scan(&user)

// 更新
dao.User.Ctx(ctx).Data(do.User{Name: "Jerry"}).Where("id", 1).Update()

所有的数据库操作都通过 dao.Table.Ctx(ctx) 开始,这样做的好处是屏蔽了底层数据库表名、连接组等信息,让代码更加清晰。

关键点:

  • 通常不直接修改
  • 所有操作通过 dao.Table.Ctx(ctx) 开始
  • 用于屏蔽底层数据库细节

五、pb:服务层的数据结构(可选)

位置: /api/pb//api/proto/

当项目使用 gRPC、OpenAPI 等接口规范时,GoFrame 推荐将接口层的请求与响应结构体通过 protobuf 定义,自动生成到 pb 目录。

例如:

message User {
    uint64 id = 1;
    string name = 2;
}

生成后在 Go 中会有对应结构体:

type User struct {
    Id   uint64
    Name string
}

这与 entity 相似,但属于接口层结构体,避免直接暴露数据库字段。这是一种很好的实践:API 层和数据库层解耦,即使数据库表结构变化,也不会直接影响到外部接口。


六、四者关系:从数据库到 API 的完整链路

+-------------+          +------------+         +-----------+         +------------+
|   pb 层     |  <———→  |   service  |  <———→ |   dao 层  |  <———→  | entity/do  |
| (接口模型)  |          | (业务逻辑) |         | (数据库操作) |      | (表映射结构) |
+-------------+          +------------+         +-----------+         +------------+
  • pb:面向外部 API 层,定义接口契约
  • service:业务逻辑封装,处理模型转换
  • dao/entity/do:数据访问和存储层,与数据库交互

这种分层让每一层都有明确的职责,代码维护起来也更加清晰。


七、推荐目录结构

internal/
├── dao/
│   └── user.go
├── model/
│   ├── entity/
│   │   └── user.go
│   └── do/
│       └── user.go
├── service/
│   └── user.go
api/
└── pb/
    └── user.proto

这是 GoFrame 官方推荐的标准目录结构,遵循这个结构可以让项目更加规范。


八、最佳实践建议

1. 只在 dao 层访问数据库

保持 service 层业务逻辑的纯净,所有数据库操作都通过 dao 进行。

2. 不要手动修改自动生成的文件

entitydodao 都是自动生成的,手动修改后下次生成会被覆盖。改表结构后重新执行:

gf gen dao

3. 在 service 层做参数转换

pb.Userentity.Userdo.User 由 service 层处理,保持层次清晰。

4. 保持 pb 与 entity 的解耦

避免直接把数据库结构暴露给外部接口,这样可以更灵活地调整数据库结构而不影响 API。


写在最后

GoFrame 的 ORM 分层设计,其实是在践行"读写分离 + 模型分层“的工程理念。

  • entity 是数据库的镜子
  • do 是写入的容器
  • dao 是访问的桥梁
  • pb 是服务的外壳

理解了这四者的关系,你就理解了 GoFrame 的设计哲学。这种分层看似复杂,但在大型项目中能带来极大的便利性和可维护性。

如果你也在使用 GoFrame,不妨按照这个思路重新审视一下你的项目结构,也许会有新的收获。


🧠 延伸阅读