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/sqlitemattn/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 这一节。


扫码关注微信公众号