基于 beedb 开发 Go 轻量级 ORM:database/sql 之上的链式 CRUD 实战
基于 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 框架。作者开发这个库的初衷有两点:
- 帮助开发者学习如何实现一个 ORM——通过阅读它的源码,可以理解 struct 反射、SQL 拼装、链式 API 设计等核心机制;
- 在功能与性能之间寻找平衡——在数据持久化场景下,尽可能在 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"):告诉 ORMuid是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 的整体脉络