基于 beedb 开发 Go 轻量级 ORM:database/sql 之上的链式 CRUD 实战

原创2026-10-02 18:45:071,948 阅读
文章标签:文档教程后端

基于 beedb 开发 Go 轻量级 ORM:database/sql 之上的链式 CRUD 实战

本篇技术指南以《build-web-application-with-golang》第 5.5 节为核心,系统讲解作者 astaxie 开源的轻量级 Go ORM 框架 beedb:从 go get 安装、beedb.New() 初始化、结构体到数据表的映射规则,到 Save/Insert/Update/Find/Delete 等完整 CRUD API,以及 SetTable 链式调用、Join 关联查询、GroupBy/Having 等高级用法。读完本文,你将掌握如何用一个纯 Go 风格的轻量 ORM 替代手写 SQL,并理解其基于 database/sql 标准接口的设计原理。

beedb 是什么:一个以教学为目的的轻量级 Go ORM

beedb 是本书作者用 Go 开发的 ORM(对象关系映射)库。它采用 Go 惯用(idiomatic)的编程风格操作数据库,实现了"结构体(struct)到数据表记录"的自动映射,整体上是一个十分轻量级的 Go ORM 框架。作者开发这个库的初衷有两点:

  1. 帮助开发者学习如何实现一个 ORM——通过阅读它的源码,可以理解 struct 反射、SQL 拼装、链式 API 设计等核心机制;
  2. 在功能与性能之间寻找平衡——在数据持久化场景下,尽可能在 ORM 的易用性与运行效率之间取得折中。

需要特别说明的是,beedb 是一个支持基础 ORM 功能、但不支持结构体之间关联查询的开源项目;并且根据文档中的标注,该项目目前已不再维护,但代码仍然保留(Project beedb is no longer maintained, but the code is still there)。因此,本文更适合作为学习 Go ORM 设计与源码分析的教材,而非生产项目的首选依赖。

设计基石:database/sql 标准接口

beedb 之所以能兼容多种数据库,关键在于它建立在 Go 标准库 database/sql 接口之上。任何实现了 database/sql 接口的数据库驱动,理论上都可以无缝接入 beedb,无需为每种数据库单独编写适配层。这也是文档中反复强调"beedb 支持 database/sql 标准接口"的原因——它让 ORM 层与具体数据库解耦,驱动切换只需要更换 import 的驱动包即可。

作者实测过以下驱动(均支持 database/sql 接口):

数据库 驱动
MySQL github.com/ziutek/mymysql/godrv
MySQL github.com/go-sql-driver/mysql
PostgreSQL github.com/lib/pq
SQLite github.com/mattn/go-sqlite3
MS ADODB github.com/mattn/go-adodb
ODBC bitbucket.org/miquella/mgodbc

注意:以上驱动地址均以作者实测时期(本书写作年代)为准,如今各驱动仓库的导入路径与维护状态可能已发生变化,实际使用时请以当前可用的驱动版本为准。

安装:go get 一键获取

beedb 完全按照 Go 的包管理风格发布,安装方式与普通第三方库一致:

go get github.com/astaxie/beedb

执行完成后,beedb 源码会被下载到你的 $GOPATH/src/github.com/astaxie/beedb 目录下,可直接在代码中 import 使用。

初始化:导入、建连、创建 ORM 对象

第一步:导入必要包

首先需要导入 database/sql 标准接口包、beedb 包,以及你选用的数据库驱动包(驱动以匿名导入 _ 方式注册):

import (
    "database/sql"
    "github.com/astaxie/beedb"
    _ "github.com/ziutek/mymysql/godrv"
)

第二步:打开连接并创建 beedb 对象

以 MySQL 为例,先通过 sql.Open 建立数据库连接,再调用 beedb.New() 创建 ORM 对象:

db, err := sql.Open("mymysql", "test/xiemengjun/123456")
if err != nil {
    panic(err)
}
orm := beedb.New(db)

