本文同步发布在个人博客。
前言:何为ORM
要说ORM是何物,咱们得先从面向目标谈起。
在面向目标的编程思想中遵循着一句话:“全部皆目标。”
而在数据库那边,以联系型数据库来说吧,联系型数据库则讲究:“全部实体都有联系。”
你发现了什么?联系是不是也能用目标的思想去描绘?
举个比如,假如有一张表:
CREATE TABLE `users` (
`id` integer PRIMARY KEY,
`username` varchar(255),
`role` varchar(255),
`created_at` timestamp
);
在这张名为users
的表内有着4个字段:id
,username
,role
和created_at
。
假如咱们将它用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 的官方镜像:
在右侧现已写好了拉取命令,仿制,在本地终端履行一下:
$ 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
拉取完镜像后咱们发动镜像:
$ 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客户端尝试一下:
$ 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
的数据库:
mysql> create database test;
Query OK, 1 row affected (0.10 sec)
初始化Go项目
运用go get -u gorm.io/gorm
为项目导入GORM结构:
$ 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,因此咱们先要下载驱动:
$ 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
:
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
这个办法让咱们去树立一个数据库的衔接,而在树立衔接的过程中咱们也能够传递一些装备来装备衔接,此处咱们传入的是一个空结构体,因此咱们没有传入任何装备。
func Open(dialector Dialector, opts ...Option) (db *DB, err error)
树立映射
前面咱们现已说过了,ORM结构树立了记载——结构体的一个映射,因此咱们此刻就要先树立一个结构体。
例如这儿咱们新建一个user
的结构体:
type User struct {
gorm.Model
Name string
Age string
}
此处的gorm.Model
是结构自带的一个结构体,供给了常见的一些字段:
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中,咱们能够按照这样的办法来主动搬迁:
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("主动搬迁失利")
}
}
履行程序,然后来看一下数据库此刻的情况:
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
结构体:
type User struct {
gorm.Model
Name string
Age string
NickName string
}
再次运转后咱们查看一下表结构:
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()
办法来新建一条记载:
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)
运转后咱们此刻查看表:
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)
当然你也能够经过传入一个切片的办法来批量添加记载:
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)
运转后查看原表:
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
错误
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的语法根本共同的。
// 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
办法来更新字段:
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
:
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会删去全部符合挑选条件的记载。
假如咱们依据主键删去:
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
来查询到被软删去的记载。
var users []User
db.Unscoped().Where("age = '16'").Find(&users)
// SELECT * FROM users WHERE age = '16';
你也能够运用 Unscoped
来永久删去匹配的记载
db.Unscoped().Delete(&user)
// DELETE FROM users WHERE age = '16';
总结
GORM 作为Go 比较老练的ORM 结构,它的事务才能是有目共睹的。关于新手而言,若要快速学习与SQL的交互,从GORM入手也许是一个不错的选择。
一同GORM还有着更多好玩的特性,下篇文章笔者将尝试解说将Gin和Gorm结合起来的实践应用。
本文示例代码已放在仓库内
参考文档
-
Gorm官方文档
-
ORM实例手册