本文同步发布在个人博客。

前言:何为ORM

要说ORM是何物,咱们得先从面向目标谈起。

在面向目标的编程思想中遵循着一句话:“全部皆目标。”

而在数据库那边,以联系型数据库来说吧,联系型数据库则讲究:“全部实体都有联系。”

你发现了什么?联系是不是也能用目标的思想去描绘?

举个比如,假如有一张表:

CREATE TABLE `users` (
  `id` integer PRIMARY KEY,
  `username` varchar(255),
  `role` varchar(255),
  `created_at` timestamp
);

在这张名为users的表内有着4个字段:id,username,rolecreated_at

假如咱们将它用Go的结构体去描绘呢?

type Users struct {
	Id        int      
	Username  string    
	Role      string 
	CreatedAt time.Time
}

自此,咱们便完结了一个从表到结构体的映射。

而ORM做的就是这样一种工作,从表映射到目标。ORM 就是经过实例目标的语法,完结联系型数据库的操作的技术,是”目标-联系映射”(Object/Relational Mapping) 的缩写。

Go实践—初识Gorm框架

一般来说,ORM会完结以下的映射联系:

  • 数据库的表(table) –> 类(class)
  • 记载(record,行数据)–> 目标(object)
  • 字段(field)–> 目标的特点(attribute)

当然因为Go并没有class这个概念,因此在Go中ORM会完结以下的映射联系:

  • 数据库的表(table) –> 结构体(struct)
  • 记载(record,行数据)–> 结构体的实例化(object)
  • 字段(field)–> 结构体的字段(fields)

ORM有着下面的优点:

  1. 弱化SQL原生语句的要求,关于新手来说简略操作易上手;
  2. 将SQL笼统成结构体和目标,易于了解;
  3. 必定程度上添加了开发效率。

但也有必定的缺陷:

  1. 添加了一层中间环节,一同运用了反射,献身了必定的性能;
  2. 献身了灵敏性,弱化了SQL的才能;
  3. 献身了一些原生功用。

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 的官方镜像:

Go实践—初识Gorm框架

在右侧现已写好了拉取命令,仿制,在本地终端履行一下:

$ 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 nullsize, 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 供给了 FirstTakeLast 办法,以便从数据库中检索单个目标。当查询数据库时它添加了 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——删

首先确定两个概念:

  1. 软删去:经过特定的标记办法在查询的时分将此记载过滤掉。虽然数据在界面上现已看不见,可是数据库仍是存在的。
  2. 硬删去:传统的物理删去,直接将该记载从数据库中删去。

为什么引进这两个概念,这儿留给读者自行思考。

在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实例手册