目录

前言

以下是官方文档的地址,也是我参考的文档

GORM官方中文文档

  • 虽然 database/sql 是一个功能强大的工具,但 GORM 的出现是为了简化数据库操作,提高开发效率,让开发者能更专注于业务逻辑而非低层的数据库细节。这就是 GORM 的设计理念。

本文只讲述如何链接mysql数据库并进行相应操作

安装Gorm

  1. 首先新建项目并且使用go mod init初始化项目
  2. 调用以下两句命令安装GORM:
1
2
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
  1. 通过查看mod 或者使用 go mod graph查看是否安装成功

Gorm连接mysql数据库

GORM的一些约定

  1. 主键:GORM 使用一个名为ID 的字段作为每个模型的默认主键。
  2. 表名:默认情况下,GORM 将结构体名称转换为 snake_case 并为表名加上复数形式。
    例如,一个 User 结构体在数据库中的表名变为 users 。可以使用重定义某个标的名字来做到自定义表名,例如:
    1
    2
    3
    func (User) TableName() string {
    return "user"
    }
  3. 列名:GORM 自动将结构体字段名称转换为 snake_case 作为数据库中的列名。
  4. 时间戳字段:GORM使用字段 CreatedAt(该字段的值将会是初次创建记录的时间。) 和 UpdatedAt 来自动跟踪记录的创建和更新时间(该字段的值将会是每次更新记录的时间)。

主键

1
2
3
4
5
6
7
8
9
10
11
type User struct {
ID string // 名为`ID`的字段会默认作为表的主键
Name string
}

// 使用`AnimalID`作为主键
type Animal struct {
AnimalID int64 `gorm:"primary_key"`
Name string
Age int64
}

表名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type User struct {} // 默认表名是 `users`

// 将 User 的表名设置为 `profiles`
func (User) TableName() string {
return "profiles"
}

func (u User) TableName() string {
if u.Role == "admin" {
return "admin_users"
} else {
return "users"
}
}

// 禁用默认表名的复数形式,如果置为 true,则 `User` 的默认表名是 `user`
db.SingularTable(true)

数据库链接的基本方式

  1. 首先我们需要导入需要用到的包,并且定义数据库链接信息
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    package main

    import (
    "fmt"
    "log"

    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    )

    func main() {
    // 数据库连接信息
    dsn := "root:123456@(localhost:3306)/pigcanstudy?charset=utf8mb4&parseTime=True&loc=Local"
    }
    • 其中连接信息的模板是 "user:password@(localhost)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
  2. 然后我们使用gorm.Open()方法连接数据库
    1
    2
    3
    4
    5
    6
      // 连接数据库
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
    log.Fatalf("failed to connect to database: %v", err)
    }
    defer db.Close()
    • 其中mysql.Open()方法是用来连接mysql数据库的,dsn参数是连接信息,&gorm.Config{}是gorm的配置信息,可以不用管

现有的数据库连接方式

GORM 允许通过一个现有的数据库连接来初始化 *gorm.DB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import (
"database/sql"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)

func main() {
// 数据库连接信息
dsn := "root:123456@(localhost:3306)/pigcanstudy?charset=utf8mb4&parseTime=True&loc=Local"

sqlDB, _ := sql.Open("mysql", dsn)
gormDB, _ := gorm.Open(mysql.New(mysql.Config{
Conn: sqlDB,
}), &gorm.Config{})
}

Gorm定义数据库模型的注意事项

  1. 结构体的名称必须首字母大写,并且和数据库表名称对应
  2. 结构体中的字段名称的首字母必须大写,并且和数据库表中的列名对应
  3. 默认情况表名是结构体的复数形式,可以通过重写TableName()方法自定义表名

GORM操作mysql的基本用法

假设我们声明的模型是如下定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package models

type User struct {
Id int
Name string
Email string
pwd string
sex int
}

func (u *User) TableName() string {
return "user"
}

创建

附带下mysql的书写和执行顺序
mysql书写和执行顺序

创建表

一般情况下,我们都是先将数据库表创建完后在进行实际项目的开发,所以这里创建用的较少,但是我们可以了解下如何创建表:

1
2
3
 // 自动迁徙(作用是自动迁移数据库表的结构,以与 User 结构体的定义保持一致)
// 这意味着它会根据 User 结构体的字段信息来创建或更新数据库中的相应表。
db.AutoMigrate(&User{})

也就是说可以是用AutoMigrate方法来自动创建表格

创建记录(增加记录)

  1. 使用上面的定义的模型
  2. 使用create创建

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package main

