Go实践—初识Gorm框架

本文同步发布在个人博客

前言:何为ORM

要说ORM是何物,我们得先从面向对象谈起。

在面向对象的编程思想中贯彻着一句话:"一切皆对象。"

而在数据库那边,以关系型数据库来说吧,关系型数据库则讲究:"一切实体都有关系。"

你发现了什么?关系是不是也能用对象的思想去描述?

举个例子,假如有一张表:

sql 复制代码
CREATE TABLE `users` (
  `id` integer PRIMARY KEY,
  `username` varchar(255),
  `role` varchar(255),
  `created_at` timestamp
);

在这张名为users的表内有着4个字段:id,username,rolecreated_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有着下面的优点:

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

在右侧已经写好了拉取命令,复制,在本地终端执行一下:

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 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中,我们可以按照这样的方式来自动迁移:

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 提供了 FirstTakeLast 方法,以便从数据库中检索单个对象。当查询数据库时它添加了 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------删

首先确定两个概念:

  1. 软删除:通过特定的标记方式在查询的时候将此记录过滤掉。虽然数据在界面上已经看不见,但是数据库还是存在的。
  2. 硬删除:传统的物理删除,直接将该记录从数据库中删除。

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

在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结合起来的实际应用。

本文示例代码已放在仓库内

参考文档

相关推荐
BlockChain8886 小时前
Solidity 实战【二】:手写一个「链上资金托管合约」
go·区块链
BlockChain88814 小时前
Solidity 实战【三】:重入攻击与防御(从 0 到 1 看懂 DAO 事件)
go·区块链
剩下了什么19 小时前
Gf命令行工具下载
go
地球没有花20 小时前
tw引发的对redis的深入了解
数据库·redis·缓存·go
BlockChain8881 天前
字符串最后一个单词的长度
算法·go
龙井茶Sky1 天前
通过higress AI统计插件学gjson表达式的分享
go·gjson·higress插件
宇宙帅猴2 天前
【Ubuntu踩坑及解决方案(一)】
linux·运维·ubuntu·go
SomeBottle3 天前
【小记】解决校园网中不同单播互通子网间 LocalSend 的发现问题
计算机网络·go·网络编程·学习笔记·计算机基础
且去填词3 天前
深入理解 GMP 模型:Go 高并发的基石
开发语言·后端·学习·算法·面试·golang·go
大厂技术总监下海4 天前
向量数据库“卷”向何方?从Milvus看“全功能、企业级”的未来
数据库·分布式·go·milvus·增强现实