GO GORM库

GORM

gorm是Golang语言中一款性能极好的ORM库,对开发人员相对是比较友好的。它提供了强大的功能和简洁的 API,让数据库操作变得更加简单和易维护。

文档

英文 https://gorm.io/docs/

中文 https://gorm.io/zh_CN/docs/index.html

安装

1
2
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite

连接

https://gorm.io/zh_CN/docs/connecting_to_the_database.html#MySQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"log"

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

func main() {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalln(err)
}
fmt.Println(db)
}

模型定义

https://gorm.io/zh_CN/docs/models.html

GORM 倾向于约定优于配置,如果不遵从约定就要写自定义配置

  • 使用名为ID的属性会作为主键
  • 使用snake_cases作为表名
    • 结构体命名为employee,那么数据库表名就是employees
  • 使用snake_case作为字段名,字段首字母大写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 不符合约定的定义,很多都需要配置,直接用不行
type Emp struct { // 默认表名emps
emp_no int // 不是ID为主键,需要配置
first_name string // 首字母未大写,也需要配置
last_name string
gender byte
birth_date string
}

// 符合约定的定义如下
type student struct { // 默认表名students
ID int // Id也可以
Name string // 字段首字母要大写
Age int
}

表名配置

1
2
3
4
// 表名并没有遵守约定
func (Emp) TableName() string {
return "employees"
}

字段配置

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
package main

import (
"fmt"
"log"

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

var db *gorm.DB

func init() {
var err error
dsn := "root:123456@tcp(localhost:3306)/test?charset=utf8mb4"
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
}) // 不要用:=
if err != nil {
log.Fatalln(err)
}
fmt.Println(db)
}

type Emp struct {
emp_no int `gorm:"primarykey"` // 不是ID为主键
First_name string // 首字母大写,对应字段first_name
Last_name string
Gender int16
Birth_date string //BirthDate string `gorm:"column:birth_date"`
}

type Student struct {
ID int //主键
Name string
Age byte
BirthDate string
Gender byte
}

// 表名并没有遵守约定
func (Emp) TableName() string {
return "employees"
}

func main() {
var e Emp
result := db.Take(&e) // 等价于Limit 1,取1条数据
fmt.Println(result.Error)
fmt.Println(result.RowsAffected)
fmt.Println(e)
}

使用 gorm:"primaryKey" 来指定字段为主键,默认使用名为ID的属性作为主键。primaryKey是tag名,大小写不敏感,但建议小驼峰。

列名

如果未按照约定定义字段,需要定义结构体属性时指定数据库字段名称是什么。

1
2
BirthDate string `gorm:"column:birth_date"` // 字段名可以不符合约定,但字段名首字母一定要大写
Xyz string `gorm:"column:birth_date"` // 可以用注解映射在数据库中的列名

GORM常用操作

https://gorm.io/zh_CN/docs/migration.html#%E8%A1%A8

下面,新建一个students表,来看看结构体中属性类型和数据库表中字段类型的对应关系

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

type Student struct {
ID int // 缺省主键bigint AUTO_INCREMENT
Name string `gorm:"not null;type:varchar(48);comment:姓名"`
Age byte // byte=>tinyint unsigned
Birthday time.Time // datetime
Gender byte `gorm:"type:tinyint"`
}

func main() {
db.Migrator().CreateTable(&Student{})
}

/** 对应建表语句
CREATE TABLE `students` (
`id` bigint AUTO_INCREMENT,
`name` varchar(48) NOT NULL COMMENT '姓名',
`age` tinyint unsigned,
`birthday` datetime(3) NULL,
`gender` tinyint,
PRIMARY KEY (`id`)
)
**/

由于int => bigint、string => longtext,这些默认转换不符合我们的要求,所以,在tag中使用type指定字段类型。

属性是用来构建结构体实例的,生成的SQL语句也要使用这些数据。而tag是用来生成迁移

1
2
3
Name     string    `gorm:"size:48"` // 定义为varchar(48)
Age int `gorm:"size:32"` // 定义为4字节的int
Age int `gorm:"size:64"` // 定义为8字节的bigint

迁移用的较少,主要是理解其作用。

结构体属性类型用来封装实例的数据,Tag中类型指定迁移到数据库表中字段的类型

新增记录

参考 https://gorm.io/zh_CN/docs/create.html#%E5%88%9B%E5%BB%BA%E8%AE%B0%E5%BD%95

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
package main

import (
"fmt"
"log"
"time"

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

var db *gorm.DB

func init() {
var err error
dsn := "root:123456@tcp(localhost:3306)/test?charset=utf8mb4"
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
}) // 不要用:=
if err != nil {
log.Fatalln(err)
}
fmt.Println(db)
}

type Student struct {
ID int // 缺省主键bigint AUTO_INCREMENT
Name string `gorm:"not null;type:varchar(48);comment:姓名"`
Age byte // byte=>tinyint unsigned
Birthday time.Time // datetime
Gender byte `gorm:"type:tinyint"`
}

