likes
comments
collection
share

Go实践—初识Gorm框架

作者站长头像
站长
· 阅读数 20

本文同步发布在个人博客

前言:何为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根据参数创建索引,多个字段使用相同的名称则创建复合索引,查看 索引 获取详情
uniqueIndexindex 相同,但创建的是唯一索引
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结合起来的实际应用。

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

参考文档

转载自:https://juejin.cn/post/7260783558544998459
评论
请登录