import (
"fmt"
"log"

"gorm.io/driver/mysql"
"gorm.io/gorm"
)

type User struct {
ID uint
Name string
Email string
Pwd string
Sex int
}

func (User) TableName() string {
return "user"
}

func main() {
// 数据库连接信息
dsn := "root:123456@(localhost:3306)/db1?charset=utf8mb4&parseTime=True&loc=Local"
// 连接数据库
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalf("failed to connect to database: %v", err)
}

// 获取原始数据库的链接*sql.DB的对象
// sqlDB, err := db.DB()
// if err != nil {
// log.Fatalf("failed to get DB: %v", err)
// }

// -------------------------------------------创建表格-----------------------------------------------

// 自动迁徙(作用是自动迁移数据库表的结构,以与 User 结构体的定义保持一致)
// 这意味着它会根据 User 结构体的字段信息来创建或更新数据库中的相应表。
db.AutoMigrate(&User{})

// -------------------------------------------插入记录-----------------------------------------------

// 批量插入
var users []User = []User{
{ID: 1, Name: "Alice", Email: "alice@example.com", Pwd: "123456", Sex: 1},
{ID: 2, Name: "Bob", Email: "bob@example.com", Pwd: "654321", Sex: 2},
}

// 创建记录
db.Create(&users)

//挨个插入
user1 := User{ID: 3, Name: "Charlie", Email: "charlie@example.com", Pwd: "123456", Sex: 1}
user2 := User{ID: 4, Name: "Dave", Email: "dave@example.com", Pwd: "654321", Sex: 2}

// 创建记录
db.Create(&user1)
db.Create(&user2)
}

查询

使用一般查询(内置的查询接口)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

// ------------------------------First() 方法------------------------------

var u User

// db.First(desc interface{}, conds ...interface{})
// 第一个参数是要查询的结构体,第二个参数是查询条件,可以为空,表示无条件查询
// 这个接口会根据条件查询符合条件的第一条记录,并将其赋值给第一个参数的结构体
// 例如查询第一个pwd为123456的记录(仅当主键为整形可用)
db.First(&u, "pwd = ?", "123456")
// 对应的sql语句是: SELECT * FROM user WHERE pwd = '123456' LIMIT 1;

// 根据主键查询第一条记录
db.First(&u)
// 对应的sql语句是: SELECT * FROM user OREDER BY id LIMIT 1;


// -------------------------------Find()方法-------------------------------

// db.Find(dest interface{}, conds ...interface{})
// 第一个参数是要查询的结构体切片,第二个参数是查询条件,可以为空,表示无条件查询
// 这个接口会根据条件查询所有符合条件的记录,并将其赋值给第一个参数的结构体切片
// 例如查询所有Sex为1(男)的记录
var uu []User
db.Find(&uu, "Sex = ?", 1)
// 对应的sql语句是: SELECT * FROM user WHERE Sex = 1;


// -------------------------------Take()方法-------------------------------

// db.Take(dest interface{}, conds ...interface{})
// 这个接口和db.First()类似,但是它查询的时候不是排序的,如果给定了条件,则会先根据条件查询符合条件的第一条记录,然后将其赋值给dest参数的结构体,然后返回。
// 例如查询第一个Sex为1(男)的记录
var u User
db.Take(&u, "Sex = ?", 1)
// SELECT * FROM user WHERE Sex = 1 LIMIT 1;

db.Take(&u)
// SELECT * FROM user LIMIT 1;


// -------------------------------Last()方法------------------------------

// db.Last(dest interface{}, conds ...interface{})
// 这个接口是根据主键查询(满足条件)最后一条记录
// 例如查询满足Sex为1(男)的最后一条记录
db.Last(&u, "Sex = ?", 1)
// 对应的sql语句是: SELECT * FROM user WHERE Sex = 1 ORDER BY id DESC LIMIT 1;

使用Where()方法

普通sql查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

var u User

// ---------------------------db.Where()方法------------------------------

// db.Where(query interface{}, args ...interface{})
// 第一个参数是查询条件,可以是字符串或者map或者结构体,第二个参数是参数(只有当前面为字符串且出现了 ? 时候才会使用 也就是sql注入),可以为空,表示无参数

// 例如查询第一个名字为Alice的记录
db.Where("name = ?", "Alice").First(&u)
// 对应的sql语句是: SELECT * FROM user WHERE name = 'Alice' LIMIT 1;

