别再混淆了!一文搞懂GoFrame的四层数据模型entity、do、dao、pb
2025年10月21日 · 2001 字 · 4 分钟 · GoFrame ORM Go 数据库 架构设计 后端开发
写在前面

在使用 GoFrame(特别是 v2.3.3 及之后版本)进行项目开发时,你可能会注意到 ORM 相关代码中总是出现几组文件:dao/、entity/、do/、pb/。
这些文件大多是由命令自动生成的,但初学者常常搞不清它们的关系与用途。我刚接触 GoFrame 时也有同样的困惑:为什么要分这么多层?它们之间是什么关系?
今天就来系统地梳理一下它们的职责边界、典型使用方式,以及最佳实践。
一、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.proto、pb/user.pb.go |
⚙️(gRPC 项目中生成) |
这种分层设计乍一看有些复杂,但理解之后你会发现它的优雅之处。
二、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. 不要手动修改自动生成的文件
entity、do、dao 都是自动生成的,手动修改后下次生成会被覆盖。改表结构后重新执行:
gf gen dao
3. 在 service 层做参数转换
将 pb.User → entity.User 或 do.User 由 service 层处理,保持层次清晰。
4. 保持 pb 与 entity 的解耦
避免直接把数据库结构暴露给外部接口,这样可以更灵活地调整数据库结构而不影响 API。
写在最后
GoFrame 的 ORM 分层设计,其实是在践行"读写分离 + 模型分层“的工程理念。
entity是数据库的镜子do是写入的容器dao是访问的桥梁pb是服务的外壳
理解了这四者的关系,你就理解了 GoFrame 的设计哲学。这种分层看似复杂,但在大型项目中能带来极大的便利性和可维护性。
如果你也在使用 GoFrame,不妨按照这个思路重新审视一下你的项目结构,也许会有新的收获。