(本文内容基于 Bun ORM 官方文档 翻译整理)
1. 简介:什么是 Bun ORM?
Bun 是一个 SQL 优先的 Go 语言 ORM(对象关系映射)框架,支持 PostgreSQL、MySQL、MSSQL 和 SQLite。它旨在提供一种简单高效的方式来操作数据库,同时利用 Go 的类型安全特性并减少样板代码。
核心特性
- 基于标准库构建:构建在 Go 标准
database/sql包之上 - 类型安全:提供类型安全的查询构建器,性能卓越
- 复杂关系支持:支持复杂的关系和连接操作
- 迁移支持:提供迁移和架构管理功能
- 强大的扫描能力:全面的数据扫描功能
- 钩子和中间件:支持钩子和中间件
- 生产就绪:经过广泛测试,可用于生产环境
为什么选择 Bun?
Bun 通过 SQL 优先 的理念区别于其他 Go ORM,不试图对开发者隐藏 SQL。这种方法具有以下优势:
- 可预测的查询:你确切知道生成的 SQL 是什么
- 高性能:对原始 SQL 的开销最小
- 渐进式采用:易于集成到现有代码库
- 灵活性:需要时可降级到原始 SQL
- 类型安全:大多数操作的编译时检查
2. 安装与配置
安装 Bun
要安装 Bun 和所需的数据库驱动:
# 核心 Bun 包
go get github.com/uptrace/bun@latest
# 数据库驱动(选择一个或多个)
go get github.com/uptrace/bun/driver/pgdriver # PostgreSQL
go get github.com/uptrace/bun/driver/sqliteshim # SQLite
go get github.com/go-sql-driver/mysql # MySQL
go get github.com/denisenkom/go-mssqldb # SQL Server
快速开始示例
package main
import (
"context"
"database/sql"
"fmt"
"log"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/sqlitedialect"
"github.com/uptrace/bun/driver/sqliteshim"
"github.com/uptrace/bun/extra/bundebug"
)
// User 模型
type User struct {
bun.BaseModel `bun:"table:users,alias:u"`
ID int64 `bun:",pk,autoincrement"`
Name string `bun:",notnull"`
Email string `bun:",unique"`
}
func main() {
ctx := context.Background()
// 打开数据库连接
sqldb, err := sql.Open(sqliteshim.ShimName, "file::memory:?cache=shared")
if err != nil {
panic(err)
}
defer sqldb.Close()
// 创建 Bun 数据库实例
db := bun.NewDB(sqldb, sqlitedialect.New())
// 添加查询调试(可选)
db.AddQueryHook(bundebug.NewQueryHook(
bundebug.WithVerbose(true),
))
// 创建表
_, err = db.NewCreateTable().Model((*User)(nil)).IfNotExists().Exec(ctx)
if err != nil {
panic(err)
}
// 插入用户
user := &User{Name: "张三", Email: "zhangsan@example.com"}
_, err = db.NewInsert().Model(user).Exec(ctx)
if err != nil {
panic(err)
}
// 查询用户
var selectedUser User
err = db.NewSelect().Model(&selectedUser).Where("email = ?", "zhangsan@example.com").Scan(ctx)
if err != nil {
panic(err)
}
fmt.Printf("用户: %+v\n", selectedUser)
}
3. 数据库连接配置
PostgreSQL 连接
import (
"database/sql"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
"github.com/uptrace/bun/driver/pgdriver"
)
// 使用 pgdriver(推荐)
sqldb := sql.OpenDB(pgdriver.NewConnector(
pgdriver.WithDSN("postgres://user:password@localhost:5432/dbname?sslmode=disable"),
))
db := bun.NewDB(sqldb, pgdialect.New())
// 或者使用 lib/pq
import _ "github.com/lib/pq"
sqldb, err := sql.Open("postgres", "postgres://user:password@localhost/dbname?sslmode=disable")
if err != nil {
log.Fatal(err)
}
db := bun.NewDB(sqldb, pgdialect.New())
MySQL 连接
import (
"database/sql"
"github.com/uptrace/bun/dialect/mysqldialect"
_ "github.com/go-sql-driver/mysql"
)
sqldb, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname?parseTime=true")
if err != nil {
panic(err)
}
db := bun.NewDB(sqldb, mysqldialect.New())
SQLite 连接
import (
"database/sql"
"github.com/uptrace/bun/dialect/sqlitedialect"
"github.com/uptrace/bun/driver/sqliteshim"
)
sqldb, err := sql.Open(sqliteshim.ShimName, "file:test.db?cache=shared&mode=rwc")
if err != nil {
panic(err)
}
db := bun.NewDB(sqldb, sqlitedialect.New())
连接池配置
为了获得最佳性能,配置数据库连接池:
// 配置连接池
sqldb.SetMaxOpenConns(25) // 最大打开连接数
sqldb.SetMaxIdleConns(10) // 最大空闲连接数
sqldb.SetConnMaxLifetime(5 * time.Minute) // 连接生命周期
sqldb.SetConnMaxIdleTime(5 * time.Minute) // 空闲连接超时
// 测试连接
if err := sqldb.Ping(); err != nil {
log.Fatal("连接数据库失败:", err)
}
4. 模型定义与结构体标签
基本模型结构
Bun 使用基于结构体的模型来构建查询和扫描结果。模型使用 Go 结构体和结构体标签定义数据库架构:
type User struct {
bun.BaseModel `bun:"table:users,alias:u"`
ID int64 `bun:",pk,autoincrement"`
Name string `bun:",notnull"`
Email string `bun:",unique,notnull"`
CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"`
}
常用结构体标签
| 标签 | 描述 | 示例 |
|---|---|---|
pk |
主键 | bun:",pk" |
autoincrement |
自增字段 | bun:",pk,autoincrement" |
notnull |
NOT NULL 约束 | bun:",notnull" |
unique |
UNIQUE 约束 | bun:",unique" |
default:value |
默认值 | bun:",default:0" |
type:varchar(100) |
自定义列类型 | bun:",type:varchar(100)" |
nullzero |
将零值视为 NULL | bun:",nullzero" |
- |
忽略字段 | bun:"-" |
高级模型示例
// 包含 JSON 字段和自定义类型的用户
type User struct {
bun.BaseModel `bun:"table:users"`
ID int64 `bun:",pk,autoincrement"`
Name string `bun:",notnull"`
Email string `bun:",unique,notnull"`
Settings map[string]interface{} `bun:",type:jsonb"` // PostgreSQL JSONB
Status UserStatus `bun:",type:varchar(20),default:'active'"`
CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"`
DeletedAt time.Time `bun:",soft_delete,nullzero"` // 软删除支持
}
type UserStatus string
const (
UserStatusActive UserStatus = "active"
UserStatusInactive UserStatus = "inactive"
UserStatusBanned UserStatus = "banned"
)
// 带外键的用户资料
type Profile struct {
bun.BaseModel `bun:"table:profiles"`
ID int64 `bun:",pk,autoincrement"`
UserID int64 `bun:",notnull"`
Bio string
Avatar string
// 关系定义
User *User `bun:"rel:belongs-to,join:user_id=id"`
}
5. CRUD 操作
插入操作
// 插入单个用户
user := &User{Name: "李四", Email: "lisi@example.com"}
_, err := db.NewInsert().Model(user).Exec(ctx)
// user.ID 现在已被填充
// 批量插入用户
users := []*User{
{Name: "王五", Email: "wangwu@example.com"},
{Name: "赵六", Email: "zhaoliu@example.com"},
}
_, err := db.NewInsert().Model(&users).Exec(ctx)
// 处理冲突的插入
_, err = db.NewInsert().
Model(user).
On("CONFLICT (email) DO UPDATE").
Set("name = EXCLUDED.name").
Exec(ctx)
// 插入并返回特定列
var ids []int64
_, err = db.NewInsert().
Model(&users).
Returning("id").
Exec(ctx, &ids)
更新操作
// 根据主键更新
user := &User{ID: 1, Name: "更新的名字"}
_, err := db.NewUpdate().
Model(user).
Column("name").
WherePK().
Exec(ctx)
// 使用 WHERE 子句更新
_, err = db.NewUpdate().
Model((*User)(nil)).
Set("last_login = ?", time.Now()).
Where("status = ?", "active").
Exec(ctx)
// 使用子查询更新
_, err = db.NewUpdate().
Model((*User)(nil)).
Set("post_count = (SELECT COUNT(*) FROM posts WHERE user_id = users.id)").
Exec(ctx)
// 批量更新(使用 CASE)
_, err = db.NewUpdate().
Model((*User)(nil)).
Set("status = CASE WHEN last_login < ? THEN 'inactive' ELSE 'active' END",
time.Now().AddDate(0, -3, 0)).
Exec(ctx)
删除操作
// 根据主键删除
user := &User{ID: 1}
_, err := db.NewDelete().
Model(user).
WherePK().
Exec(ctx)
// 使用 WHERE 子句删除
_, err = db.NewDelete().
Model((*User)(nil)).
Where("created_at < ?", time.Now().AddDate(-1, 0, 0)).
Exec(ctx)
// 软删除(需要 soft_delete 标签)
_, err = db.NewDelete().
Model(user).
WherePK().
Exec(ctx) // 设置 deleted_at 时间戳
// 强制删除(绕过软删除)
_, err = db.NewDelete().
Model(user).
WherePK().
ForceDelete().
Exec(ctx)
查询操作
// 根据主键查询
user := new(User)
err := db.NewSelect().
Model(user).
Where("id = ?", 1).
Scan(ctx)
// 查询多个用户
var users []User
err := db.NewSelect().
Model(&users).
Where("status = ?", "active").
Order("created_at DESC").
Limit(10).
Scan(ctx)
// 复杂条件查询
err = db.NewSelect().
Model(&users).
Where("name ILIKE ?", "%张%").
WhereOr("email ILIKE ?", "%admin%").
WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Where("created_at > ?", time.Now().AddDate(0, -1, 0)).
Where("status != ?", "banned")
}).
Scan(ctx)
// 查询特定列
var names []string
err = db.NewSelect().
Model((*User)(nil)).
Column("name").
Where("status = ?", "active").
Scan(ctx, &names)
// 计数
count, err := db.NewSelect().
Model((*User)(nil)).
Where("status = ?", "active").
Count(ctx)
6. 高级查询与关系
Belongs-To 关系
type Post struct {
bun.BaseModel `bun:"table:posts"`
ID int64 `bun:",pk,autoincrement"`
Title string `bun:",notnull"`
Content string
AuthorID int64 `bun:",notnull"`
// Belongs-to 关系
Author *User `bun:"rel:belongs-to,join:author_id=id"`
}
// 查询时包含关系
var posts []Post
err := db.NewSelect().
Model(&posts).
Relation("Author").
Where("post.status = ?", "published").
Scan(ctx)
Has-One 关系
type User struct {
bun.BaseModel `bun:"table:users"`
ID int64 `bun:",pk,autoincrement"`
Name string `bun:",notnull"`
// Has-one 关系
Profile *Profile `bun:"rel:has-one,join:id=user_id"`
}
// 查询包含 has-one
var users []User
err := db.NewSelect().
Model(&users).
Relation("Profile").
Scan(ctx)
Has-Many 关系
type User struct {
bun.BaseModel `bun:"table:users"`
ID int64 `bun:",pk,autoincrement"`
Name string
// Has-many 关系
Posts []Post `bun:"rel:has-many,join:id=author_id"`
}
// 查询包含 has-many
var users []User
err := db.NewSelect().
Model(&users).
Relation("Posts", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Where("status = ?", "published").Order("created_at DESC")
}).
Scan(ctx)
Many-to-Many 关系
type User struct {
bun.BaseModel `bun:"table:users"`
ID int64 `bun:",pk,autoincrement"`
Name string
// Many-to-many 关系
Roles []Role `bun:"m2m:user_roles,join:User=Role"`
}
type Role struct {
bun.BaseModel `bun:"table:roles"`
ID int64 `bun:",pk,autoincrement"`
Name string `bun:",unique,notnull"`
}
type UserRole struct {
bun.BaseModel `bun:"table:user_roles"`
UserID int64 `bun:",pk"`
RoleID int64 `bun:",pk"`
User *User `bun:"rel:belongs-to,join:user_id=id"`
Role *Role `bun:"rel:belongs-to,join:role_id=id"`
}
// 查询 many-to-many
var users []User
err := db.NewSelect().
Model(&users).
Relation("Roles").
Scan(ctx)
子查询
// WHERE 中的子查询
subq := db.NewSelect().
Model((*Post)(nil)).
Column("author_id").
Where("status = ?", "published").
Group("author_id").
Having("COUNT(*) > ?", 5)
var users []User
err := db.NewSelect().
Model(&users).
Where("id IN (?)", subq).
Scan(ctx)
// SELECT 中的子查询
err = db.NewSelect().
Model(&users).
ColumnExpr("(SELECT COUNT(*) FROM posts WHERE author_id = users.id) as post_count").
Scan(ctx)
窗口函数
// 带分区的行号
var results []struct {
User `bun:",embed"`
RowNum int `bun:"row_num"`
PostRank int `bun:"post_rank"`
}
err := db.NewSelect().
Model(&results).
ColumnExpr("*, ROW_NUMBER() OVER (PARTITION BY status ORDER BY created_at) as row_num").
ColumnExpr("RANK() OVER (ORDER BY post_count DESC) as post_rank").
Scan(ctx)
公共表表达式(CTE)
// 递归 CTE
cte := db.NewSelect().
With("RECURSIVE user_hierarchy", db.NewSelect().
ColumnExpr("id, name, manager_id, 0 as level").
Model((*User)(nil)).
Where("manager_id IS NULL").
UnionAll(
db.NewSelect().
ColumnExpr("u.id, u.name, u.manager_id, uh.level + 1").
TableExpr("users u").
Join("JOIN user_hierarchy uh ON u.manager_id = uh.id"),
),
).
Table("user_hierarchy").
Column("*")
var hierarchy []struct {
ID int64 `bun:"id"`
Name string `bun:"name"`
ManagerID *int64 `bun:"manager_id"`
Level int `bun:"level"`
}
err := cte.Scan(ctx, &hierarchy)
7. 错误处理与调试
常见错误模式
import (
"database/sql"
"errors"
)
// 检查无行错误
user := new(User)
err := db.NewSelect().Model(user).Where("id = ?", 999).Scan(ctx)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// 处理未找到
return fmt.Errorf("用户未找到")
}
return err
}
// 检查唯一约束冲突
_, err = db.NewInsert().Model(user).Exec(ctx)
if err != nil {
if strings.Contains(err.Error(), "duplicate key") ||
strings.Contains(err.Error(), "UNIQUE constraint") {
return fmt.Errorf("用户已存在")
}
return err
}
查询调试
import "github.com/uptrace/bun/extra/bundebug"
// 添加调试钩子
db.AddQueryHook(bundebug.NewQueryHook(
bundebug.WithVerbose(true),
bundebug.FromEnv("BUNDEBUG"), // 使用 BUNDEBUG=1 启用
))
// 或创建自定义调试钩子
type DebugHook struct{}
func (h *DebugHook) BeforeQuery(ctx context.Context, event *bun.QueryEvent) context.Context {
return ctx
}
func (h *DebugHook) AfterQuery(ctx context.Context, event *bun.QueryEvent) {
fmt.Printf("查询: %s\n持续时间: %s\n", event.Query, event.Dur)
}
db.AddQueryHook(&DebugHook{})
8. 性能优化技巧
查询优化
// 有效使用索引
_, err := db.NewCreateIndex().
Model((*User)(nil)).
Index("idx_users_email_status").
Column("email", "status").
Exec(ctx)
// 适当使用 LIMIT
var users []User
err := db.NewSelect().
Model(&users).
Where("status = ?", "active").
Order("created_at DESC").
Limit(100). // 总是限制大查询
Scan(ctx)
// 使用特定列而不是 *
var userSummaries []struct {
ID int64 `bun:"id"`
Name string `bun:"name"`
}
err = db.NewSelect().
Model((*User)(nil)).
Column("id", "name"). // 只选择需要的列
Scan(ctx, &userSummaries)
批量操作
// 批量插入(使用批次大小)
const batchSize = 1000
users := make([]*User, 10000) // 大切片
for i := 0; i < len(users); i += batchSize {
end := i + batchSize
if end > len(users) {
end = len(users)
}
batch := users[i:end]
_, err := db.NewInsert().Model(&batch).Exec(ctx)
if err != nil {
return err
}
}
事务处理
// 简单事务
err := db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error {
user := &User{Name: "张三"}
if _, err := tx.NewInsert().Model(user).Exec(ctx); err != nil {
return err
}
profile := &Profile{UserID: user.ID, Bio: "你好"}
if _, err := tx.NewInsert().Model(profile).Exec(ctx); err != nil {
return err
}
return nil // 提交
})
// 手动事务控制
tx, err := db.BeginTx(ctx, &sql.TxOptions{})
if err != nil {
return err
}
defer tx.Rollback()
// 使用 tx 代替 db 进行操作
_, err = tx.NewInsert().Model(user).Exec(ctx)
if err != nil {
return err
}
return tx.Commit()
9. 测试策略
单元测试
import (
"testing"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dbfixture"
)
func TestUserOperations(t *testing.T) {
// 设置测试数据库
db := setupTestDB(t)
// 加载测试数据
fixture := dbfixture.New(db)
if err := fixture.Load(ctx, "testdata/users.yml"); err != nil {
t.Fatal(err)
}
// 测试操作
var count int
count, err := db.NewSelect().Model((*User)(nil)).Count(ctx)
if err != nil {
t.Fatal(err)
}
if count != 3 {
t.Errorf("期望 3 个用户,得到 %d", count)
}
}
测试数据(testdata/users.yml)
model: User
rows:
- id: 1
name: 张三
email: zhangsan@example.com
- id: 2
name: 李四
email: lisi@example.com
- id: 3
name: 王五
email: wangwu@example.com
集成测试
func TestUserRepository(t *testing.T) {
// 使用内存 SQLite 进行集成测试
sqldb, err := sql.Open(sqliteshim.ShimName, ":memory:")
if err != nil {
t.Fatal(err)
}
defer sqldb.Close()
db := bun.NewDB(sqldb, sqlitedialect.New())
// 创建表
_, err = db.NewCreateTable().Model((*User)(nil)).Exec(ctx)
if err != nil {
t.Fatal(err)
}
repo := NewUserRepository(db)
// 测试创建
user := &User{Name: "测试用户", Email: "test@example.com"}
err = repo.Create(ctx, user)
if err != nil {
t.Fatal(err)
}
// 测试查询
found, err := repo.GetByID(ctx, user.ID)
if err != nil {
t.Fatal(err)
}
if found.Name != user.Name {
t.Errorf("期望名称 %s,得到 %s", user.Name, found.Name)
}
}
10. 最佳实践与常见陷阱
✅ 推荐做法
- 使用参数化查询:始终使用占位符(
?)防止 SQL 注入 - 使用事务:对必须一起成功或失败的操作使用事务
- 添加适当索引:为频繁查询的列添加索引
- 限制结果集:对可能返回大结果集的查询使用
LIMIT - 验证输入:在查询前验证和清理输入
- 使用连接池:在生产环境中使用连接池
// ✅ 好的示例
err := db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
// 使用事务确保数据一致性
user := &User{Name: validatedName, Email: validatedEmail}
if _, err := tx.NewInsert().Model(user).Exec(ctx); err != nil {
return err
}
profile := &Profile{UserID: user.ID, Bio: validatedBio}
if _, err := tx.NewInsert().Model(profile).Exec(ctx); err != nil {
return err
}
return nil
})
❌ 避免的做法
- 忽略错误:不要忽略数据库操作的错误
- 字符串拼接:不要使用字符串拼接构建查询
- 忘记关闭:不要忘记关闭数据库连接和事务
- **SELECT ***:只需要特定列时不要使用
SELECT * - 循环中的数据库操作:不要在没有批处理的循环中执行数据库操作
// ❌ 错误示例
for _, user := range users {
// 低效:在循环中逐个插入
_, err := db.NewInsert().Model(&user).Exec(ctx)
if err != nil {
log.Println(err) // 错误被忽略了
}
}
// ✅ 正确示例
// 高效:批量插入
_, err := db.NewInsert().Model(&users).Exec(ctx)
if err != nil {
return fmt.Errorf("批量插入用户失败: %w", err)
}
常见问题解答
Q: 如何处理 NULL 值?
A: 使用指针类型或 sql.Null* 类型:
type User struct {
ID int64 `bun:",pk,autoincrement"`
Name string `bun:",notnull"`
Email *string // 可为 NULL 的字符串
Age sql.NullInt64 // 替代方法
}
Q: Bun 与原始 SQL 相比的性能差异是什么?
A: Bun 对原始 SQL 的开销很小。在大多数情况下,性能差异可以忽略不计(< 5%),同时提供了类型安全和开发效率方面的显著优势。
Q: 如何处理复杂的 WHERE 条件?
A: 使用 WhereGroup 处理复杂逻辑:
err := db.NewSelect().
Model(&users).
Where("status = ?", "active").
WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.WhereOr("name LIKE ?", "%admin%").
WhereOr("email LIKE ?", "%admin%")
}).
Scan(ctx)
Q: 如何处理数据库迁移?
A: Bun 通过 bun/migrate 包提供迁移支持:
import "github.com/uptrace/bun/migrate"
migrations := migrate.NewMigrations()
migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
// 迁移 up
_, err := db.NewCreateTable().Model((*User)(nil)).Exec(ctx)
return err
}, func(ctx context.Context, db *bun.DB) error {
// 迁移 down
_, err := db.NewDropTable().Model((*User)(nil)).Exec(ctx)
return err
})
migrator := migrate.NewMigrator(db, migrations)
if err := migrator.Init(ctx); err != nil {
return err
}
if err := migrator.Migrate(ctx); err != nil {
return err
}
总结
Bun ORM 是一个强大而高效的 Go 语言数据库操作库,它通过 SQL 优先的设计理念,在提供类型安全和开发便利性的同时,保持了与原生 SQL 相当的性能表现。
核心优势
- 高性能:最小化开销,接近原生 SQL 性能
- 类型安全:编译时检查,减少运行时错误
- 灵活性强:可以随时降级到原始 SQL
- 渐进式采用:易于集成到现有项目
- 功能完整:支持复杂查询、关系、迁移等
适用场景
- 需要高性能数据库操作的应用
- 复杂的企业级应用
- 需要类型安全的数据库操作
- 微服务架构中的数据访问层
- 现有
database/sql项目的升级
通过本文的全面介绍,你应该已经掌握了 Bun ORM 的核心概念和实际应用方法。建议在实际项目中逐步应用这些技术,并根据具体需求进行相应的优化和调整。
延伸阅读
希望这篇指南能帮助你更好地理解和使用 Bun ORM!