GORM 极速入门
基础概念
ORM(Object Relational Mapping),意思是对象关系映射。数据库会提供官方客户端驱动,但是需要自己处理 SQL 和结构体的转换。使用 ORM 框架让我们避免转换,写出一些无聊的冗余代码。理论上 ORM 框架可以让我们脱离 SQL,但实际上还是需要懂 SQL 才可以使用 ORM。
我本人是非常排斥使用 ORM 框架的,原因有两点。
不自由,我不能随心所欲的控制我的数据库。
性能差,比官方客户端驱动直接编写 SQL 的效率低 3-5 倍。
不过 ORM 也有很多优点,它可以在一定程度上让新手避免慢 SQL。也有一些文章讨论过 ORM 的利弊。比如这篇:ORM_is_an_antipattern。总的来说,是否使用 ORM 框架取决于一个项目的开发人员组织结构。
老手渴望自由,新手需要规则。世界上新手多,老手就要做出一些迁就。
gorm 是一款用 Golang 开发的 orm 框架,目前已经成为在 Golang Web 开发中最流行的 orm 框架之一。
除了 gorm,你还有其他选择,比如 sqlx 和 sqlc。
连接MySQL
gorm 可以连接多种数据库,只需要不同的驱动即可。官方目前仅支持 MySQL、PostgreSQL、SQlite、SQL Server 四种数据库,不过可以通过自定义的方式接入其他数据库。
下面以连接 mySQL 为例,首先需要安装两个包。
1 | import ( |
连接代码:
1 | // MySQL 配置信息 |
声明模型
每一张表都会对应一个模型(结构体)。
比如数据库中有一张 goods 表:
1 | CREATE TABLE `gorm1`.`goods` ( |
那么就会对应如下的结构体:
1 | type Goods struct { |
约定
gorm 制定了很多约定,并按照约定大于配置的思想工作。比如会根据结构体的复数寻找表名,会使用 ID 作为主键,会根据 CreateAt、UpdateAt 和 DeletedAt 表示创建时间、更新时间和删除时间。
gorm 提供了一个 Model 结构体,可以将它嵌入到自己的结构体中,省略以上几个字段。
1 | type Model struct { |
嵌入到 goods 结构体中:
1 | type Goods struct { |
这样在每次创建不同的结构体时就可以省略创建 ID、CreatedAt、UpdatedAt、DeletedAt 这几个字段。
字段标签 tag
在创建模型时,可以给字段设置 tag 来对该字段一些属性进行定义。比如创建 Post 结构体,我们希望 Title 映射为 t,设置最大长度为 256,该字段唯一。
1 | type Post struct { |
等同于以下 SQL:
1 | CREATE TABLE `posts` (`t, size:256, unique:true` longtext) |
更多功能可参照下面这张表。
自动迁移
在数据库的表尚未初始化时,gorm 可以根据指定的结构体自动建表。
通过 db.AutoMigrate
方法根据 User 结构体,自动创建 user 表。如果表已存在,该方法不会有任何动作。
1 | type User struct { |
建表的规则会把 user 调整为复数,并自动添加 gorm.Model 中的几个字段。由于很多数据库是不区分大小写的,如果采用 camelCase 风格命名法,在迁移数据库时会遇到很多问题,所以数据库的字段命名风格都是采用 underscorecase 风格命名法,gorm 会自动帮我们转换。
等同于以下 SQL:
1 | CREATE TABLE `gorm1`.`users` ( |
创建数据 Craete Insert
使用 db.Create
方法,传入结构体的指针创建。
1 | user := User{UserName: "l", Password: "ddd"} |
等同于以下 SQL:
1 | INSERT INTO |
gorm 会自动维护 created_at、updated_ad 和 deleted_at 三个字段。
插入后返回的常用数据
下面是一些常用的插入数据:
1 | fmt.Println("ID:", user.ID) // 插入的主键 |
只插入指定字段
通过 Select 选择指定字段:
1 | user := User{UserName: "lzq", Password: "ccc"} |
等同于以下 SQL:
1 | INSERT INTO `users` (`user_name`) VALUES ('lzq') |
_需要注意:使用 select 时不会自动维护 created_at、updated_ad 和 deleted_at。
不插入指定字段
使用 Omit 方法过滤一些字段。
1 | result := db.Omit("UserName").Create(&user) |
批量插入
当需要批量插入时,传入一个切片即可。
1 | users := []User{ |
等同于以下 SQL:
1 | INSERT INTO `users` ( `created_at`, `updated_at`, `deleted_at`, `user_name`, `password` ) |
分批批量插入
在某些情况下,users 的数量可能非常大,此时可以使用 CreateInBatches
方法分批次批量插入。
假设有 6 条 user 数据,你想每次插入 2 条,这样就会执行 3 次 SQL。
1 | users := []User{ |
等同于依次执行以下 3 句 SQL:
1 | INSERT INTO `users` |
1 | INSERT INTO `users` |
1 | INSERT INTO `users` |
CreateInBatches
方法的内部是使用 for 进行切割切片的,并没有使用 goroutine。
查询数据 Read Select
查询单个对象
gorm 提供了 First、Take、Last 方法。它们都是通过 LIMIT 1
来实现的,分别是主键升序、不排序和主键降序。
1 | user := User{} |
如果没有查询到对象,会返回 ErrRecordNotFound 错误。
1 | result := db.First(&user) |
根据主键查询
在 First/Take/Last 等函数中设置第二个参数,该参数被认作是 ID。可以选择 int 或 string 类型。
1 | db.First(&user, 10) |
选择 string 类型的变量时,需要注意 SQL 注入问题。
查询多个对象(列表)
使用 Find 方法查询多个对象。
1 | users := []User{} |
返回值会映射到 users 切片上。
依然可以通过访问返回值上的 Error 和 RowsAffected 字段获取异常和影响的行号。
1 | result.Error |
设置查询条件 Where
gorm 提供了万能的 Where 方法,可以实现 =、<>、IN、LIKE、AND、>、<、BETWEEN 等方法,使用 ? 来占位。
1 | db.Where("name = ?", "l").First(&user) |
Where 快速设置条件的方法
传递 Struct、Map 和切片时,可以实现更简便的设置条件。
1 | db.Where(&User{UserName:"lzq", Password:"aaa"}).Find(&user) |
结构体和 Map 的效果几乎是相等的。
1 | SELECT * FROM `users` WHERE |
两者唯一的不同之处在于 struct 中的零值字段不会查询。比如 0、””、false。
切片是查询主键。
1 | db.Where([]int{10, 11}).Find(&user) |
等同于如下 SQL:
1 | SELECT * FROM `users` WHERE |
所有的查询,gorm 都会默认设置 tabel.deleted_at IS NULL
查询条件。
除了 Where 方法外,还有内联查询的方式,但是不推荐同时使用两种风格。
1 | db.Find(&user, "user_name = ?", "lzq") |
其他查询 Not & Or
gorm 还提供了 Not 和 Or 方法,但不推荐使用,因为 Where 同样可以实现两者的功能,记忆额外的 API 无疑会增加心智负担。
1 | db.Where("password = ?", "aaa").Not("user_name", "l").Or("id > ?", 10).Find(&users) |
等同于如下 SQL:
1 | SELECT * FROM `users` WHERE (( PASSWORD = 'aaa' ) |
选取特定字段 Select
使用 Select 方法。
1 | db.Select("password").Where(&User{UserName:"lzq"}).Find(&user) |
等同于以下 SQL:
1 | SELECT `password` FROM `users` WHERE |
其他操作
排序 Order
1 | db.Order("user_name desc, password").Find(&users) |
等同于以下 SQL:
1 | SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL |
分页 Limit Offset
Limit 和 Offset 可以单独使用,也可以组合使用。
1 | db.Limit(3).Find(&users) |
等同于以下 SQL:
1 | SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL |
分组 Group Having
根据 username 统计用户名的重复。
1 | result := []map[string]interface{}{} |
等同于以下 SQL:
1 | SELECT |
去重 Distinct
1 | result := []string{} |
等同于以下 SQL:
1 | SELECT DISTINCT user_name FROM users |
连表 Join
在业务中不太建议使用 Join,而是使用多条查询来做多表关联。
更新数据 Update
更新所有字段
使用 Save 方法更新所有字段,即使是零值也会更新。
1 | db.First(&user) |
等同于以下 SQL:
1 | UPDATE `users` |
更新单列
使用 Model 和 Update 方法更新单列。
可以使用结构体作为选取条件,仅选择 ID。
1 | user.ID = 12 |
等同于以下SQL:
1 | UPDATE `users` |
也可以在 Model 中设置空结构体,使用 Where 方法自己选取条件。
1 | db.Model(&User{}).Where("user_name", "gry").Update("user_name", "gry2") |
等同于以下 SQL:
1 | UPDATE `users` |
还可以组合选取条件。
1 | user.ID = 20 |
等同于以下 SQL:
1 | UPDATE `users` |
更新多列
使用 Updates 方法进行更新多列。支持 struct 和 map 更新。当更新条件是 struct 时,零值不会更新,如果确保某列必定更新,使用 Select 选择该列。
更新选定字段 Select Omit
使用 Select 和 Omit 方法。
批量更新
如果在 Model 中没有设置 ID,默认是批量更新。
删除数据 Delete
删除单条
使用 Delete 方法删除单条数据。但需要指定 ID,不然会批量删除。
1 | user.ID = 20 |
等同于以下 SQL:
1 | UPDATE `users` |
设置删除条件
使用 Where 方法进行设置条件。
1 | db.Where("user_name", "lzq").Delete(&user) |
等同于以下 SQL:
1 | UPDATE `users` |
根据主键删除
第二个参数可以是 int、string。使用 string 时需要注意 SQL 注入。
1 | db.Delete(&User{}, 20) |
等同于以下 SQL:
1 | UPDATE `users` |
也可以使用切片[]int、[]string
进行根据 ID 批量删除。
1 | db.Delete(&User{}, []string{"21", "22", "23"}) |
等同于以下 SQL:
1 | UPDATE `users` |
批量删除
空结构体就是批量删除。
软删除(逻辑删除)
如果结构体包含 gorm.DeletedAt 字段,会自动获取软删除的能力。在调用所有的 Delete 方法时,会自动变为 update 语句。
1 | UPDATE users SET deleted_at="2020-12-04 09:40" WHERE id = 31; |
在查询时会自动忽略软删除的数据。
1 | SELECT * FROM users WHERE user_name = 'gry' AND deleted_at IS NULL; |
查询软删除的数据
使用 Unscoped 方法查找被软删除的数据。
1 | db.Unscoped().Where("user_name = gry").Find(&users) |
永久删除(硬删除 物理删除)
使用 Unscoped 方法永久删除数据。
1 | user.ID = 14 |
原生 SQL
除了上面的封装方法外,gorm 还提供了执行原生 SQL 的能力。
执行 SQL 并将结果映射到变量上
使用 Raw 方法配合 Scan 方法。可以查询单条数据扫描并映射到结构体或 map 上。
1 | db.Raw("SELECT id, record_id, user_name, password FROM users WHERE id = ?", 25).Scan(&user) |
也可以映射到其他类型上:
1 | var userCount int |
如果返回结果和传入的映射变量类型不匹配,那么变量的值不会有变化。
只执行 SQL 不使用结果
使用 Exec 方法执行 SQL。
1 | db.Exec("UPDATE users SET password=? WHERE id = ?", "abcdefg", 22) |
钩子 Hook
gorm 提供了 Hook 功能。可以在创建、查询、更新和删除之前和之后自动执行某些逻辑。
创建
gorm 提供了 4 个创建钩子,BeforeCreate、AfterCreate 和 BeforeSave、AfterSave。
假设现在需要添加一个 RecordID,并且在每次创建时生成一个 16 位的 uuid。
1 | type User struct { |
除此之外,还希望在存储之前打印生成的 uuid,在存储之后打印创建后的 id。
实现方式就是给模型结构体 User 添加 BeforeCreate 和 AfterCreate 两个方法。
1 | func (u *User) BeforeCreate(tx *gorm.DB) error { |
更新
更新的 Hook 是 BeforeUpdate、AfterUpdate 和 BeforeSave、AfterSave,用法与创建一致。
查询
查询的 Hook 是 AfterFind,用法与创建一致。
删除
删除的 Hook 是 BeforeDelete 和 AfterDelete,用法与创建一致。
除了查询的 Hook 外,其他 Hook 都是在事务上运行的,一旦在函数中 return error 时,就会触发事务回滚。
事务
事务保证了事务一致性,但会降低一些性能。gorm 的创建、修改和删除操作都在事务中执行。
如果不需要可以在初始化时禁用事务,可以提高 30% 左右的性能。
全局关闭事务
1 | db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ |
会话级别关闭事务
1 | tx := db.Session(&Session{SkipDefaultTransaction: true}) |
在事务中执行 SQL
假设现在需要添加一个 company 表存储公司信息,并创建一个 company_users 表用于关联用户和公司的信息。
1 | // 创建结构体 |
日志
gorm 默认实现了一个 Logger,它仅输出慢 SQL。
1 | newLogger := logger.New( |
日志的级别可以配置,可以设置 Silent
、Error
、Warn
、Info
。
全局模式开启
1 | db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ |
会话模式开启
1 | tx := db.Session(&Session{Logger: newLogger}) |
自定义 Logger
gorm 提供了一个 Interface 接口,可以通过实现这个接口来自定义 Logger。
1 | type Interface interface { |