SQL 优先的 Go ORM 框架——bun 介绍
Bun 是一个 SQL 有限的 Golang ORM(对象关系映射),支持 PostgreSQL、MySQL、MSSQL 和 SQLite。它旨在提供一种简单搞笑的数据库使用方法,同时利用 Go 的类型安全性并减少重复代码。
基本介绍
Bun 是开源公司 uptrace 开源的一款 Go 语言 ORM 框架。
虽说它是一款 ORM 框架,但是它与其他 ORM 框架有很大的不同,Bunch 的目标是帮助你编写 SQL,而不是将其隐藏在奇怪的构造体后面。除此之外,Bun 还有以下特点。
- 使用原生的 database/sql,并以兼容和惯用的方式对其进行扩展。
- 使用既有的 SQL 语句获得类似 ORM 的体验。Bun 支持 structs、map、标量以及 map/structs/标量的切片。
- 开箱即用适用于 PostgreSQL、MySQL5.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 工作在databse/sql之上,所以首选要创建一个sql.DB。不管使用的是什么数据库,只要创建好了sql.DB对象,就可以使用对应的 Bun 方言来创建 Bun 对象。
连接 MySQL
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/mysqldialect"
)
func main() {
sqldb, err := sql.Open("mysql", "root:pass@/test")
if err != nil {
panic(err)
}
db := bun.NewDB(sqldb, mysqldialect.New())
fmt.Println(db)
}连接 PostgreSQL
Bun 使用它自己实现的 pgdriver 作为 PostgreSQL 驱动来使用 DSN(connection string)来连接 PostgreSQL 数据库:
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
"github.com/uptrace/bun/driver/pgdriver"
)
func main() {
dsn := "postgres://postgres:@localhost:5432/test?sslmode=disable"
sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))
db := bun.NewDB(sqldb, pgdialect.New())
fmt.Println(db)
}连接 MSSQL
Bun 从 v1.1.1版本开始支持 SQL Server v2019.CU4.要连接到 SQL Server,请使用go-mssqldb驱动程序和 mssqldialect:
package main
import (
"database/sql"
"fmt"
_ "github.com/denisenkom/go-mssqldb"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/mssqldialect"
)
func main() {
sqldb, err := sql.Open("sqlserver", "sqlserver://sa:password@localhost:1433?database=test")
if err != nil {
panic(err)
}
db := bun.NewDB(sqldb, mssqldialect.New())
fmt.Println(db)
}连接 SQLite
连接 SQLite 数据库、使用 sqlitehim 驱动,该驱动程序会根据平台自动导入 modernc.org/sqlite 或 mattn/go-sqlite3数据库。
package main
import (
"database/sql"
"fmt"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/sqlitedialect"
"github.com/uptrace/bun/driver/sqliteshim"
)
func main() {
sqldb, err := sql.Open(sqliteshim.ShimName, "file::memory:?cache=shared")
if err != nil {
panic(err)
}
db := bun.NewDB(sqldb, sqlitedialect.New())
fmt.Println(db)
}连接 Oracle
连接 Oracle 数据库、使用 go-oci8驱动程序和 oracledialect:
package main
import (
"database/sql"
"fmt"
_ "github.com/mattn/go-oci8"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/oracledialect"
)
func main() {
sqldb, err := sql.Open("oci8", "127.0.0.1")
if err != nil {
panic(err)
}
db := bun.NewDB(sqldb, oracledialect.New())
fmt.Println(db)
}编写特定于数据库的代码
Bun 自带 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, MarialDB)
}你还可以直接在代码中检查数据库方言名称,根据不同的数据库方言执行不同的逻辑。
switch db.Dialect().Name() {
case dialect.SQLite:
case dialect.PG:
case dialect.MySQL:
case dialect.MSSQL:
default:
panic("not reached")
}基本使用
下面使用 Bun 连接 MySQL 演示 Bun 的基本使用。
先连接数据库,创建。
package main
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"
)
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/sqlAPI 执行查询:
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, 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, _ := 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)
// 批量插入
user1 := User{Name: "admin1"}
user2 := User{Name: "admin2"}
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, Name: "admin"}
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这一节。