beedb.New() 实际接收两个参数:

  • 第一个参数:*sql.DB 数据库连接对象(必填);
  • 第二个参数:数据库引擎标识(可选)。如果使用的是 MySQL 或 SQLite,可以省略第二个参数(走默认引擎逻辑)。

对于其他数据库引擎,则必须显式指定,例如:

// SQL Server
orm = beedb.New(db, "mssql")

// PostgreSQL
orm = beedb.New(db, "pg")

第三步:开启调试模式

beedb 内置调试输出支持,设置全局变量 OnDebug 即可打印执行的 SQL 语句,便于开发期排查:

beedb.OnDebug = true

第四步:定义结构体与表映射

在本书前几节中,我们一直在使用 userinfo 表,对应的结构体定义如下:

type Userinfo struct {
    Uid     int `PK` // 如果主键不是 id,需要加上 `PK` 标签显式声明自定义主键
    Username    string
    Departname  string
    Created     time.Time
}

这里的关键点是 PK 标签:beedb 默认约定主键字段名为 id,如果表的主键不是 id(例如这里是 uid),就必须在字段上添加 PK 标签,告诉 ORM 该字段才是主键,后续的 Save 自动更新、Where 短路查询等逻辑才能正确工作。

命名映射规则(重要):beedb 会自动将驼峰命名(camelCase)转换为下划线命名(snake_case)。例如结构体名 UserInfo 会映射为数据库表 user_info,字段 Departname 会映射为列 departname。这条规则对结构体名和字段名同样适用,定义数据库表结构时需要与此保持一致。

仓库中配套的示例代码 en/code/src/apps/ch.5.5/main.go 给出了另一种标签写法——使用键值对形式 beedb:"PK":

type Userinfo struct {
    Uid        int `beedb:"PK"`
    Username   string
    Department string
    Created    string
}

对比可见,书中教学示例使用简写 PK,仓库示例代码则使用 beedb:"PK" 的完整形式,两种写法在 beedb 的不同版本/用法中都出现过,阅读源码时需留意。

插入数据

beedb 提供两种插入风格:结构体方式与 map 方式。

方式一:Save 保存结构体

与手写 SQL 不同,这里直接操作结构体对象,通过 Save 方法将变更持久化到数据库:

var saveone Userinfo
saveone.Username = "Test Add User"
saveone.Departname = "Test Add Departname"
saveone.Created = time.Now()
orm.Save(&saveone)

插入完成后,可以检查 saveone.Uid 字段——beedb 会自动将数据库自增生成的主键 ID 回填到结构体中,无需你手动查询。

方式二:map 风格插入

beedb 还支持 Go 的 map 类型插入数据,key 为数据库列名,value 为字段值:

add := make(map[string]interface{})
add["username"] = "astaxie"
add["departname"] = "cloud develop"
add["created"] = "2012-12-02"
orm.SetTable("userinfo").Insert(add)

这里出现了 beedb 最具特色的 SetTable 链式 API:方法调用后返回 ORM 对象自身,因此可以继续串联其他操作——这种风格对用过 jQuery 的开发者会非常熟悉。SetTable("userinfo") 的作用是告诉 ORM 本次操作的数据表是 userinfo。

批量插入:InsertBatch

批量插入多个 map 记录时,使用 InsertBatch:

addslice := make([]map[string]interface{}, 10)
add := make(map[string]interface{})
add2 := make(map[string]interface{})
add["username"] = "astaxie"
add["departname"] = "cloud develop"
add["created"] = "2012-12-02"
add2["username"] = "astaxie2"
add2["departname"] = "cloud develop2"
add2["created"] = "2012-12-02"
addslice = append(addslice, add, add2)
orm.SetTable("userinfo").InsertBatch(addslice)

更新数据

方式一:复用 Save(自动识别插入/更新)

继续使用上面的 saveone——此时它的主键 Uid 已经有值,beedb 检测到主键存在后,会自动执行 UPDATE 操作而不是再次 INSERT:

saveone.Username = "Update Username"
saveone.Departname = "Update Departname"
saveone.Created = time.Now()
orm.Save(&saveone)  // update

