GORM CRUD 10 分钟快速上手

2023-10-12 15:31:08 浏览数 (2)

1.ORM 是什么

ORM(Object Relational Mapping),中文名为对象关系映射。

使用 ORM 组件,可以让开发者通过操作对象的方式完成对数据库的操作(读写),避免手动书写 SQL 和完成数据到对象的转换,让我们更方便地操作数据库。

理论上 ORM 可以让我们脱离 SQL,但实际上还是需要懂 SQL 才能更好地使用 ORM。

2.GORM 是什么

GORM 是一个流行的 Golang ORM 库。

类似于 Java 生态里大家听到过的 Mybatis、Hibernate、SpringData 等。

GORM 由国人开发,中文文档齐全,对开发者友好,支持主流关系型数据库。

  • MySQL
  • SQL Server
  • PostgreSQL
  • SQlite

GORM 功能丰富齐全:

  • 关联 (拥有一个,拥有多个,属于,多对多,多态,单表继承)
  • 钩子(before/after create/save/update/delete/find)
  • 支持 Preload、Joins 的预加载
  • 事务,嵌套事务,保存点,回滚到保存点
  • Context、预编译模式、DryRun 模式
  • 批量插入,FindInBatches,使用 Map Find/Create,使用 SQL 表达式、Context Valuer 进行 CRUD
  • SQL 构建器,Upsert,锁,Optimizer/Index/Comment Hint,命名参数,子查询
  • 复合主键,索引,约束
  • 自动迁移
  • 自定义 Logger
  • 灵活的可扩展插件 API:Database Resolver(多数据库,读写分离)、Prometheus…
  • 每个特性都经过了测试的重重考验
  • 开发者友好

GORM 最新源码地址:go-gorm/gorm。

GORM V1 版本地址:jinzhu/gorm。

GORM 中文文档地址:这里。

本文将讲解 GORM 中常用的功能,帮助你快速上手。

当然除了 GORM,你还有其他选择,比如 facebook-ent、sqlx 和 sqlc 等。

3.安装

基于 Go Module 开发,import 最新包然后 go get 即可。

代码语言:javascript复制
go get -u gorm.io/gorm

// 不同 DB 对应的驱动
go get -u gorm.io/driver/sqlite
go get -u gorm.io/driver/mysql
go get -u gorm.io/driver/postgres
go get -u gorm.io/driver/sqlserver

驱动包按照自己实际使用的 DB 选择即可。

本文将以 MySQL 为例,讲解 GORM 的使用。

4.连接 DB

以 MySQL 为例,建立数据库连接。