func (s *Student) string() string {
return fmt.Sprintf("%d", s.ID)
}

func main() {
// 建表
// db.Migrator().CreateTable(&Student{})

// 新增一条记录
n := time.Now()
s := Student{Name: "Tom", Age: 23, Birthday: n}
fmt.Println(s)
result := db.Create(&s) // 新增,传入指针
fmt.Println(s) // 注意前后ID的变化
fmt.Println(result.Error)
fmt.Println(result.RowsAffected)

// 新增多条记录
n = time.Now()
s = Student{Name: "Tom", Age: 23, Birthday: n}
fmt.Println(s)
result = db.Create([]*Student{&s, &s, &s, &s}) // 传入指针切片
fmt.Println(s)
fmt.Println(result.Error)
fmt.Println(result.RowsAffected)
}

查询一条记录

Take被转换为Limit 1

1
2
3
4
5
6
7
8
9
10
func main() {
var s = Student
fmt.Println(s) // 零值
row := db.Take(&s) // Limit 1
fmt.Println(s)
fmt.Println(row)
fmt.Println(row.Error)
row.Scan(&s)
fmt.Println(s)
}
1
2
row := db.First(&s) // ORDER BY `students`.`id` LIMIT 1
row := db.Last(&s) // ORDER BY `students`.`id` DESC LIMIT 1

根据id查,可以使用下面的方式

1
2
3
4
row := db.First(&s, 15)

var s = Student{ID: 16}
row := db.First(&s)

时间相关错误

1、时间类型字段

上例错误如下,主要是使用了*time.Time,而不是string。

1
2
sql: Scan error on column index 3, name "birthday": unsupported Scan, storing driver.Value type []uint8 into type *time.Time
[]byte 转 *time.Time失败了

解决方案

在连接字符串中增加parseTime=true,这样时间类型就会自动转化为time.Time类型

1
dsn := "root:123456@tcp(localhost:3306)/test?charset=utf8mb4&parseTime=true"

也可以 Birthday string ,拿到Birthday字符串后,必要时,自行转换成时间类型

2、UTC时间

Create写入的时间,也就是说time.Now()取当前时区时间,但是存入数据库的时间是UTC时间。

Take拿回的时间也是UTC时间,可以通过s.Birthday.Local()转成当前时区时间。

如果想存入的时间或读取的时间直接是当前时区时间,可以使用loc参数loc=Local。

如果loc=Local

  • 存入时,数据库字段中的时间就是当前时区的时间值
  • 读取时,数据库字段中的时间就被解读为当前时区
1
2
3
4
5
6
7
8
9
dsn := "root:123456@tcp(localhost:3306)/test?charset=utf8mb4&parseTime=true&loc=Local"
func LoadLocation(name string) (*Location, error) {
if name == "" || name == "UTC" {
return UTC, nil
}
if name == "Local" {
return Local, nil
}
}

千万不要存入数据库时采用Local存入,却使用UTC解读时间,会造成时间时区的混乱。应该保证时间存入、读取时区一致。

一定要统一项目中数据库中时间类型字段的时区。可以考虑统一采用UTC,为了本地化显示转换为当前时区即可。

查询所有数据

1
2
3
4
5
6
7
8
9
10
var students []*Student
db.Find(&students)
rows := db.Find(&students)
fmt.Println(students)
fmt.Println(rows)
fmt.Println(rows.Error)

for _, i := range students {
fmt.Println(*i)
}

distinct

1
rows := db.Distinct("name").Find(&students)

投影

投影是关系模型的操作,就是选择哪些字段

1
2
rows := db.Select("id", "name", "age").Find(&students)
rows := db.Select([]string{"id", "name", "age"}).Find(&students)

Limit和Offset

1
2
var students []*Student
rows := db.Limit(2).Offset(2).Find(&students)

条件查询

1、字符串条件

1
2
3
4
5
6
7
8
9
var students []*Student
rows := db.Where("name=?", "Tom").Find(&students)
rows := db.Where("name <> ?", "Tom").Find(&students)
rows := db.Where("name in ?", []string{"jerry", "tom"}).Find(&students)
rows := db.Where("name like ?", "t%").Find(&students)
rows := db.Where("name like binary ?", "T%").Find(&students)
rows := db.Where("name like ? and age >?", "t%", 20).Find(&students)
rows := db.Where("id between ? and ?", 15, 17).Find(&students)
rows := db.Where("id = ? or id = ?", 15, 17).Find(&students)

2、struct 或map 条件

1
2
3
4
5
6
7
var students []*Student
rows := db.Where([]int{14, 16, 17}).Find(&students) // WHERE students.id IN (14,16,17)
rows := db.Where(&Student{}).Find(&students) // 查询所有数据
rows := db.Where(&Student{ID: 5}).Find(&students)
rows := db.Where(&Student{ID: 5, Name: "Tom"}).Find(&students)
rows := db.Where(map[string]interface{}{"name": "Tom", "id": 5}).Find(&students)
rows := db.Select([]string{"id", "name", "age"}).Find(&students)