这是 beedb 的一个核心设计:Save 根据"主键是否有值"自动决定执行插入还是更新,从而让 CRUD 代码保持统一。

方式二:map 风格更新

同样可以用 map 配合链式 API 更新指定记录:

t := make(map[string]interface{})
t["username"] = "astaxie"
orm.SetTable("userinfo").SetPK("uid").Where(2).Update(t)

逐一说明这里用到的方法:

  • .SetPK("uid"):告诉 ORM uid 是 userinfo 表的主键字段;
  • .Where(2):设置查询/操作条件,支持多个参数。如果第一个参数是整数,它是 Where("<主键>=?", 值) 的简写形式——即等价于 Where("uid=?", 2);
  • .Update(t):接收一个 map,按条件更新数据库记录。

查询数据

beedb 的查询接口非常灵活,Where 支持从 1 到多个参数的不同调用形式。

单条查询:Find

示例 1:按主键精确查询(双参数形式)

var user Userinfo
// Where 接受两个参数,第二个为整数值
orm.Where("uid=?", 27).Find(&user)

示例 2:省略主键名的短形式

var user2 Userinfo
orm.Where(3).Find(&user2) // 等价于 Where("uid=?", 3)

示例 3:普通条件查询(字符串值)

var user3 Userinfo
// Where 接受两个参数,第二个为字符串
orm.Where("name = ?", "john").Find(&user3)

示例 4:多条件组合查询

var user4 Userinfo
// Where 接受三个参数,自动拼接多个条件
orm.Where("name = ? and age < ?", "john", 88).Find(&user4)

多条查询:FindAll + Limit + OrderBy

示例 1:带偏移量的分页查询——获取 id > 3 的记录,从第 20 条开始取 10 条:

var allusers []Userinfo
err := orm.Where("id > ?", "3").Limit(10, 20).FindAll(&allusers)

示例 2:省略偏移量——不指定第二个参数时,默认从位置 0 开始取 10 条:

var tenusers []Userinfo
err := orm.Where("id > ?", "3").Limit(10).FindAll(&tenusers)

示例 3:获取全部记录并按条件排序:

var everyone []Userinfo
err := orm.OrderBy("uid desc,username asc").FindAll(&everyone)

这两个方法的参数约定:

  • .Limit() 支持两个参数:(结果数量, 起始位置)。起始位置默认值为 0;
  • .OrderBy() 用于排序,参数为排序条件字符串(直接对应 SQL 的 ORDER BY 子句),例如 "uid desc,username asc"。

查询结果放入 map:Select + FindMap

前面的示例都是把记录映射到结构体;如果你更希望以 map 形式拿到原始数据,可以这样写:

a, _ := orm.SetTable("userinfo").SetPK("uid").Where(2).Select("uid,username").FindMap()
  • .Select("uid,username"):指定要查询的字段列表。如果不调用 Select,默认返回全部字段;
  • .FindMap():返回 []map[string][]byte 类型的结果,字段值为原始字节,需要你自行转换为需要的类型。

删除数据

beedb 提供了多种删除接口:

示例 1:删除单条记录(按结构体的主键定位):

// saveone 是前面示例中插入/更新过的对象
orm.Delete(&saveone)

示例 2:批量删除切片中的所有记录:

// alluser 是存放了多条记录的结构体切片
orm.DeleteAll(&alluser)

示例 3:直接用 SQL 条件删除:

orm.SetTable("userinfo").Where("uid>?", 3).DeleteRow()

关联查询:Join 链式实现

如开头所述,beedb 本身不支持结构体之间的关联查询(association queries)。但考虑到部分应用确实需要多表连接,beedb 还是提供了一个"简陋但可用"的实现——通过 Join 方法直接拼接 SQL JOIN:

a, _ := orm.SetTable("userinfo").Join("LEFT", "userdetail", "userinfo.uid=userdetail.uid").
    Where("userinfo.uid=?", 1).
    Select("userinfo.uid,userinfo.username,userdetail.profile").
    FindMap()