代码语言:javascript复制
import (
	"fmt"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// MySQLConn GORM MySQL 连接。
var MySQLConn *gorm.DB

// Init gorm mysql connnection.
// 依赖服务配置初始化完成。
func InitMySQLConn() error {
	// data source name.
	dsn := fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?charset=utf8mb4&parseTime=True&loc=Local", Conf.Mysql.User, Conf.Mysql.Passwd, Conf.Mysql.IP, Conf.Mysql.Port, Conf.Mysql.Dbname)
	var err error
	MySQLConn, err = gorm.Open(mysql.Open(dsn))
	return err
}

填入 DB 对应的正确的用户名、密码、地址、端口、数据库名称等信息后,便可建立对应数据源的连接。相关配置一般在服务启动时,事先从配置文件中加载。

5.创建数据表

在进行增查改删(CRUD)之前,需要先创建一个数据表。

GORM 中一个 struct 对应一张数据库表,对应的 struct 被称为模型。

假如我们要创建一张商品(goods)表,那么模型可定义为:

代码语言:javascript复制
// Good 商品。
type Good struct {
	gorm.Model
	Name  string `gorm:"type:string;size:256;not null"`
	Price int    `gorm:"type:int;not null"`
}

其中 gorm.Model 时 GORM 预先定义的一些基础字段,我们可以嵌入直接拿来用。

代码语言:javascript复制
// Model a basic GoLang struct which includes the following fields: ID, CreatedAt, UpdatedAt, DeletedAt
// It may be embedded into your model or you may build your own model without it
//
//	type User struct {
//	  gorm.Model
//	}
type Model struct {
	ID        uint `gorm:"primarykey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt DeletedAt `gorm:"index"`
}

字段后的 tag 用来定义字段在 DB 中的相关属性,如 primarykey 表示主键,index 表示索引,type 表示字段类型。

除此以外,还有更加丰富的标签定义参见官方文档:字段标签。

一般在服务启动时创建数据表,如建立 DB 连接后只执行一次来完成数据表的创建。

代码语言:javascript复制
db.AutoMigrate(&User{})

db.AutoMigrate(&User{}, &Product{}, &Order{})

// 创建表时添加后缀。
db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{})

比如创建我们上面的商品表。

代码语言:javascript复制
// 自动创建表,如果表已经存在不会有任何动作。
err := MySQLConn.AutoMigrate(&Good{})

创建好后的数据表名为 struct 名称命名方式是 snake_case(下划线命名法)的复数形式,字段名为 struct 字段的 sanke_case 形式。

如果想更改表名,可以通过在模型结构体上添加 TableName() 方法来自定义表名称。

代码语言:javascript复制
func (Good) TableName() string {
	return "tb_good"
}

如果想更改表字段名,可在模型结构体上通过 gorm tag 的 column 标签指定。

代码语言:javascript复制
type User struct {
	gorm.Model
	Name string `gorm:"column:user_name"`
}

6.选择表

通过如下方式选择要操作的表。

代码语言:javascript复制
DB.Model(&ModelName{})
DB.Table("table_name")
DB.Table("table_name alias_name")
DB.Table("table_name AS alias_name")

使用 Table() 方法为指定要操作的表时,如果表名太长,可以使用 AS(可省略)设置一个短别名来引用表。

7.CRUD

1.增加(Create)

增加单个
代码语言:javascript复制
// 插入商品。
task := &Good{
	Name:   name,
	Price: price,
}
err := MySQLConn.Create(task).Error
// 或
// err := MySQLConn.Save(task).Error

主键 ID 会自增,此外 GORM 还会自动维护 created_at、updated_ad 和 deleted_at 三个字段。

其中 Save 方法在保存记录时,如果主键 ID 非空则执行更新操作,零值也会更新到 DB。如果主键 ID 为空,则执行插入操作。

增加多个

我们还可以使用 Create() 创建多项记录。

代码语言:javascript复制
users := []*User{
    User{Name: "Jinzhu", Age: 18, Birthday: time.Now()},
    User{Name: "Jackson", Age: 19, Birthday: time.Now()},
}

result := db.Create(users) // pass a slice to insert multiple row

result.Error        // returns error
result.RowsAffected // returns inserted records count

同样地,Save() 也可以创建多项记录。

代码语言:javascript复制
result := db.Save(users) // pass a slice to insert multiple row when value does not contain primary key

result.Error        // returns error
result.RowsAffected // returns inserted records count

2.查询(Read)

按照主键查询
代码语言:javascript复制
db.First(&user, 10)
// SELECT * FROM users WHERE id = 10;

db.First(&user, "10")
// SELECT * FROM users WHERE id = 10;

如果查不到,将报 gorm.ErrRecordNotFound 错误。

当目标对象有一个主键值时,将使用主键构建查询条件,例如:

代码语言:javascript复制
var user = User{ID: 10}
db.First(&user)
// SELECT * FROM users WHERE id = 10;

var result User
db.Model(User{ID: 10}).First(&result)
// SELECT * FROM users WHERE id = 10;
IN 查询

比如按照多个主键查询。

代码语言:javascript复制
db.Find(&goods, []int{1,2,3})

或者通过内联条件。查询条件可以以类似于 Where 的方式内联到 First 和 Find 等方法中。

代码语言:javascript复制
db.Find(&goods, "id IN ?", []int{1,2,3})

或者通过 Where 指定 IN 条件。

代码语言:javascript复制
db.Where("id IN ?", []int{1,2,3}).Find(&goods)
AND 条件

再如按照其他字段进行 and 查询。

多次调用 Where 方法可指定多个条件,条件关系为 AND。

代码语言:javascript复制
// getGoodsByInfo 根据商品信息分页拉取。
func getGoodsByInfo(name string, price int, lastID uint) ([]Good, error) {
	db := internal.MySQLConn
	if name != "" {
		db.Where("name = ?", name)
	}
	db.Where("price >= ?", price)
	
	// 按照每页大小 50 拉取商品。
	db.Where("id > ?", lastID).Order("id ASC").Limit(50)

	var goods []Good
	result := db.Find(&goods)
	return goods, result.Error
}
OR 条件
代码语言:javascript复制
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)
// SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin';

// Struct
db.Where("name = 'jinzhu'").Or(User{Name: "jinzhu 2", Age: 18}).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' OR (name = 'jinzhu 2' AND age = 18);

// Map
db.Where("name = 'jinzhu'").Or(map[string]interface{}{"name": "jinzhu 2", "age": 18}).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' OR (name = 'jinzhu 2' AND age = 18);
Group 条件

使用 Group 条件可以更轻松地编写复杂 SQL。

代码语言:javascript复制
db.Where(
    db.Where("pizza = ?", "pepperoni").Where(db.Where("size = ?", "small").Or("size = ?", "medium")),
).Or(
    db.Where("pizza = ?", "hawaiian").Where("size = ?", "xlarge"),
).Find(&Pizza{})

// SELECT * FROM `pizzas` WHERE (pizza = "pepperoni" AND (size = "small" OR size = "medium")) OR (pizza = "hawaiian" AND size = "xlarge")
通过结构体指定查询字段
代码语言:javascript复制
db.Where(&Good{Name: "衣服", Price: 10}).Find(&goods)
// SELECT * FROM goods WHERE name = "衣服" AND price = 10;

db.Where(&Good{Name: "衣服", Price: 10}, "Name").Find(&goods)
// SELECT * FROM goods WHERE name = "衣服";
查询记录数
代码语言:javascript复制
// getGoodNumber 获取符合条件的商品数量。
func getGoodNumber(price int) (int, error) {
	var c int
	err := internal.MySQLConn.Model(&Good{}).Where("price >= ?", price).Count(&c).Error
	return c, err
}
查询记录是否存在

在 GORM 中,可以使用 Count 方法来判断一个查询是否返回了记录。

代码语言:javascript复制
func GoodExist(name string) (bool, error) {
	var c int64
	err := MySQLConn.Model(&Good{}).Where("name = ?", name).Count(&count).Error
	return count > 0, err
}

当然,你也可以使用 First 方法,并结合 ErrRecordNotFound 错误来判断记录是否存在。

代码语言:javascript复制
func GoodExist(name string) (bool, error) {
	var good Good
	err := MySQLConn.Where("name = ?", name).First(&good).Error
	// 不存在。
	if err == gorm.ErrRecordNotFound {
		return false, nil
	}
	// 查询发生错误。
	if err != nil {
		return false, err
	}
	// 存在。
	return true, nil
}

使用 Count 方法可以在不加载实际记录的情况下检查是否存在记录。这种方法比使用 First 方法更高效,尤其是在需要检查大量记录是否存在的情况下。因为 Count 方法只计算匹配条件的记录数,而不需要加载和返回记录的实际内容。但是,它可能会有一些微小的开销,因为它需要向数据库发送一个额外的 COUNT(*) 查询来计算记录数。

所以,如果你只是需要检查记录是否存在,推荐使用 Count 方法。

查询单个字段

使用 Pluck 方法可以查询指定字段的所有值。如下面的代码查询 users 表中所有用户的姓名。

代码语言:javascript复制
var names []string
db.Model(&User{}).Pluck("name", &names)

如果是一条记录的某个字段,可以使用单个变量而非切片接收查询结果。

代码语言:javascript复制
var name string
db.Model(&User{}).Where("id = ?", 1).Pluck("name", &name)
查询多个字段

如果您想要查询多列,您应该使用 Select 和 Scan 或 Find。

代码语言:javascript复制
// 超过一列的查询,应该使用 Scan 或 Find
db.Select("name", "age").Scan(&users)
db.Select("name", "age").Find(&users)

Scan 和 Find 的作用是类似的,关于二者的区别可参考 GORM Issue #4218。下面是大佬 jinzhu 的回答。

Scan, Find using different callbacks, Scan won’t call hooks methods.

Distinct

从模型中选择不同的值。

代码语言:javascript复制
db.Distinct("name", "age").Order("name, age desc").Find(&results)

Distinct 也可与 Pluck 和 Count 配合使用。

Limit & Offset

可以使用 Limit & Offset 实现分页查询。

Limit 指定要检索的最大记录数, Offset 指定在开始返回记录之前要跳过的记录数。

代码语言:javascript复制
db.Limit(3).Find(&users)
// SELECT * FROM users LIMIT 3;

// Cancel limit condition with -1
db.Limit(10).Find(&users1).Limit(-1).Find(&users2)
// SELECT * FROM users LIMIT 10; (users1)
// SELECT * FROM users; (users2)

db.Offset(3).Find(&users)
// SELECT * FROM users OFFSET 3;

db.Limit(10).Offset(5).Find(&users)
// SELECT * FROM users OFFSET 5 LIMIT 10;

// Cancel offset condition with -1
db.Offset(10).Find(&users1).Offset(-1).Find(&users2)
// SELECT * FROM users OFFSET 10; (users1)
// SELECT * FROM users; (users2)

利用 Limit & Offset 实现分页查询时,如果需要同时查询符合条件的记录总数,则需要先查询记录数,再查询记录。

代码语言:javascript复制
db.Model(&User{}).Where("age >= ?", 18)

// 先查询记录数。
var count int64
db.Count(&count)
// 再查询记录。
db.Limit(10).Offset(10).Find(&users)

// 或 Count 在前。
var count int64
db.Count(&count).Limit(10).Offset(10).Find(&users)
排序

从数据库检索记录时指定顺序。

代码语言:javascript复制
db.Order("age desc, name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;

// Multiple orders
db.Order("age desc").Order("name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;

db.Clauses(clause.OrderBy{
  Expression: clause.Expr{SQL: "FIELD(id,?)", Vars: []interface{}{[]int{1, 2, 3}}, WithoutParentheses: true},
}).Find(&User{})
// SELECT * FROM users ORDER BY FIELD(id,1,2,3)
子查询

GORM 允许您在 Table 方法中通过 FROM 子句使用子查询。

代码语言:javascript复制
db.Table("(?) AS u", db.Model(&User{}).Select("name", "age")).Where("age = ?", 18).Find(&User{})
// SELECT * FROM (SELECT `name`,`age` FROM `users`) AS u WHERE `age` = 18

subQuery1 := db.Model(&User{}).Select("name")
subQuery2 := db.Model(&Pet{}).Select("name")
db.Table("(?) AS u, (?) AS p", subQuery1, subQuery2).Find(&User{})
// SELECT * FROM (SELECT `name` FROM `users`) as u, (SELECT `name` FROM `pets`) as p

当然也可以用于 JOIN。

代码语言:javascript复制
subQuery := db.Model(&User{}).Select("id", "name").Where("age = ?", 18)
db.Model(&Pet{}).Select("pets.name", "users.name").Joins("LEFT JOIN (?) users ON pets.owner_id = users.id", subQuery)

还可以用于 IN 条件。

代码语言:javascript复制
subQuery := db.Model(&User{}).Select("id").Where("age = ?", 18)
db.Model(&Pet{}).Select("name").Where("owner_id IN (?)", subQuery)

还有很多查询方式,比如按照 map 指定查询字段以及 or 和 not 条件等,具体请参考官方文档 GORM 查询。

预加载
  • 预加载实例

GORM 允许使用 Preload 通过多个 SQL 中来直接加载关系。

代码语言:javascript复制
type User struct {
  gorm.Model
  Username string
  Orders   []Order
}

type Order struct {
  gorm.Model
  UserID uint
  Price  float64
}

// 查找 user 时预加载相关 Order
db.Preload("Orders").Find(&users)
// SELECT * FROM users;
// SELECT * FROM orders WHERE user_id IN (1,2,3,4);

db.Preload("Orders").Preload("Profile").Preload("Role").Find(&users)
// SELECT * FROM users;
// SELECT * FROM orders WHERE user_id IN (1,2,3,4); // has many
// SELECT * FROM profiles WHERE user_id IN (1,2,3,4); // has one
// SELECT * FROM roles WHERE id IN (4,5,6); // belongs to

其中一个用户可以拥有多个订单(has many),但是一个用户只能拥有一份用户资料(has one),并每个用户属于(belong to)某一个用户角色。

预加载时,需要在模型的定义中体现这种关系,比如上面示例中 User 定义中有一个订单的切片,预加载时指定切片名称。

  • 条件预加载

GORM 允许预加载时使用条件,其工作原理类似于内联条件。

代码语言:javascript复制
// Preload Orders with conditions
db.Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users)
// SELECT * FROM users;
// SELECT * FROM orders WHERE user_id IN (1,2,3,4) AND state NOT IN ('cancelled');

db.Where("state = ?", "active").Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users)
// SELECT * FROM users WHERE state = 'active';
// SELECT * FROM orders WHERE user_id IN (1,2) AND state NOT IN ('cancelled');

预加载还有很多方式,比如预加载全部、自定义预加载 SQL、嵌套预加载等,详情请见官方文档。

3.更新(Update)

更新所有字段

使用 Save 方法更新所有字段,即使是零值也会更新。

代码语言:javascript复制
// 先根据 ID  查询。
db.First(&good, 1)

// 再修改值。
good.Name = "小米"

// 最后写回。
db.Save(&user)
更新单列

注意,当使用 Model 方法且其值具有主键时,主键将用于构建条件。

代码语言:javascript复制
// 条件更新
db.Model(&User{}).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE active=true;

// 注意:user 的 ID 是 111。
db.Model(&user).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;

// 根据条件和 model 的值进行更新
db.Model(&user).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true;
更新多列

Updates 方法支持 structmap[string]interface{} 参数。当使用 struct 更新时,默认情况下,GORM 只会更新非零值的字段。

代码语言:javascript复制
// 注意:user 的 ID 是 111。

// 根据 `struct` 更新属性,只会更新非零值的字段
db.Model(&user).Updates(User{Name: "hello", Age: 18, Active: false})
// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111;

// 根据 `map` 更新属性
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;

4.删除(Delete)

删除一条记录

删除一条记录时,删除对象需要指定主键,否则会触发批量 Delete,例如:

代码语言:javascript复制
// Email 的 ID 是 10。
db.Delete(&email)
// DELETE from emails where id = 10;

// 带额外条件的删除。
db.Where("name = ?", "jinzhu").Delete(&email)
// DELETE from emails where id = 10 AND name = "jinzhu";
根据主键删除

GORM 允许通过主键(可以是复合主键)和内联条件来删除对象,它可以使用数字,也可以使用字符串。

代码语言:javascript复制
db.Delete(&User{}, 10)
// DELETE FROM users WHERE id = 10;

db.Delete(&User{}, "10")
// DELETE FROM users WHERE id = 10;

db.Delete(&users, []int{1,2,3})
// DELETE FROM users WHERE id IN (1,2,3);
批量删除

指定的值没有主键值,GORM会执行批量删除,它会删除所有匹配的记录。

代码语言:javascript复制
db.Where("email LIKE ?", "%jinzhu%").Delete(&Email{})
// DELETE from emails where email LIKE "%jinzhu%";

db.Delete(&Email{}, "email LIKE ?", "%jinzhu%")
// DELETE from emails where email LIKE "%jinzhu%";

要有效删除大量记录,请将带有主键的切片传递给 Delete 方法。

代码语言:javascript复制
var users = []User{{ID: 1}, {ID: 2}, {ID: 3}}
db.Delete(&users)
// DELETE FROM users WHERE id IN (1,2,3);

db.Delete(&users, "name LIKE ?", "%jinzhu%")
// DELETE FROM users WHERE name LIKE "%jinzhu%" AND id IN (1,2,3); 
软删除

如果您的模型包含了一个 gorm.DeletedAt 字段(gorm.Model 已经包含了该字段),它将自动获得软删除的能力!

如果您不想引入 gorm.Model,您也可以这样启用软删除特性:

代码语言:javascript复制
type User struct {
  ID      int
  Deleted gorm.DeletedAt
  Name    string
}

拥有软删除能力的模型调用 Delete 时,记录不会被数据库。但 GORM 会将 DeletedAt 置为当前时间, 并且你不能再通过普通的查询方法找到该记录。

使用 Unscoped 方法查找被软删除的数据。

代码语言:javascript复制
db.Unscoped().Where("user_name = gry").Find(&users)

要想物理删除,使用 Unscoped 方法永久删除数据。

代码语言:javascript复制
user.ID = 14
db.Unscoped().Delete(&user)

5.Upsert

GORM 提供了 Upsert 的能力,记录存在(根据主键判断)则更新,不存在则增加。

代码语言:javascript复制
func UpsertYourModel(m *YourModel) error {
	return Db.Save(m).Error
}

或者在键冲突时决定要更新的列。

代码语言:javascript复制
import "gorm.io/gorm/clause"

// Do nothing on conflict
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)

// Update columns to new value on `id` conflict
db.Clauses(clause.OnConflict{
  Columns:   []clause.Column{{Name: "id"}},
  DoUpdates: clause.AssignmentColumns([]string{"name", "age"}),
}).Create(&users)

// Update all columns to new value on conflict except primary keys and those columns having default values from sql func
db.Clauses(clause.OnConflict{
  UpdateAll: true,
}).Create(&users)

// Update all columns to new value on composite unique index conflict.
db.Clauses(clause.OnConflict{
  Columns:   []clause.Column{{Name: "student_no"}, {Name: "course_no"}},
  DoUpdates: clause.AssignmentColumns([]string{"status", "updated_at"}),
}).Create(&courseSelection)

8.小结

本文简单介绍了 ORM、GORM、以及 GORM 连接数据库,创建数据表和 CRUD 的简单操作,帮忙新手快速上手。

更多用法,请参见官方文档 GORM 指南,这里有你想要的一切。

如果您喜欢这篇文章,欢迎关注我的微信公众号“恋喵大鲤鱼”了解最新精彩内容。


参考文献

GORM 指南| GORM - GORM GORM 极速入门- 卢振千的博客 19-Gorm入门到精通- 刘清政 - 博客园 Go组件学习——gorm四步带你搞定DB增删改查 - 掘金

0 人点赞