// 得到所有满足pwd为123456的记录
db.Where("pwd = ?", "123456").Find(&u)
// 对应的sql语句是: SELECT * FROM user WHERE pwd = '123456';

// 得到所有不满足name为Alice的记录
db.Where("name <> ?", "Alice").Find(&u)
// 对应的sql语句是: SELECT * FROM user WHEARE name <> 'Alice';


// 得到所有name满足一个查询数组条件的记录
db.Where("name IN (?)", []string{"Alice", "Bob"}).Find(&uu)
// 对应的sql语句是: SELECT * FROM user WHERE name IN ('Alice', 'Bob')


// 查询名字为%A%的所有记录
db.Where("name LIKE ?", "%A%").Find(&uu)
// 对应的sql语句是: SELECT * FROM user WHERE name LIKE '%A%'


// 查询名字为Alice同时密码为123456的记录
db.Where("name = ? AND pwd = ?", "Alice", "123456").Find(&uu)
// 对应的sql语句是: SELECT * FROM user WHERE name = 'Alice' AND pwd = '123456'


// 查询更新时间为上周的记录
db.Where("updated_at > ?", time.Now().Add(-7*24*time.Hour)).Find(&uu)
// 对应的sql语句是: SELECT * FROM user WHERE updated_at > '2024-11-24 00:00:00'


// 查询创建时间位于一个区间之间的记录
db.Where("created_at BETWEEN ? AND ?", "2021-01-01 00:00:00", "2022-01-01 00:00:00").Find(&uu)
// 对应的sql语句是: SELECT * FROM user WHERE created_at BETWEEN '2021-01-01 00:00:00' AND '2022-01-01 00:00:00'


Stuct以及Map查询
1
2
3
4
5
6
7
8
9
10
11
12
13

var uuu User
// 使用结构体查询
db.Where(User{Name: "Alice", Pwd: "123456"}).Find(&uuu)

fmt.Println("查询结果为: ", uuu)

var uuuu User
// 使用map来查询
db.Where(map[string]interface{}{"name": "Bob", "pwd": "654321"}).Find(&uuuu)

fmt.Println("查询结果为: ", uuuu)

注意:当通过结构体进行查询时,GORM将会只通过非零值字段查询,这意味着如果你的字段值为0,‘’,false或者其他零值时,将不会被用于构建查询条件,例如:

1
2
3
db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)
//// SELECT * FROM users WHERE name = "jinzhu";

你可以使用指针或实现 Scanner/Valuer 接口来避免这个问题.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用指针
type User struct {
gorm.Model
Name string
Age *int
}

// 使用 Scanner/Valuer
type User struct {
gorm.Model
Name string
Age sql.NullInt64 // sql.NullInt64 实现了 Scanner/Valuer 接口
}

Not条件

作用与 Where 类似的情形如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// ---------------------------db.Not()方法---------------------------------

// db.Not(query interface{}, args ...interface{})
// 第一个为查询的条件,第二个参数是条件的具体值,可以为空,表示无参数
/*
使用Not()时候有两种区别:
1. 使用name = ? 作为条件,会被翻译成 NOT (name = 'Alice') 这种形式,例如:
db.Not("name = ?", "Alice").Find(&u)
对应的sql语句是: SELECT * FROM user WHERE NOT (name = 'Alice');

db.Not("name = ?", []string{"Alice", "Bob"}).Find(&u)
其对应的sql语句是 SELECT * FROM user WHERE NOT name = ('Alice', 'Bob') 这种写法是错的,所以编译器会报错,这是就得使用下面这种方式

2. 翻译成NOT IN的形式,例如:
db.Not("name", []string{"Alice", "Bob"}).Find(&u)
对应的sql语句是: SELECT * FROM user WHERE name NOT IN ('Alice', 'Bob')
*/


// 得到所有name不为Alice的记录
db.Not("name = ?", "Alice").Find(&uu)
// 对应的sql语句是 SELECT* FROM user WHERE NOT (name = 'Alice')

// 得到所有name不为Alice且pwd不为123456的记录
db.Not("name = ? AND pwd = ?", "Alice", "123456").Find(&uu)
// 对应的sql语句是 SELECT* FROM user WHERE NOT (name = 'Alice' AND pwd = '123456')

// 得到name不在切片([]string{"Alice", "Bob"})中的记录
db.Not("name", []string{"Alice", "Bob"}).Find(&uu)
// 对应的sql语句是 SELECT* FROM user WHERE name NOT IN ('Alice', 'Bob')

