SQL优先的 Go ORM 框架——Bun 介绍
承蒙大家厚爱,我的《Go语言之路》的纸质版图书已经上架京东,有需要的朋友请点击 此链接 购买。
Bun 是一个 SQL 优先的 Golang ORM(对象关系映射),支持 PostgreSQL、MySQL、MSSQL和SQLite。它旨在提供一种简单高效的数据库使用方法,同时利用 Go 的类型安全性并减少重复代码。
基本介绍
Bun 是开源公司 uptrace 开源的一款 Go 语言 ORM 框架。
虽说它是一款 ORM 框架,但是它与其他 ORM 框架有很大的不同,Bun 的目标是帮助你编写 SQL,而不是将其隐藏在奇怪的构造体后面。除此之外,Bun 还有以下特点。
- 使用原生的
database/sql
,并以兼容和惯用的方式对其进行扩展。 - 使用既有 SQL 语句获得类似 ORM 的体验。Bun 支持 structs、map、标量以及 map/structs/标量的切片。
- 开箱即用适用于 PostgreSQL、MySQL 5.7+(包括 MariaDB)、MSSQL 和 SQLite。
- 使用 Go 和基于 SQL 的迁移来更新数据库 schema 。
与 GORM 比较
相比于使用 GORM 框架,使用 Bun 可以更容易地编写复杂查询。Bun 可以更好地集成特定数据库的功能,例如 PostgreSQL arrays。通常 Bun 的速度也更快。
Bun 不支持自动迁移、优化器/索引/注释提示和数据库解析器等 GORM 常用功能。
与 ent 比较
- 使用 Bun 的时候你可以利用以前使用 SQL DBMS 和 Go 的经验来编写快速、习惯化的代码。Bun 的目标是帮助你编写 SQL 而不是取代或隐藏它。
- 使用 ent 框架则无法利用之前的经验,因为 ent 提供了一种全新/不同的方法来编写 Go 应用程序,你只能遵循它的规则。
安装
安装 Bun:
go get github.com/uptrace/bun@latest
连接数据库
Bun 工作在 database/sql
之上,所以首选要创建一个 sql.DB
。不管使用的是什么数据库,只要创建好了 sql.DB
对象,就可以使用对应的 Bun 方言来创建 Bun 对象。
连接 MySQL
Bun 使用 github.com/go-sql-driver/mysql 和 Bun 的 mysqldialect
方言支持 MySQL 5+。
import (
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/mysqldialect"
_ "github.com/go-sql-driver/mysql"
)
sqldb, err := sql.Open("mysql", "root:pass@/test")
if err != nil {
panic(err)
}
db := bun.NewDB(sqldb, mysqldialect.New())
连接 PostgreSQL
Bun 使用它自己实现的 pgdriver 作为 PostgreSQL 驱动来使用 DSN (connection string) 来连接 PostgreSQL 数据库:
import (
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
"github.com/uptrace/bun/driver/pgdriver"
)
dsn := "postgres://postgres:@localhost:5432/test?sslmode=disable"
// dsn := "unix://user:pass@dbname/var/run/postgresql/.s.PGSQL.5432"
sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))
db := bun.NewDB(sqldb, pgdialect.New())
连接 MSSQL
Bun 从v1.1.1 版本开始支持 SQL Server v2019.CU4。 要连接到 SQL Server,请使用 go-mssqldb 驱动程序和 mssqldialect:
import (
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/mssqldialect"
_ "github.com/denisenkom/go-mssqldb"
)
sqldb, err := sql.Open("sqlserver", "sqlserver://sa:passWORD1@localhost:1433?database=test")
if err != nil {
panic(err)
}
db := bun.NewDB(sqldb, mssqldialect.New())
连接 SQLite
连接 SQLite 数据库、 使用 sqliteshim 驱动,该驱动程序会根据平台自动导入modernc.org/sqlite 或 mattn/go-sqlite3 数据库。
import (
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/sqlitedialect"
"github.com/uptrace/bun/driver/sqliteshim"
)
sqldb, err := sql.Open(sqliteshim.ShimName, "file::memory:?cache=shared")
if err != nil {
panic(err)
}
db := bun.NewDB(sqldb, sqlitedialect.New())
连接 Oracle
连接 Oracle 数据库、 使用 go-oci8 驱动程序 和 oracledialect :
import (
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/oracledialect"
_ "github.com/mattn/go-oci8"
)
sqldb, err := sql.Open("oci8", "127.0.0.1")
if err != nil {
panic(err)
}
db := bun.NewDB(sqldb, oracledialect.New())
编写特定于数据库的代码
Bun 自带 feature 包,可让你发现数据库管理系统支持的功能:
import "github.com/uptrace/bun/dialect/feature"
if db.HasFeature(feature.InsertOnConflict) {
// DBMS supports `ON CONFLICT DO UPDATE` (PostgreSQL, SQLite)
}
if db.HasFeature(feature.InsertOnDuplicateKey) {
// DBMS supports `ON DUPLICATE KEY UPDATE` (MySQL, MariaDB)
}
你还可以直接在代码中检查数据库方言名称,根据不同的数据库方言执行不同的逻辑。
import "github.com/uptrace/bun/dialect"
switch db.Dialect().Name() {
case dialect.SQLite:
case dialect.PG:
case dialect.MySQL:
case dialect.MSSQL:
default:
panic("not reached")
}
基本使用
下面使用 Bun 连接 MySQL 演示 Bun 的基本使用。
先连接数据库,创建。
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/mysqldialect"
"github.com/uptrace/bun/extra/bundebug"
)
// MustInitBunDB 初始化 Bun 数据库连接
// dsn: "root:pass@tcp(127.0.0.1:3306)/db?charset=utf8mb4&parseTime=True"
func MustInitBunDB(dsn string) *bun.DB {
// 连接数据库
sqldb, err := sql.Open("mysql", dsn)
if err != nil {
panic(err)
}
// 基于 sql.DB 对象创建 Bun DB 对象
db := bun.NewDB(sqldb, mysqldialect.New())
// 想要在终端输出执行的 SQL 语句,可以安装下面的 query hook
db.AddQueryHook(bundebug.NewQueryHook(
bundebug.WithVerbose(true),
bundebug.FromEnv("BUNDEBUG"), // 可通过环境变量 BUNDEBUG 控制
))
return db
}
查询
你可以像之前一样,使用标准库 database/sql
API 执行查询:
func queryWithStdAPI(ctx context.Context, db *bun.DB) {
res, err := db.ExecContext(ctx, "SELECT 1")
fmt.Printf("res:%v err:%v\n", res, err)
var num int
err = db.QueryRowContext(ctx, "SELECT 1").Scan(&num)
if err != nil {
fmt.Printf("QueryRowContext failed, err:%v\n", err)
}
}
或者使用 Bun 的查询生成器:
func queryWithBunAPI(ctx context.Context, db *bun.DB) {
res, err := db.NewSelect().ColumnExpr("1").Exec(ctx)
fmt.Printf("res:%v err:%v\n", res, err)
var num int
err = db.NewSelect().ColumnExpr("1").Scan(ctx, &num)
if err != nil {
fmt.Printf("QueryRowContext failed, err:%v\n", err)
}
}
在既有代码中使用 Bun
学习 Bun 的所有功能可能需要一些时间,但你可以通过执行 SQL 查询并用 Bun 扫描结果,立即开始在项目中使用它:
func queryWithExistingCode(ctx context.Context, db *bun.DB) {
type User struct {
ID int64
Name string
}
users := make([]User, 0)
err := db.NewRaw(
"SELECT id, name FROM ? LIMIT ?", // 使用之前写好的 SQL 语句
bun.Ident("users"), 100,
).Scan(ctx, &users) // 使用 bun 扫描结果
fmt.Printf("users:%v err:%v\n", users, err)
}
上面的代码将执行以下 SQL。
SELECT id, name FROM "users" LIMIT 100
如果你先前的代码中使用了 *sql.Tx
或 *sql.Conn
,你仍然可以使用 Bun 查询生成器,而无需重写现有代码:
func queryWithExistingTraction(ctx context.Context, sqldb *sql.DB, bundb *bun.DB) {
tx, err := sqldb.Begin()
if err != nil {
panic(err)
}
if _, err := tx.Exec("...existing query..."); err != nil {
panic(err)
}
model := struct {
ID int64
Name string
}{
ID: 1,
Name: "Q1mi",
}
res, err := bundb.NewInsert().
Conn(tx). // 使用既有的事务进行查询
Model(&model).
Exec(ctx)
fmt.Printf("res:%#v\n", res)
}
定义模型
Bun 使用基于结构体的模型来构造查询和扫描结果。典型的 Bun 模型如下:
type User struct {
bun.BaseModel `bun:"table:users,alias:u"`
ID int64 `bun:",pk,autoincrement"`
Name string
}
有了模型,你就可以创建和删除表:
// 创建 users 表
res, err := db.NewCreateTable().Model((*User)(nil)).Exec(ctx)
// 删除 users 表
res, err := db.NewDropTable().Model((*User)(nil)).Exec(ctx)
// 删除和创建表
err := db.ResetModel(ctx, (*User)(nil))
插入行
// 插入一个 user
user := &User{Name: "admin"}
res, err := db.NewInsert().Model(user).Exec(ctx)
// 批量插入
users := []User{user1, user2}
res, err := db.NewInsert().Model(&users).Exec(ctx)
更新行
user := &User{ID: 1, Name: "admin"}
res, err := db.NewUpdate().Model(user).Column("name").WherePK().Exec(ctx)
删除行
user := &User{ID: 1}
res, err := db.NewDelete().Model(user).WherePK().Exec(ctx)
查询并扫描结果:
// 根据主键查询
user := new(User)
err := db.NewSelect().Model(user).Where("id = ?", 1).Scan(ctx)
// 根据 ID 排序,查询前10个 user
var users []User
err := db.NewSelect().Model(&users).OrderExpr("id ASC").Limit(10).Scan(ctx)
扫描查询结果
当涉及到扫描查询结果时,Bun 非常灵活,允许扫描到结构体中:
user := new(User)
err := db.NewSelect().Model(user).Limit(1).Scan(ctx)
扫描结果到变量
var id int64
var name string
err := db.NewSelect().Model((*User)(nil)).Column("id", "name").Limit(1).Scan(ctx, &id, &name)
扫描到 map[string]interface{}
var m map[string]interface{}
err := db.NewSelect().Model((*User)(nil)).Limit(1).Scan(ctx, &m)
并扫描到上述类型的切片:
var users []User
err := db.NewSelect().Model(&users).Limit(1).Scan(ctx)
var ids []int64
var names []string
err := db.NewSelect().Model((*User)(nil)).Column("id", "name").Limit(1).Scan(ctx, &ids, &names)
var ms []map[string]interface{}
err := db.NewSelect().Model((*User)(nil)).Scan(ctx, &ms)
还可以返回插入/更新/删除查询的结果,并对其进行扫描:
var ids []int64
res, err := db.NewDelete().Model((*User)(nil)).Returning("id").Exec(ctx, &ids)
表关系
Bun 还可以识别常见的表关系,例如,你可以定义一个属于关系:
type Story struct {
ID int64
Title string
AuthorID int64
Author *User `bun:"rel:belongs-to,join:author_id=id"`
}
查询 Story 的 Author
story := new(Story)
err := db.NewSelect().
Model(story).
Relation("Author").
Limit(1).
Scan(ctx)
上面代码执行的 SQL 命令如下。
SELECT
"story"."id", "story"."title", "story"."author_id",
"author"."id" AS "author__id",
"author"."name" AS "author__name"
FROM "stories" AS "story"
LEFT JOIN "users" AS "author" ON ("author"."id" = "story"."author_id")
LIMIT 1
更多表关系的查询请查看官方文档 relations 这一节。