3、Not

1
2
3
var students []*Student
rows := db.Not("id = ? or id = ?", 5, 1).Find(&students)
rows := db.Not("name = ?", "Tom").Find(&students)

4、Or

Or的用法和Where一样,Where().Where()是And关系,Where().Or()是Or关系

1
2
3
var students []*Student
rows := db.Where("name = ?", "Tom").Or("name = ?", "Jerry").Find(&students)
rows := db.Where("name = ?", "Tom").Or(&Student{Name: "Jerry"}).Find(&students)

排序

1
2
3
4
var students []*Student
rows := db.Order("id desc").Find(&students)
rows := db.Order("name,id desc").Find(&students)
rows := db.Order("name").Order("id desc").Find(&students)

分组

1
2
3
4
5
6
var students []*Student
rows := db.Group("id").Find(&students)
rows := db.Group("name").Group("id").Find(&students)

// 但是students中没有属性来保存count的值
rows := db.Select("name,count(id) as c").Group("name").Find(&students)

可以使用以下方式保存count的值

1
2
3
4
5
6
7
8
9
10
11
// 使用Scan填充容器
type Result struct {
Name string
C int
}

var rows = []*Result{}
db.Table("students").Select("name,count(id) as c").Group("name").Having("c > 3").Scan(&rows)
for i, r := range rows {
fmt.Printf("%d, %T %#[2]v\n", i, r)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用Rows()返回所有行,自行获取字段值,但是要用Table指定表名
type Result struct {
name string
count int
}

var r = Result{}
rows, err := db.Table("students").Select("name,count(id) as c").Group("name").Having("c > 3").Rows()
fmt.Println(err)
// 遍历每一行,填充2个属性的结构体实例
for rows.Next() {
rows.Scan(&r.name, &r.count)
fmt.Println(r, "===")
}

Join

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
// Join
type Result struct {
EmpNo int
FirstName string
LastName string
Salary int
}

// Join 1
var results = []*Result{}
rows := db.Table("employees as e").Select("e.emp_no,first_name,last_name,salary").
Joins("join salaries on e.emp_no = salaries.emp_no").Find(&results)
fmt.Println(rows)
fmt.Println(rows.RowsAffected)
for i, r := range results {
fmt.Println(i, *r)
}

// Join 2
rows, err := db.Table("employees as e").Select("e.emp_no,first_name,last_name,salary").
Joins("join salaries as s on e.emp_no = s.emp_no").Rows()
fmt.Println(err)
var r Result
for rows.Next() {
rows.Scan(&r.EmpNo, &r.FirstName, &r.LastName, &r.Salary)
fmt.Println(r, "===")
}

// Join 3
var results = []*Result{}
db.Table("employees as e").Select("e.emp_no,first_name,last_name,salary").
Joins("join salaries as s on e.emp_no = s.emp_no").Scan(&results)
for i, r := range results {
fmt.Println(i, *r)
}

更新

https://gorm.io/zh_CN/docs/update.html

先查后改:先查到一个实例,对这个实例属性进行修改,然后调用db.Save()方法保存。

db.Save()方法会保存所有字段,对于没有主键的实例相当于Insert into,有主键的实例相当于Update。

1
2
3
4
5
6
7
8
9
10
11
// 更新
// 先查
var student Student
db.First(&student)
fmt.Println(student)

student.Age += 10
student.Name = "Jerry"
// 后改
db.Save(&student)
fmt.Println(student)

Update 更新单个字段

1
2
3
4
5
6
// Update 更新单个字段
db.Model(&Student{ID: 5}).Update("age", 19) // 更新符合条件的所有记录的一个字段
// UPDATE `students` SET `age`=5 WHERE `id` = 19

r := db.Model(&Student{}).Update("age", 20) // 没有指定ID或where条件,是全表更新age字段,这是非常危险的
fmt.Println(r.Error)

Updates 更新多列

1
2
3
4
5
6
// Updates 更新多个字段
r := db.Model(&Student{}).Where("age < ?", 20).Updates(map[string]interface{}{"name": "John", "age": 25})
fmt.Println(r.Error)

r := db.Model(&Student{}).Where("age < ?", 24).Updates(Student{Name: "John", Age: 18})
fmt.Println(r.Error)

删除

https://gorm.io/zh_CN/docs/delete.html

删除操作是危险的,慎重操作!

1
2
3
4
5
6
7
8
9
10
11
12
// 删除
result := db.Delete(&Student{})
fmt.Println(result.Error)
// 报WHERE conditions required错误,这是全表删除,危险

result = db.Delete(&Student{}, 5) // 指定主键
fmt.Println(result.Error)

db.Delete(&Student{}, []int{2, 3, 4}) // DELETE FROM `students` WHERE `students`.`id` IN (15,16,18)

result = db.Where("id > ?", 5).Delete(&Student{})
fmt.Println(result.Error)
-------------本文结束感谢您的阅读-------------