// 使用结构体来查询
db.Not(User{Name: "Alice", Pwd: "123456"}).Find(&uu)
// 对应的sql语句是 SELECT * FROM user WHERE NOT (name = 'Alice' AND pwd = '123456')

Or条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ---------------------------db.Or()方法----------------------------------

// db.Or(query interface{}, args ...interface{})
// 第一个参数为查询条件,可以是字符串或者map或者结构体,第二个参数是条件的具体值,可以为空,表示无参数

// 直接使用Or()方法,例如:
db.Or("name = ?", "Alice").Or("name = ?", "Bob").Find(&uu)
// 对应的sql语句是: SELECT * FROM user WHERE name = 'Alice' OR name = 'Bob'

// 使用Or()方法和Where()方法组合,例如:
db.Where("name = ?", "Alice").Or("name = ?", "Bob").Find(&uu)
// 对应的sql语句是: SELECT * FROM user WHERE name = 'Alice' OR name = 'Bob'

// 使用结构体
db.Where("name = 'Alice'").Or(User{Name: "Bob", Pwd: "123456"}).Find(&uu)
// 对应的sql语句是 SELECT * FROM `user` WHERE name = 'Alice' OR (`user`.`name` = 'Bob' AND `user`.`pwd` = '654321')

// 使用MAP
db.Where("namse = 'Alice'").Or(map[string]interface{}{"name": "Bob", "pwd": "654321"}).Find(&uu)
// 对应的sql语句是 SELECT * FROM `user` WHERE name = 'Alice' OR (`name` = 'Bob' AND `pwd` = '654321')

注意: 使用结构体和使用MAP来查询的时候其最终被翻译成的sql语句有所差别,前者会加上表名,后者不会。具体看上面例子。

内联条件

就是把条件卸载一起,例如:

1
2
3
4
5
6
7
8
9
10
11

db.First(&u, "name = ?", "Alice")
// 对应的sql语句是: SELEECT * FROM user WHERE name = 'Alice' ORDER BY `user`.`id` LIMIT 1;

db.Find(&users, "name <> ? AND age > ?", "jinzhu", 20)
//// SELECT * FROM users WHERE name <> "jinzhu" AND age > 20;

db.First(&u, "name = ? AND pwd = ?", "Alice", "123456")

db.Where("name = ? AND pwd = ?", "Alice", "123456").Find(&uu)

额外查询选项

1
2
3
4
// 为查询 SQL 添加额外的 SQL 操作
db.Set("gorm:query_option", "FOR UPDATE").First(&user, 10)
//// SELECT * FROM users WHERE id = 10 FOR UPDATE;

一些问题

  1. 诸如db.Where和db.Not方法它是怎么确定查询的是数据库的哪个表格的?

答: Gorm会根据你定义的模型(也就是你定义的结构体)来确定查询的表格,这些方法必须跟着Find(&uu)或者First(&u)等方法一起使用才会生效。跟了这个之后就知道查询的是哪个表格了。

FirstOrInit()方法

作用: 查询满足条件的第一条记录,如果没有找到,就初始化一个满足给定条件的结构体并返回。但是并不会在数据库中创建新的记录,并且仅支持结构体和map两种查询条件。

1
2
db.FirstOrInit(&u, User{Name: "ppig", Pwd: "123456", Sex: 1})

通常会和Attrs和Assign方法一起使用,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

// ---------------------------db.Attrs()方法----------------------
// 如果未找到才会使用参数初始化结构体

// 找的到的情况下
db.Where(User{Name: "ppig", Pwd: "123456", Sex: 1}).Attrs("age", 20).FirstOrInit(&u)
// 输出 {1 Alice alice@example.com 123456 1}

// 没找到的情况下
db.Where(User{Name: "ppid", Pwd: "123456", Sex: 1}).Attrs("sex", 2).FirstOrInit(&u)
// 输出 查询结果为: {1 ppid alice@example.com 123456 2}


// ---------------------------db.Assign()方法----------------------
// 不管有没有找到都会将参数赋给结构体

// 找的到的情况下
db.Where(User{Name: "Alice", Pwd: "123456", Sex: 1}).Assign(User{Sex: 2}).FirstOrInit(&u)
// 输出 {1 Alice alice@example.com 123456 2}(数据库中sex为1)

// 没找到的情况下
// 未找到
db.Where(User{Name: "non_existing"}).Assign(User{Sex: 2}).FirstOrInit(&user)
//// SELECT * FROM USERS WHERE name = 'non_existing';

那么这种有啥实际的应用场景吗?

  1. 可以在未找到的情况下初始化一个对象,然后选择性的create进数据库中
  2. 可以避免一些相同的初始化而导致代码冗余

