GO语言之Gorm
什么是ORM
对象(Go结构体)-关系(数据库)-映射
三层对应:
- 数据表-结构体
- 数据行-结构体实例
- 字段-结构体字段
安装与链接
1 | go get -u github.com/jinzhu/gorm |
连接不同的数据库都需要导入对应数据的驱动程序,GORM
已经贴心的为我们包装了一些驱动程序,只需要按如下方式导入需要的数据库驱动即可:
1 | import _ "github.com/jinzhu/gorm/dialects/mysql" |
例如:
Mysql
1 | import ( |
Postgresql
1 | import ( |
基本用例
docker启动Mysql
可以使用一下命令快速运行一个MySQL8.0.19实例,当然前提是你要有docker环境…
在本地的13306
端口运行一个名为mysql8019
,root用户名密码为root1234
的MySQL容器环境:
1 | docker run --name mysql8019 -p 13306:3306 -e MYSQL_ROOT_PASSWORD=root1234 -d mysql:8.0.19 |
在另外启动一个MySQL Client
连接上面的MySQL环境,密码为上一步指定的密码root1234
:
1 | docker run -it --network host --rm mysql mysql -h127.0.0.1 -P13306 --default-character-set=utf |
创建数据库
1 | CREATE DATABASE db1; |
GORM操作MySQL
使用GORM连接上面的db1
进行创建、查询、更新、删除操作。
1 | package main |
GORM Model定义:定义模型
为了方便模型定义,GORM内置了一个gorm.Model
结构体。gorm.Model
是一个包含了ID
, CreatedAt
, UpdatedAt
, DeletedAt
四个字段的Golang结构体。
1 | // gorm.Model 定义 |
可以将它嵌入到你自己的模型中:1
2
3
4
5
6// 将 `ID`, `CreatedAt`, `UpdatedAt`, `DeletedAt`字段注入到`User`模型中
type User struct {
gorm.Model
Name string
}
也可以完全自己定义模型:1
2
3
4
5
6// 不使用gorm.Model,自行定义模型
type User struct {
ID int
Name string
}
模型定义示例
1 | type User struct { |
支持的结构体标记(Struct tags)
结构体标记(Tag) | 描述 |
---|---|
Column | 指定列名 |
Type | 指定列数据类型 |
Size | 指定列大小, 默认值255 |
PRIMARY_KEY | 将列指定为主键 |
UNIQUE | 将列指定为唯一 |
DEFAULT | 指定列默认值 |
PRECISION | 指定列精度 |
NOT NULL | 将列指定为非 NULL |
AUTO_INCREMENT | 指定列是否为自增类型 |
INDEX | 创建具有或不带名称的索引, 如果多个索引同名则创建复合索引 |
UNIQUE_INDEX | 和 INDEX 类似,只不过创建的是唯一索引 |
EMBEDDED | 将结构设置为嵌入 |
EMBEDDED_PREFIX | 设置嵌入结构的前缀 |
- | 忽略此字段 |
关联相关标记(tags)
结构体标记(Tag) | 描述 |
---|---|
MANY2MANY | 指定连接表 |
FOREIGNKEY | 设置外键 |
ASSOCIATION_FOREIGNKEY | 设置关联外键 |
POLYMORPHIC | 指定多态类型 |
POLYMORPHIC_VALUE | 指定多态值 |
JOINTABLE_FOREIGNKEY | 指定连接表的外键 |
ASSOCIATION_JOINTABLE_FOREIGNKEY | 指定连接表的关联外键 |
SAVE_ASSOCIATIONS | 是否自动完成 save 的相关操作 |
ASSOCIATION_AUTOUPDATE | 是否自动完成 update 的相关操作 |
ASSOCIATION_AUTOCREATE | 是否自动完成 create 的相关操作 |
ASSOCIATION_SAVE_REFERENCE | 是否自动完成引用的 save 的相关操作 |
PRELOAD | 是否自动完成预加载的相关操作 |
主键、表名、列名的约定
主键(Primary Key)
GORM 默认会使用名为ID的字段作为表的主键。
1 | type User struct { |
表名(Table Name)
表名默认就是结构体名称的复数,例如:1
2
3
4
5
6
7type User struct {} // 默认表名是 `users`
// 禁用默认表名的复数形式,如果置为 true,则 `User` 的默认表名是 `user`
db.SingularTable(true)
也可以通过Table()
指定表名:
写法一:1
2
3
4
5
6
7
8
9
10
11
12
13// 将 User 的表名设置为 `profiles`
// 可以用TableName函数,名字必须写成TableName,无需其他配置(但推荐下一种)
func (User) TableName() string {
return "profiles"
}
func (u User) TableName() string {
if u.Role == "admin" {
return "admin_users"
} else {
return "users"
}
}
写法2:1
2
3
4
5
6
7
8
9
10// 使用User结构体创建名为`deleted_users`的表
db.Table("deleted_users").CreateTable(&User{})
var deleted_users []User
db.Table("deleted_users").Find(&deleted_users)
//// SELECT * FROM deleted_users;
db.Table("deleted_users").Where("name = ?", "jinzhu").Delete()
//// DELETE FROM deleted_users WHERE name = 'jinzhu';
GORM还支持更改默认表名称规则:
1 | gorm.DefaultTableNameHandler = func (db *gorm.DB, defaultTableName string) string { |
列名(Column Name)
列名由字段名称进行下划线分割来生成
1 | type User struct { |
可以使用结构体tag指定列名:
1 | type Animal struct { |
时间戳跟踪
CreatedAt
如果模型有 CreatedAt
字段,该字段的值将会是初次创建记录的时间。
1 | db.Create(&user) // `CreatedAt`将会是当前时间 |
UpdatedAt
如果模型有UpdatedAt
字段,该字段的值将会是每次更新记录的时间。
1 | db.Save(&user) // `UpdatedAt`将会是当前时间 |
DeletedAt
如果模型有DeletedAt
字段,调用Delete
删除该记录时,将会设置DeletedAt
字段为当前时间,而不是直接将记录从数据库中删除。
GORM CRUD增删改查
查
普通查询
单个对象
GORM 提供了 First
、Take
、Last
方法,以便从数据库中检索单个对象。当查询数据库时它添加了 LIMIT 1
条件,且没有找到记录时,它会返回 ErrRecordNotFound
错误
1 | // 按主键升序获取第一条记录(主键升序) |
如果你想避免ErrRecordNotFound
错误,你可以使用Find
,比如db.Limit(1).Find(&user)
,Find
方法可以接受struct和slice的数据。
对单个对象使用
Find
而不带limit,db.Find(&user)
将会查询整个表并且只返回第一个对象,这是性能不高并且不确定的。
按照主键检索
1 | db.First(&user, 10) |
如果主键是字符串(例如像uuid),查询将被写成如下:
1 | db.First(&user, "id = ?", "1b74413f-f3b8-409f-ac47-e8c062e3472a") |
当目标对象有一个主键值时,将使用主键构建查询条件,例如:
1 | var user = User{ID: 10} |
全部对象
1 | // Get all records |
条件查询
string条件
1 | // Get first matched record |
如果对象设置了主键,条件查询将 不会 覆盖主键的值,而是用 And 连接条件。 例如:
1 | var user = User{ID: 10} |
这个查询将会给出record not found
错误 所以,在你想要使用例如 user
这样的变量从数据库中获取新值前,需要将例如 id
这样的主键设置为nil。
Struct和Map条件
1 | // Struct |
注意:当使用struct进行查询时,GORM将只使用非零字段进行查询,这意味着如果字段的值为0、’’、false或其他零值,则不会用于构建查询条件
1 | db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users) |
要在查询条件中包括零值,可以使用映射Map,该映射将包括所有键值作为查询条件,例如:
1 | db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users) |
也可以通过将相关字段名或dbname传递到Where()来指定要在查询条件中使用的结构中的特定值,例如:
1 | db.Where(&User{Name: "jinzhu"}, "name", "Age").Find(&users) |
内联条件(where)与逻辑(or not)
查询条件可以以与Where类似的方式内联到First和Find等方法中。
内联条件与多个立即执行方法一起使用时, 内联条件不会传递给后面的立即执行方法。
立即执行方法是指那些会立即生成SQL语句并发送到数据库的方法, 他们一般是CRUD方法,比如:Create, First, Find, Take, Save, UpdateXXX, Delete, Scan, Row, Rows…
1 | // Get by primary key if it were a non-integer type |
Not 条件
1 | db.Not("name = ?", "jinzhu").First(&user) |
Or条件
1 | db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users) |
多条件解耦Scpoes
基于它,你可以抽取一些通用逻辑,写出更多可重用的函数库。
1 | func AmountGreaterThan1000(db *gorm.DB) *gorm.DB { |
查询时初始化
就是查询,如果没有查到或者怎么初始化一条记录
FirstOrInit
获取匹配的第一条记录,否则根据给定的条件初始化一个新的对象 (仅支持 struct 和 map 条件),注意,这里是初始化对象,没有写入数据库
1 | // 未找到 |
可以使用attr指定初始化对象的参数,注意,这里是初始化对象,没有写入数据库
1 | // 未找到 |
使用assign,不管记录是否找到,都将参数赋值给 struct. 注意,这里是初始化对象,没有写入数据库
1 | // 未找到 |
FirstOrCreate
这里就写入数据库了
1 | // 未找到 |
特定列、排序偏移、组
选择特定字段(列)
使用select
1 | db.Select("name", "age").Find(&users) |
排序& Limit & Offset&distinct
排序使用order
1 | db.Order("age desc, name").Find(&users) |
Limit指定要检索的最大记录数Offset指定开始返回记录之前要跳过的记录数
1 | db.Limit(3).Find(&users) |
distinct1
2db.Distinct("name", "age").Order("name, age desc").Find(&results)
Group By & Having
1 | type result struct { |
左连接右连接
1 | db.Joins("Company").Find(&users) |
在查询前加入debug语句可以输出对应得sql,即例如
db.Debug.Find(......
创建
首先定义模型:
1 | type User struct { |
使用使用NewRecord()
查询主键是否存在,主键为空使用Create()
创建记录:
1 | user := User{Name: "q1mi", Age: 18} |
默认值
可以通过 tag 定义字段的默认值,比如:
1 | type User struct { |
注意: 通过tag定义字段的默认值,在创建记录时候生成的 SQL 语句会排除没有值或值为 零值 的字段。 在将记录插入到数据库后,Gorm会从数据库加载那些字段的默认值。
举个例子:
1 | var user = User{Name: "", Age: 99} |
上面代码实际执行的SQL语句是INSERT INTO users("age") values('99');
,排除了零值字段Name
,而在数据库中这一条数据会使用设置的默认值小王子
作为Name字段的值。
注意: 所有字段的零值, 比如0
, ""
,false
或者其它零值
,都不会保存到数据库内,但会使用他们的默认值。 如果你想避免这种情况,可以考虑使用指针或实现 Scanner/Valuer
接口,比如:
使用指针方式实现零值存入数据库
1 | // 使用指针 |
使用Scanner/Valuer接口方式实现零值存入数据库
1 | // 使用 Scanner/Valuer |
改
更新所有字段
Save()
默认会更新该对象的所有字段,即使你没有赋值。
1 | db.First(&user) |
更新修改字段
如果你只希望更新指定字段,可以使用Update
或者Updates
。当然 updated_at等钩子字段仍然会更新
1 | // 更新单个属性,如果它有变化 |
更新选定字段
如果你想更新或忽略某些字段,你可以使用 Select
,Omit
1 | db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false}) |
关于Hook
上面的更新操作会自动运行 model 的 BeforeUpdate
, AfterUpdate
方法,更新 UpdatedAt
时间戳, 在更新时保存其 Associations
, 如果你不想调用这些方法,你可以使用 UpdateColumn
, UpdateColumns
1 | // 更新单个属性,类似于 `Update` |
但是注意!
批量跟新的时候,狗钩子函数不会执行1
2
3
4
5
6db.Table("users").Where("id IN (?)", []int{10, 11}).Updates(map[string]interface{}{"name": "hello", "age": 18})
//// UPDATE users SET name='hello', age=18 WHERE id IN (10, 11);
// 使用 struct 更新时,只会更新非零值字段,若想更新所有字段,请使用map[string]interface{}
db.Model(User{}).Updates(User{Name: "hello", Age: 18})
//// UPDATE users SET name='hello', age=18;
如何修改Hook,以在更新(save)之前,将密码加密为例子。1
2
3
4
5
6func (user *User) BeforeSave(scope *gorm.Scope) (err error) {
if pw, err := bcrypt.GenerateFromPassword(user.Password, 0); err == nil {
scope.SetColumn("EncryptedPassword", pw)
}
}
获取更新行数
1 | // 使用 `RowsAffected` 获取更新记录总数 |
更新表达式(计算)
1 | db.Model(&user).Update("age", gorm.Expr("age * ? + ?", 2, 100)) |
删
删除记录
警告 删除记录时,请确保主键字段有值,GORM 会通过主键去删除记录,如果主键为空,GORM 会删除该 model 的所有记录。
使用使用NewRecord()
查询主键是否存在:
1 | user := User{Name: "q1mi", Age: 18} |
1 | // 删除现有记录 |
根据主键删除1
2
3
4
5
6
7
8
9db.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);
批量删除
删除全部匹配的记录
1 | db.Where("email LIKE ?", "%jinzhu%").Delete(Email{}) |
软删除
如果一个 model 有 DeletedAt
字段,他将自动获得软删除的功能! 当调用 Delete
方法时, 记录不会真正的从数据库中被删除, 只会将DeletedAt
字段的值会被设置为当前时间
1 | db.Delete(&user) |
物理删除
1 | // Unscoped 方法可以物理删除记录 |
使用钩子函数实现权限控制
1 | func (u *User) BeforeDelete(tx *gorm.DB) (err error) { |