Join() 方法接收三个参数:

  • 第一个参数:连接类型,如 INNER、LEFT、OUTER、CROSS 等;
  • 第二个参数:要连接的目标表名;
  • 第三个参数:连接条件(JOIN ON 子句)。

注意,这种方式的本质是"面向 SQL 字符串的轻量封装",结果通过 FindMap 以 map 形式返回,而不是自动组装成嵌套结构体对象。

Group By 与 Having

beedb 同样实现了 SQL 的 GROUP BY 与 HAVING 能力:

a, _ := orm.SetTable("userinfo").GroupBy("username").Having("username='astaxie'").FindMap()
  • .GroupBy("username"):指定分组字段;
  • .Having("username='astaxie'"):指定 HAVING 过滤条件。

仓库示例:一次完整的 CRUD 编排

本书英文版仓库提供了完整的可运行示例 en/code/src/apps/ch.5.5/main.go,它以 SQLite 为数据库(sql.Open("sqlite3", "./foo.db")),并在 main() 中按顺序演示了本章全部核心 API:

  • insertUsingStruct():用 orm.Save(&obj) 结构体方式插入,并回读自增 Uid;
  • insertUsingMap():用 orm.SetTable("userinfo").Insert(add) map 方式插入;
  • getOneUserInfo():orm.Where("uid=?", id).Find(&obj) 单条查询;
  • getAllUserInfo():orm.Limit(10).Where("uid>?", id).FindAll(&alluser) 多条查询;
  • updateUserinfo():修改字段后再次 orm.Save(&obj) 触发更新;
  • updateUsingMap():orm.SetTable("userinfo").Where("uid>?", id).Update(t) map 批量更新(示例注释中保留了 SetPK("uid").Where(2) 的单条更新写法);
  • getMapsFromSelect():orm.SetTable("userinfo").SetPK("uid").Where(id).Select("uid,username").FindMap() 返回 map 结果;
  • groupby() / joinTables():分别演示 GroupBy+Having 与 Join 链式查询;
  • deleteWithUserinfo() / deleteRows() / deleteAllUserinfo():分别对应 Delete、DeleteRow、DeleteAll 三种删除方式。

整个示例还演示了 checkError 统一 panic 错误处理、getTimeStamp() 生成时间字符串等工程化细节,是通读本章 API 的最佳参照物——结构与本文前述各节一一对应,可以直接 go run 验证。

后续规划与现状

作者在文档末尾记录了收到全球大量用户反馈后计划重构的方向,这些内容对理解 ORM 设计边界很有参考价值:

  • 实现类似 database/sql/driver 的接口设计,以进一步简化 CRUD 操作;
  • 实现关系型数据库的关联能力(一对一、一对多、多对多),并给出了设想中的结构体示例:
type Profile struct {
    Nickname string
    Mobile   string
}
type Userinfo struct {
    Uid         int
    PK_Username string
    Departname  string
    Created     time.Time
    Profile     HasOne
}
  • 自动创建数据表和索引;
  • 使用 goroutine 实现连接池。

结合文档开头的声明(beedb 已不再维护),上述规划多数未在 beedb 中落地;如果你需要这些高级特性(关联查询、建表同步、连接池),可以关注作者后来在 beego 框架中整合的 orm 模块(见 zh/05.5.md 中 orm.RegisterDataBase、orm.RegisterModel、orm.NewOrm() 等 API),其设计思路与本节的 beedb 一脉相承。

本章相关章节

  • 本章目录索引:preface.md
  • 上一节:PostgreSQL 数据库操作——了解 beedb 驱动 lib/pq 对应的数据库及 $1, $2 占位符差异
  • 下一节:NoSQL 数据库操作——对比 beedb 与 mgo 等 NoSQL 驱动的"基于结构体"操作风格
  • 章节总结:05.7.md——回顾本章 database/sql 接口、beedb ORM 与 NoSQL 的整体脉络
登录后查看全文
build-web-application-with-golang