FirstOrCreate()方法

作用: 查询满足条件的第一条记录,如果没有找到,就创建一条新的记录并返回。但是并不会在数据库中更新已存在的记录,并且仅支持结构体和map两种查询条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

// 使用FirstOrCreate()方法 先查询是否存在,如果存在并且配上了(Assign)就会更新,不存在就需要下面的方法创建一个记录,否则会出现创建重复id记录的情况(如果指定了自增就不需要下面的方法了,可以直接使用)下述一行代码就能插入

type User struct {
ID uint `gorm:"primaryKey;autoIncrement"`
Name string
Email string
Pwd string
Sex int
}

db.Where(User{Name: "ppsd", Pwd: "123456", Sex: 2}).Assign(User{Sex: 1})



// ---------------------------不用自增字段的话----------------------

var maxID uint
err = db.Model(&User{}).Select("MAX(id)").Scan(&maxID).Error
if err != nil {
fmt.Println("Error:", err)
return
}

// 新的记录
nextID := maxID + 1
inputUser := User{
ID: nextID, // 手动设置下一个 ID
Name: "ppid",
Pwd: "123456",
Sex: 1,
}

fmt.Print("输入的记录为: ", inputUser)

db.Where(inputUser).Assign(User{ID: nextID, Sex: 2}).FirstOrCreate(&u)
fmt.Println("查询结果为: ", u)


基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 未找到
db.FirstOrCreate(&user, User{Name: "non_existing"})
//// INSERT INTO "users" (name) VALUES ("non_existing");
//// user -> User{Id: 112, Name: "non_existing"}

// 找到
db.Where(User{Name: "Jinzhu"}).FirstOrCreate(&user)
//// user -> User{Id: 111, Name: "Jinzhu"}




// ---------------------------db.Attrs()方法-----------------------------
// 如果未找到,将使用参数初始化结构体,并创建一条新的记录
// 未找到
db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrCreate(&user)
//// SELECT * FROM users WHERE name = 'non_existing';
//// INSERT INTO "users" (name, age) VALUES ("non_existing", 20);
//// user -> User{Id: 112, Name: "non_existing", Age: 20}

// 找到
db.Where(User{Name: "jinzhu"}).Attrs(User{Age: 30}).FirstOrCreate(&user)
//// SELECT * FROM users WHERE name = 'jinzhu';
//// user -> User{Id: 111, Name: "jinzhu", Age: 20}




// ---------------------------db.Assign()方法------------------------------
// 未找到
db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrCreate(&user)
//// SELECT * FROM users WHERE name = 'non_existing';
//// INSERT INTO "users" (name, age) VALUES ("non_existing", 20);
//// user -> User{Id: 112, Name: "non_existing", Age: 20}

// 找到
db.Where(User{Name: "jinzhu"}).Assign(User{Age: 30}).FirstOrCreate(&user)
//// SELECT * FROM users WHERE name = 'jinzhu';
//// UPDATE users SET age=30 WHERE id = 111;
//// user -> User{Id: 111, Name: "jinzhu", Age: 30}


高级查询

这里补充说下COALESCES()的用法,用一句话来说就是COALESCES(expr1, expr2, expr3,…), 这个函数就是用来返回从左到右的第一个非NULL表达式的值。
函数:COALESCE()

选择字段查询

使用Select()方法可以选择需要查询的字段,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 var us User

// 1.直接指定字段
db.Select("name, email").Where("id = 4").Find(&us)

fmt.Println("查询结果为: ", us)

// 这样查询完后输出{0 Dave dave@example.com 0}

// 2.使用切片指定字段
db.Select([]string{"name", "email"}).Where("id = 3").Find(&us)

// 3.指定表格
db.Table("user").Select("name, email").Where("id = 2").Find(&us)

排序Order()

Order()方法的定义:

1
Order(value interface{}, reorder ...bool) *DB

Order,指定从数据库中检索出记录的顺序。设置第二个参数 reorder 为 true ,可以覆盖前面定义的排序条件。

1
2
3
4
5
6
7
8
9
// 单字段排序
db.Order("sex desc").Find(&us)
// 对应的sql语句是: SELECT * FROM user OREDER BY sex DESC

// 多字段排序
db.Order("sex desc, name").Find(&us)
db.Order("sex desc").Order("name").Find(&us)
// 对应的sql语句是: SELECT * FROM `user` ORDER BY sex desc, name

Limit()

作用: 指定从数据库检索出的最大记录数

