buffalo 默认使用 pop 作为数据库工具,它轻量且贴近 sql,提供迁移、模型定义和 crud 封装,但不自动预加载关联、不支持嵌套事务、无字段变更追踪;模型需导出并含主键标记,字段默认 snake_case 映射;操作基于 pop.connection,事务需手动传递与回滚;null 值字段必须用 t 或 sql.nullxxx 类型。

Buffalo 默认使用 Pop 作为数据库层,它不是 ORM,而是“database toolkit”——更轻、更贴近 SQL,但提供了结构化迁移、模型定义和常见 CRUD 封装。
你不需要手动写 SQL 就能完成增删改查,但得清楚 Pop 的行为边界:它不自动处理关联预加载(需显式 Load),不支持复杂嵌套事务嵌套(需用 tx.WithContext 手动控制),也不做字段级变更追踪(Update 是全量覆盖或指定字段更新)。
定义模型并生成迁移文件
模型必须满足两个硬性条件:Pop 才能识别并生成对应表:
- 结构体必须导出(首字母大写)
- 必须包含
ID uint64 `db:"id,primarykey"`或类似主键标记(uuid类型也支持,但需额外配置) - 字段名默认映射为 snake_case 字段名(如
CreatedAt→created_at),可加db:"custom_name"覆盖
示例模型:
type User struct {
ID uint64 `db:"id,primarykey"`
Name string `db:"name"`
Email string `db:"email"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
运行命令生成迁移:
buffalo pop generate fizz create_users
编辑生成的 pop/soda/<timestamp>_create_users.up.fizz</timestamp>,补全字段定义后执行:
buffalo pop migrate
用pop.Connection执行增删改查
Pop 的操作都基于 *pop.Connection 实例(通常从 app.Pop() 获取)。所有方法默认在事务中执行,除非显式传入 nil 连接。
-
插入:用
Create,会自动填充ID、CreatedAt、UpdatedAt(如果字段存在且未设值) -
查询单条:用
Find(按主键)或Where(...).First(按条件);注意Find不支持复合主键 -
查询列表:用
All或链式Where/Order/Paginate;All返回切片,不是迭代器 -
更新:用
Update(全量)或UpdateColumns(只更新指定字段);Update会强制重写UpdatedAt -
删除:用
Destroy(按主键)或Where(...).Delete(按条件);软删除需自行实现字段逻辑
简短示例:
u := &User{Name: "Alice", Email: "a@example.com"}
err := tx.Create(u) // 插入,u.ID 被赋值
<p>var found User
err := tx.Find(&found, 1) // 按 ID 查</p><p>var users []User
err := tx.Where("email LIKE ?", "%@example.com").All(&users)</p><p>err := tx.UpdateColumns(&u, "name") // 只更新 name 字段,UpdatedAt 仍被刷新
err := tx.Destroy(&u) // 删除该记录(按 u.ID)
</p>
事务与上下文传递容易漏掉的点
Pop 的事务默认不跨函数传播。如果你在 handler 里拿到 tx := app.Pop(),然后调用一个封装了数据库操作的 service 函数,那个函数若直接调用 app.Pop(),就会新开连接,导致事务失效。
- 正确做法:把
*pop.Connection当作参数传进去,service 层不自己取连接 - 错误写法:
func CreateUser(...) error { db := app.Pop(); return db.Create(...) }—— 这会脱离当前事务 - 事务内嵌套失败不会自动回滚:必须用
defer func() { if r := recover(); r != nil { tx.Rollback() } }()或显式检查err后调用tx.Rollback() -
tx.Load加载关联数据时,要求主模型已存在且 ID 非零;否则 panic
Pop 查询性能和 NULL 处理的实际影响
Pop 对 NULL 值非常敏感:数据库字段为 NULL,但 Go 结构体字段是 string(非 *string 或 sql.NullString),查询时会报错 "cannot scan NULL into Go struct field"。
- 所有可能为 NULL 的字段,必须用
*T或sql.NullXxx类型声明(如Email *string或Age sql.NullInt64) -
Where中用IS NULL必须写成Where("email IS NULL"),不能用Where("email = ?", nil) - 大量数据分页建议用
Paginate+PerPage,避免All加len()判断总数——Paginate内部会自动发COUNT查询 - 原生 SQL 查询用
Raw,但注意它不走模型扫描逻辑,需手动Scan到结构体或 map
最常被忽略的一点:迁移文件里定义的字段类型(如 t.Column("email", "string", {"null": true}))必须和模型字段类型严格一致,否则 Pop 在 Load 或 Create 时可能静默失败或 panic。











