Go实践—初识Gorm框架

2023年 7月 31日 120.7k 0

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

前言:何为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) 的缩写。

img

一般来说,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 的官方镜像:

    image-20230728184604256

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

    $ 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?&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,查看 约束 获取详情
    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——删

    首先确定两个概念:

  • 软删除:通过特定的标记方式在查询的时候将此记录过滤掉。虽然数据在界面上已经看不见,但是数据库还是存在的。
  • 硬删除:传统的物理删除,直接将该记录从数据库中删除。
  • 为什么引入这两个概念,这里留给读者自行思考。

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

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论