1
2
3
4
5
6
db.Order("sex desc").Limit(3).Find(&us)
// 对应的sql语句是: SELECT * FROM user ORDER BY sex DESC LIMIT 3
// 此时指挥输出三条记录

// 当参数为-1的时候,表示取消之前的limit条件
db.Limit(3).Find(&users1).Limit(-1).Find(&users2)
Offset()

作用: 指定从数据库检索出的记录的起始位置(指定开始返回记录前要跳过的记录数。), 一般配合Limit()方法一起使用。

1
2
3
4
5
// 例如有如下需求:查找入职员工时间排名倒数第三的员工所有信息
// 原生的sql语句是: 1. SELECT * FROM employees ORDER BY hire_date DESC Limit 1 OFFSET 2;
db.Order("hire_date desc").Limit(1).Offset(2).Find(&employee)
// 其作用就是去掉前两个值,只返回第三个值。
// 也可以这样写:2. SELECT * FROM employees ORDER BY hire_date DESC LIMIT 2, 1;
Count()

获取记录总数

  • 注意:使用的时候必须放到.Find()方法之后,否则不会执行查询操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
 var count int64

// 例如获取性别为男(2)的用户总
db.Where("sex = ?", 2).Find(&users).Count(&count)
// 对应的sql语句是: SELECT COUNT(*) FROM user WHERE sex = 2;

db.Table("user").Count(&count)
// 对应的sql语句是: SELECT COUNT(*) FROM user;

db.Table("user").Select("count(pwd)").Count(&count)
// 对应的sql语句是: SELECT COUNT(pwd) FROM user;

db.Model(&User{}).Count(&count)
// 对应的sql语句是: SELECT COUNT(*) FROM user;
  • 注意: Count 必须是链式查询的最后一个操作 ,因为它会覆盖前面的 SELECT,但如果里面使用了 count 时不会覆盖
Group & Having

作用: 用于分组和过滤数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1.请编写 SQL 查询,找出每个部门的总薪资,并只返回总薪资超过 50000 的部门名称和总薪资。
// 2.结果需要按照总薪资从高到低排序

/* 首先是sql语句:
SELECT department, SUM(salary) AS total_salary FROM employees GROUP BY department HAVING total_salary > 50000 ORDER BY total_salary DESC;
*/

// 然后是Gorm的写法:

var emp []struct {
Department string
Total_salary float64
}

var employee []employees.Employee
db.Table("employees").Find(&employee)
fmt.Println("员工数据:", employee)

db.Table("employees").Select("department, SUM(salary) as total_salary").Group("department").Having("total_salary > ?", 50000).Order("total_salary DESC").Scan(&emp)

fmt.Println("查询结果为: ", emp)

Joins

作用: 用于连接多个表。

1
2
3
4
5
6
7
8
9
10
rows, err := db.Table("employees").Joins("INNER JOIN user ON employees.id = user.id").Where("user.name = ?", "Alice").Or("user.name = ?", "Bob").Rows()
// 对应的sql语句是: SELECT * FROM employees INNER JOIN user ON employees.id = user.id WHERE user.name = 'Alice' OR user.name = 'Bob';
// 也可以是left join right join
for rows.Next() {
var e employees.Employee
db.ScanRows(rows, &e)
employee = append(employee, e)
}

fmt.Println("员工数据:", employee)
Pluck

Pluck,查询 model 中的一个列作为切片,如果您想要查询多个列,您应该使用 Scan

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 通过 GORM 查询用户
var uu []user.User
var names []string

// 执行查找并提取名字
db.Model(&uu).Pluck("name", &names)

fmt.Println("查询结果为: ", names)

// 执行查找并提取名字
db.Table("user").Pluck("name", &names)

// 想查询多个字段? 这样做:
db.Select("name, age").Find(&users)

Scan

Scan 扫描结果至一个struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Result struct {
Name string
Age int
}

var result Result
db.Table("users").Select("name, age").Where("name = ?", "Antonio").Scan(&result)

var results []Result
db.Table("users").Select("name, age").Where("id > ?", 0).Scan(&results)

// 原生 SQL
db.Raw("SELECT name, age FROM users WHERE name = ?", "Antonio").Scan(&result)

链式操作相关

链式操作

在调用立即执行方法前不会调用Query语句, 所以你可以用链式操作来构造复杂的查询条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 链式操作
tx := db.Where("name = ?", "Alice")

// 添加更多查询条件
if flag := true; flag {
tx = tx.Where("sex = ?", "男")
} else {
tx = tx.Where("sex = ?", "女")
}

