本文同步发布在个人博客。
前言:何为ORM
要说ORM是何物,我们得先从面向对象谈起。
在面向对象的编程思想中贯彻着一句话:"一切皆对象。"
而在数据库那边,以关系型数据库来说吧,关系型数据库则讲究:"一切实体都有关系。"
你发现了什么?关系是不是也能用对象的思想去描述?
举个例子,假如有一张表:
sql
CREATE TABLE `users` (
`id` integer PRIMARY KEY,
`username` varchar(255),
`role` varchar(255),
`created_at` timestamp
);
在这张名为users
的表内有着4个字段:id
,username
,role
和created_at
。
假如我们将它用Go的结构体去描述呢?
go
type Users struct {
Id int
Username string
Role string
CreatedAt time.Time
}
自此,我们便完成了一个从表到结构体的映射。
而ORM做的便是这样一种事情,从表映射到对象。ORM 就是通过实例对象的语法,完成关系型数据库的操作的技术,是"对象-关系映射"(Object/Relational Mapping) 的缩写。

一般来说,ORM会完成以下的映射关系:
- 数据库的表(table) --> 类(class)
- 记录(record,行数据)--> 对象(object)
- 字段(field)--> 对象的属性(attribute)
当然由于Go并没有class
这个概念,因此在Go中ORM会完成以下的映射关系:
- 数据库的表(table) --> 结构体(struct)
- 记录(record,行数据)--> 结构体的实例化(object)
- 字段(field)--> 结构体的字段(fields)
ORM有着下面的优点:
- 弱化SQL原生语句的要求,对于新手来说简单操作易上手;
- 将SQL抽象成结构体和对象,易于理解;
- 一定程度上增加了开发效率。
但也有一定的缺点:
- 增加了一层中间环节,同时使用了反射,牺牲了一定的性能;
- 牺牲了灵活性,弱化了SQL的能力;
- 牺牲了一些原生功能。
Go 的ORM框架:GORM
在Go中也有着较为成熟的ORM框架:GORM,官网对它的特性简单枚举了一些:
- 全功能 ORM
- 关联 (拥有一个,拥有多个,属于,多对多,多态,单表继承)
- Create,Save,Update,Delete,Find 中钩子方法
- 支持 Preload、Joins 的预加载
- 事务,嵌套事务,Save Point,Rollback To to Saved Point
- Context、预编译模式、DryRun 模式
- 批量插入,FindInBatches,Find/Create with Map,使用 SQL 表达式、Context Valuer 进行 CRUD
- SQL 构建器,Upsert,锁,Optimizer/Index/Comment Hint,命名参数,子查询
- 复合主键,索引,约束
- 自动迁移
- 自定义 Logger
- 灵活的可扩展插件 API:Database Resolver(多数据库,读写分离)、Prometheus...
- 每个特性都经过了测试的重重考验
- 开发者友好
让我们结合一下MySQL简单上手一下GORM吧。
前期准备
由于笔者不喜欢物理机搞MySQL,所以此处使用Docker开一个MySQL的容器。
笔者已经安装好了Docker 和 MySQL 客户端,现在先拉取镜像。前往MySQL 的官方镜像:

在右侧已经写好了拉取命令,复制,在本地终端执行一下:
shell
$ docker pull mysql
Using default tag: latest
latest: Pulling from library/mysql
49bb46380f8c: Pull complete
aab3066bbf8f: Pull complete
d6eef8c26cf9: Pull complete
0e908b1dcba2: Pull complete
480c3912a2fd: Pull complete
264c20cd4449: Pull complete
d7afa4443f21: Pull complete
d32c26cb271e: Pull complete
f1f84a2204cb: Pull complete
9a41fcc5b508: Pull complete
7b8402026abb: Pull complete
Digest: sha256:51c4dc55d3abf4517a5a652794d1f0adb2f2ed1d1bedc847d6132d91cdb2ebbf
Status: Downloaded newer image for mysql:latest
docker.io/library/mysql:latest
拉取完镜像后我们启动镜像:
shell
$ docker run --name mysql -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql:latest
824cf9edeaaaf35aeaf58aed5a79c86fa819fd2693f063367b4a5a3404fa8aee
其中:
--name
是容器名字;-d
代表在后台运行;-p 3306:3306
代表将容器的3306端口映射到主机的3306端口;-e
是环境变量,这里有一个环境变量MYSQL_ROOT_PASSWORD
是指root用户的默认密码;mysql:latest
代表启动名为mysql
并且标签为latest
的镜像。
此时我们拿本地的MySQL客户端尝试一下:
shell
$ mysql -uroot -p123456
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 9
Server version: 8.0.34 MySQL Community Server - GPL
Copyright (c) 2000, 2023, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>
成功连接。
为了后续的操作,我们在此建立一个test
的数据库:
sql
mysql> create database test;
Query OK, 1 row affected (0.10 sec)
初始化Go项目
使用go get -u gorm.io/gorm
为项目导入GORM框架:
shell
$ go get -u gorm.io/gorm
go: added github.com/jinzhu/inflection v1.0.0
go: added github.com/jinzhu/now v1.1.5
go: added gorm.io/gorm v1.25.2
初始化连接
由于我们使用的是MySQL,因此我们先要下载驱动:
shell
$ go get -u "gorm.io/driver/mysql"
go: added github.com/go-sql-driver/mysql v1.7.1
go: added gorm.io/driver/mysql v1.5.1
下载完驱动后我们便可以连接数据库了,新建一个main.go
:
go
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
const (
user = "root"
password = "123456"
addr = "127.0.0.1:3306"
db = "test"
)
func main() {
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
user, password, addr, db)
//db 便是我们的数据库对象
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
fmt.Println("连接失败")
}
_ = db
}
可以看到,GORM 提供了gorm.Open
这个方法让我们去建立一个数据库的连接,而在建立连接的过程中我们也可以传递一些配置来配置连接,此处我们传入的是一个空结构体,因此我们没有传入任何配置。
go
func Open(dialector Dialector, opts ...Option) (db *DB, err error)
建立映射
前面我们已经说过了,ORM框架建立了记录------结构体的一个映射,因此我们此时就要先建立一个结构体。
例如这里我们新建一个user
的结构体:
go
type User struct {
gorm.Model
Name string
Age string
}
此处的gorm.Model
是框架自带的一个结构体,提供了常见的一些字段:
go
type Model struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt DeletedAt `gorm:"index"`
}
标签
GORM框架提供了各种各样的标签来为结构体丰富自带的内容,所有的标签类型如下:
标签名 | 说明 |
---|---|
column | 指定表的列名 |
type | 列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes 并且可以和其他标签一起使用,例如:not null 、size , autoIncrement ... 像 varbinary(8) 这样指定数据库数据类型也是支持的。在使用指定数据库数据类型时,它需要是完整的数据库数据类型,如:MEDIUMINT UNSIGNED not NULL AUTO_INCREMENT |
serializer | 指定将数据序列化或反序列化到数据库中的序列化器, 例如: serializer:json/gob/unixtime |
size | 定义列数据类型的大小或长度,例如 size: 256 |
primaryKey | 将列定义为主键 |
unique | 将列定义为唯一键 |
default | 定义列的默认值 |
precision | 指定列的精度 |
scale | 指定列大小 |
not null | 指定列为 NOT NULL |
autoIncrement | 指定列为自动增长 |
autoIncrementIncrement | 自动步长,控制连续记录之间的间隔 |
embedded | 嵌套字段 |
embeddedPrefix | 嵌入字段的列名前缀 |
autoCreateTime | 创建时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano /milli 来追踪纳秒、毫秒时间戳,例如:autoCreateTime:nano |
autoUpdateTime | 创建/更新时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano /milli 来追踪纳秒、毫秒时间戳,例如:autoUpdateTime:milli |
index | 根据参数创建索引,多个字段使用相同的名称则创建复合索引,查看 索引 获取详情 |
uniqueIndex | 与 index 相同,但创建的是唯一索引 |
check | 创建检查约束,例如 check:age > 13 ,查看 约束 获取详情 |
<- | 设置字段写入的权限, <-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限 |
-> | 设置字段读的权限,->:false 无读权限 |
- | 忽略该字段,- 表示无读写,-:migration 表示无迁移权限,-:all 表示无读写迁移权限 |
comment | 迁移时为字段添加注释 |
自动迁移
当我们的结构体更新了,但是表没有更新?
或者当我们写好了结构体但是没有创建表?
我们可以通过GORM提供的自动迁移功能来解决上面的问题。
在GORM中,我们可以按照这样的方式来自动迁移:
go
type User struct {
gorm.Model
Name string
Age string
}
func main() {
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
user, password, addr, db)
//db 便是我们的数据库对象
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
fmt.Println("连接失败")
}
err = db.AutoMigrate(&User{})
if err != nil {
fmt.Println("自动迁移失败")
}
}
执行程序,然后来看一下数据库此时的情况:
sql
mysql> SHOW FULL TABLES;
+----------------+------------+
| Tables_in_test | Table_type |
+----------------+------------+
| users | BASE TABLE |
+----------------+------------+
1 row in set (0.01 sec)
mysql> DESCRIBE users;
+------------+-----------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+-----------------+------+-----+---------+----------------+
| id | bigint unsigned | NO | PRI | NULL | auto_increment |
| created_at | datetime(3) | YES | | NULL | |
| updated_at | datetime(3) | YES | | NULL | |
| deleted_at | datetime(3) | YES | MUL | NULL | |
| name | longtext | YES | | NULL | |
| age | longtext | YES | | NULL | |
+------------+-----------------+------+-----+---------+----------------+
6 rows in set (0.02 sec)
假如此时我们更改一下user
结构体:
go
type User struct {
gorm.Model
Name string
Age string
NickName string
}
再次运行后我们查看一下表结构:
sql
mysql> DESCRIBE users;
+------------+-----------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+-----------------+------+-----+---------+----------------+
| id | bigint unsigned | NO | PRI | NULL | auto_increment |
| created_at | datetime(3) | YES | | NULL | |
| updated_at | datetime(3) | YES | | NULL | |
| deleted_at | datetime(3) | YES | MUL | NULL | |
| name | longtext | YES | | NULL | |
| age | longtext | YES | | NULL | |
| nick_name | longtext | YES | | NULL | |
+------------+-----------------+------+-----+---------+----------------+
7 rows in set (0.00 sec)
CRUD
当我们建立好连接后就要开始增删改查了。
Create------增
在GORM中,框架提供了Create()
方法来新建一条记录:
go
user := User{
Name: "Nick",
Age: "19",
NickName: "AAA",
}
result := db.Create(&user)
//如果想要判断创建结果是否成功,只需要调用result.Error即可
if result.Error != nil {
fmt.Println("创建失败")
}
//返回记录的ID
fmt.Println("Id = ", user.ID)
//返回插入记录的条数
fmt.Println("Rows = ", result.RowsAffected)
运行后我们此时查看表:
sql
mysql> SELECT * FROM users;
+----+-------------------------+-------------------------+------------+------+------+-----------+
| id | created_at | updated_at | deleted_at | name | age | nick_name |
+----+-------------------------+-------------------------+------------+------+------+-----------+
| 1 | 2023-07-28 19:29:18.168 | 2023-07-28 19:29:18.168 | NULL | Nick | 19 | AAA |
+----+-------------------------+-------------------------+------------+------+------+-----------+
1 row in set (0.00 sec)
当然你也可以通过传入一个切片的方式来批量增加记录:
go
users := []*User{
&User{
Name: "A",
Age: "15",
NickName: "a",
},
&User{
Name: "B",
Age: "16",
NickName: "b",
},
}
result := db.Create(&users)
//如果想要判断创建结果是否成功,只需要调用result.Error即可
if result.Error != nil {
fmt.Println("创建失败")
}
//返回插入记录的条数
fmt.Println("Rows = ", result.RowsAffected)
运行后查看原表:
go
mysql> SELECT * FROM users;
+----+-------------------------+-------------------------+------------+------+------+-----------+
| id | created_at | updated_at | deleted_at | name | age | nick_name |
+----+-------------------------+-------------------------+------------+------+------+-----------+
| 1 | 2023-07-28 19:29:18.168 | 2023-07-28 19:29:18.168 | NULL | Nick | 19 | AAA |
| 2 | 2023-07-28 19:33:38.961 | 2023-07-28 19:33:38.961 | NULL | A | 15 | a |
| 3 | 2023-07-28 19:33:38.961 | 2023-07-28 19:33:38.961 | NULL | B | 16 | b |
+----+-------------------------+-------------------------+------------+------+------+-----------+
3 rows in set (0.00 sec)
Read------查
GORM 提供了 First
、Take
、Last
方法,以便从数据库中检索单个对象。当查询数据库时它添加了 LIMIT 1
条件,且没有找到记录时,它会返回 ErrRecordNotFound
错误
go
user := User{}
// 获取第一条记录(主键升序)
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;
fmt.Println(user)
// 获取一条记录,没有指定排序字段
db.Take(&user)
// SELECT * FROM users LIMIT 1;
fmt.Println(user)
// 获取最后一条记录(主键降序)
db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;
fmt.Println(user)
result := db.First(&user)
fmt.Println(result.RowsAffected) // 返回找到的记录数
if result.Error != nil { // returns error or nil
fmt.Println(result.Error)
}
// 检查 ErrRecordNotFound 错误
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
fmt.Println("找不到记录")
}
WHERE
在GORM中,也提供了和SQL类似的WHERE
方法来过滤我们的查询结果。并且在WHERE内的查询语句是和SQL的语法基本一致的。
go
// Get first matched record
db.Where("name = ?", "Nick").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;
// Get all matched records
db.Where("name <> ?", "A").Find(&users)
// SELECT * FROM users WHERE name <> 'jinzhu';
// IN
db.Where("name IN ?", []string{"A", "B"}).Find(&users)
// SELECT * FROM users WHERE name IN ('jinzhu','jinzhu 2');
// LIKE
db.Where("name LIKE ?", "%Ni%").Find(&users)
// SELECT * FROM users WHERE name LIKE '%jin%';
// AND
db.Where("name = ? AND age >= ?", "Nick", "10").Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;
// BETWEEN
db.Where("age BETWEEN ? AND ?", "5", "15").Find(&users)
// SELECT * FROM users WHERE age BETWEEN '5' AND '15';
Update------改
当我们通过查询方法拿到记录后,我们可以更改这个结构体来更改记录,而后使用Save
方法来更新字段:
go
user := User{}
db.First(&user)
//拿到记录后我们直接更改记录即可
user.Name = "Luna"
db.Save(&user)
//有一个特性,如果你传入的结构体内没有包含主键的话,那么此时Save会调用Create方法
userWithoutId := User{
Name: "123",
}
//这里便是Create方法,相当于SQL的INSERT
db.Save(&userWithoutId)
userWithId := User{Model: gorm.Model{ID: 1}, Name: "s"}
//这里便是Save方法,相当于SQL的UPDATE
db.Save(&userWithId)
DELETE------删
首先确定两个概念:
- 软删除:通过特定的标记方式在查询的时候将此记录过滤掉。虽然数据在界面上已经看不见,但是数据库还是存在的。
- 硬删除:传统的物理删除,直接将该记录从数据库中删除。
为什么引入这两个概念,这里留给读者自行思考。
在GORM中也有着删除的方法:Delete
:
go
user := User{
Age: "16",
}
db.Delete(&user)
// DELETE from users where age = '16';
db.Where("name = ?", "s").Delete(&user)
// DELETE from users where name = 's' and age = '16';
注意的时,由于我们没有指定主键,因此GORM会删除一切符合筛选条件的记录。
如果我们根据主键删除:
go
db.Delete(&user, 1)
// DELETE from users where id = 1 and age = '16';
db.Delete(&user, []int{1, 2, 3})
// DELETE from users where id in (1,2,3) and age = '16';
软删除和硬删除
GORM中,当你的结构体携带有gorm.DeletedAt
字段时,此时GORM将不会直接删除记录,而是会将这个字段的值更新为当前时间,再使用GORM的查询时一般是无法查询到该记录的。但你可以使用Unscoped
来查询到被软删除的记录。
go
var users []User
db.Unscoped().Where("age = '16'").Find(&users)
// SELECT * FROM users WHERE age = '16';
你也可以使用 Unscoped
来永久删除匹配的记录
go
db.Unscoped().Delete(&user)
// DELETE FROM users WHERE age = '16';
总结
GORM 作为Go 比较成熟的ORM 框架,它的业务能力是有目共睹的。对于新手而言,若要快速学习与SQL的交互,从GORM入手也许是一个不错的选择。
同时GORM还有着更多好玩的特性,下篇文章笔者将尝试讲解将Gin和Gorm结合起来的实际应用。
本文示例代码已放在仓库内