if f := true; f {
tx = tx.Where("Addr = ?", "中国")
}

result := tx.Find(&uu)

立即执行方法

立即执行方法指的是会立马执行Query语句,并返回结果。常见的立即执行方法有:
Create, First, Find, Scan, Delete, Update, Save等。

范围

Scopes , 此方法是建立在链式操作的基础之上的,基于它,可以抽取一些通用逻辑,写出更多的可重用函数库。例如: 你可以把查询超过1000的或者表示已付款的查询逻辑封装成一个函数,之后需要用到的时候,使用Scopes方法来调用即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func AgeBiggerThan18(db *gorm.DB) *gorm.DB {
return db.Where("age > 18")
}

func main() {
// 数据库连接信息
dsn := "root:123456@(localhost:3306)/db1?charset=utf8mb4&parseTime=True&loc=Local"
// 连接数据库
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalf("failed to connect to database: %v", err)
}

db = db.Debug()
// 使用Scope方法调用
tx = tx.Scopes(AgeBiggerThan18)
result := tx.Find(&uu)
}

多个立即执行方法

在 GORM 中使用多个立即执行方法时,后一个立即执行方法会复用前一个立即执行方法的条件 (不包括内联条件) 。

1
2
db.Where("name LIKE ?", "jinzhu%").Find(&users, "id IN (?)", []int{1, 2, 3}).Count(&count)

生成的sql语句是这样的:

1
2
3
4
SELECT * FROM users WHERE name LIKE 'jinzhu%' AND id IN (1, 2, 3)

SELECT count(*) FROM users WHERE name LIKE 'jinzhu%'

更新

更新所有字段

save() 默认会更新该对象的所有字段,即使你没有赋值

如果数据库中有字段 就会执行UPDATE语句
1
2
3
4
5
 uu[0].Age = 40
uu[0].Sex = "女"
uu[0].Addr = "北京"

db.Save(&uu[0])

对应的sql语句

1
UPDATE `people` SET `name`='Alice',`age`=40,`sex`='女',`addr`='北京' WHERE `id` = 1
如果数据库中没有字段,就会执行INSERT语句
1
2
3
4
5
6
7
8
u := people.People{
Name: "Xiaoming",
Age: 20,
Sex: "男",
Addr: "北京",
}

db.Save(&u)

对应的sql语句

1
INSERT INTO `people` (`name`,`age`,`sex`,`addr`) VALUES ('Xiaoming',20,'男','北京')

更新指定字段

可以使用Updates或者Update方法来更新指定字段

这两的区别是 前者用来更新多个字段,后者只能用于更新单个字段,前者需要用map或者结构体来更新

使用的时候需要注意,在sql语法中 update确实在Where之前,但是在Gorm中,Update一定得放在之后,因为Update属于立即执行方法,Where则不是,立即执行方法得放在最后

1
2
3
4
5
6
7
 db.Model(&people.People{}).Where("name = ?", "Alice").Update("age", 18)

db.Model(&people.People{}).Where("name = ?", "Bob").Updates(people.People{Age: 18, Sex: "女"})

// 警告:当使用 struct 更新时,GORM只会更新那些非零值的字段
// 对于下面的操作,不会发生任何更新,"", 0, false 都是其类型的零值
db.Model(&user).Updates(User{Name: "", Age: 0, Active: false})

对应的sql语句是

1
2
3
4
5
UPDATE `people` SET `age` = 18 WHERE name = 'Alice'
UPDATE people SET age = 38 WHERE name = 'Alice' 不加``符号也能运行


UPDATE `people` SET `age`=18,`sex`='女' WHERE name = 'Bob'

更新选定字段

如果你想更新或忽略某些字段,你可以使用SelectOmit方法

1
2
// 用来忽略某个字段
db.Model(&people.People{}).Omit("name").Where("name = ?", "xiaoming").Updates(map[string]interface{}{"name": "hello", "age": 12})
1
2
3
// 对应的SQL语句
UPDATE `people` SET `age`=12 WHERE name = 'xiaoming'

批量更新

可以一次性更新多个记录的某些字段为相同的值

1
2
3
4
db.Model(&people.People{}).Where("ID in (?)", []int{1, 2, 3}).Updates(map[string]interface{}{"name": "hello", "age": 12})
// 对应的SQL语句
UPDATE `people` SET `name`='hello',`age`=12 WHERE `id` IN (1, 2, 3)

使用sql表达式更新

1
2
3
4
db.Model(&people.People{}).Where("ID = ?", 1).Update("age", gorm.Expr("age * ? + ?", 2, 10))

// 对应的SQL语句
UPDATE `people` SET `age`=age * 2 + 10 WHERE `id` = 1

删除

删除记录

注意: 删除记录时,请确保主键字段有值,GORM 会通过主键去删除记录,如果主键为空,GORM 会删除该 model 的所有记录。

1
2
3
4
5
db.Delete(&people.People{}, "id = ?", "3")

// 为删除 SQL 添加额外的 SQL 操作
db.Set("gorm:delete_option", "OPTION (OPTIMIZE FOR UNKNOWN)").Delete(&email)
//// DELETE from emails where id=10 OPTION (OPTIMIZE FOR UNKNOWN);

批量删除

1
2
3
4
5
6
db.Where("email LIKE ?", "%haohao%").Delete(Email{})
//// DELETE from emails where email LIKE "%haohao%";

db.Delete(Email{}, "email LIKE ?", "%jinzhu%")
//// DELETE from emails where email LIKE "%jinzhu%";

事务

事务在实际的项目中非常的有用,那么在gorm中它是如何使用的呢?

禁用默认事务

为了确保数据一致性,GORM 会在事务里执行写入操作(创建、更新、删除)。如果没有这方面的要求,您可以在初始化时禁用它,这将获得大约 30%+ 性能提升。

1
2
3
4
5
6
7
8
9
10
// 全局禁用
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
SkipDefaultTransaction: true, //<=
})

// 持续会话模式
tx := db.Session(&Session{SkipDefaultTransaction: true})
tx.First(&user, 1)
tx.Find(&users)
tx.Model(&user).Update("Age", 18)

Gorm手动控制事务

事务的执行流程

要在事务中执行一系列操作,通常您可以参照下面的流程来执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
db.Transaction(func(tx *gorm.DB) error {
// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
// 返回任何错误都会回滚事务
return err
}

if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
return err
}

// 返回 nil 提交事务
return nil
}
手动控制事务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 开启事务
tx := db.Begin()

// 在事务中做一些数据库操作 (这里应该使用 'tx' ,而不是 'db')

tx.Create(...)

// ....

// 如果有错误就手动回滚

tx.Rollback()

// 如果无错误就提交事务

tx.Commit()

例子:结合gin框架使用事务来执行银行转账

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
package Transaction

import (
"project/models"

"github.com/gin-gonic/gin"
"gorm.io/gorm"
)

type TransactionController struct{}

func (con TransactionController) Transfer(c *gin.Context) {
// 开启事务
tx := models.DB.Begin()

// 确保在函数结束时回滚事务(如果需要)
defer func() {
if err := recover(); err != nil {
tx.Rollback()
c.JSON(400, gin.H{
"success": false,
"error": "余额不足",
})
} else if tx.Error != nil {
tx.Rollback()
c.JSON(500, gin.H{
"success": false,
"error": tx.Error.Error(),
})
}
}()

// 定义用户余额结构体
var user1, user2 models.Balance

// 查询小明的账户余额
if err := tx.Table("balance").Where("name = ?", "小明").First(&user1).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(404, gin.H{
"success": false,
"error": "小明的账户未找到",
})
} else {
c.JSON(500, gin.H{
"success": false,
"error": err.Error(),
})
}
tx.Rollback()
return
}

// 查询Alice的账户余额
if err := tx.Table("balance").Where("name = ?", "Alice").First(&user2).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(404, gin.H{
"success": false,
"error": "Alice的账户未找到",
})
} else {
c.JSON(500, gin.H{
"success": false,
"error": err.Error(),
})
}
tx.Rollback()
return
}

// 检查小明的余额是否足够
if user1.Money < 2000 {
c.JSON(400, gin.H{
"success": false,
"error": "余额不足",
})
tx.Rollback()
return
}

// 更新小明的账户余额
user1.Money -= 2000
if err := tx.Save(&user1).Error; err != nil {
c.JSON(500, gin.H{
"success": false,
"error": err.Error(),
})
tx.Rollback()
return
}

// 更新Alice的账户余额
user2.Money += 2000
if err := tx.Save(&user2).Error; err != nil {
c.JSON(500, gin.H{
"success": false,
"error": err.Error(),
})
tx.Rollback()
return
}

// 提交事务
tx.Commit()

c.JSON(200, gin.H{
"success": true,
})
}

  • 其中可以使用Find().Error来获取错误码。然后根据错误码来判断是否需要回滚