GORM

SQLBuilder

SQLBuilder是一个用于生成SQL语句的库。

项目地址:

package main
import (
 "database/sql"
 "fmt"
 "log"
 _ "github.com/go-sql-driver/mysql"
 "github.com/huandu/go-sqlbuilder"
)
var db *sql.DB
func init() {
 var err error
 db, err = sql.Open("mysql", "wayne:wayne@/test")
 if err != nil {
 log.Panic(err)
 }
}
// 3 定义结构体
type Emp struct { // 和字段对应的变量或结构体定义,最好和数据库中字段顺序对应
 emp_no     int
 first_name string
 last_name  string
 gender     byte
 birth_date string
 // hire_date string
}

func main() {
 query := sqlbuilder.
 Select("emp_no", "first_name", "last_name", "gender", "birth_date").
 From("employees").
 Where("emp_no > 10015"). // 试一试Where("emp_no > ?")
 Offset(2).Limit(2).
 OrderBy("emp_no").Desc(). // 按照什么字段排序,降序
 String()                  // 输出为字符串,底层调用Build()
 fmt.Println(query)
 rows, err := db.Query(query)
 if err != nil {
 log.Fatal(err)
 }
 for rows.Next() {
 var emp Emp
 err = rows.Scan(&emp.emp_no, &emp.first_name, &emp.last_name,
 &emp.gender, &emp.birth_date) // 字段顺序和select的字段投影顺序一致
 if err != nil {
 log.Fatal(err)
 }
 fmt.Println(emp)
 }
}
SELECT emp_no, first_name, last_name, gender, birth_date FROM employees
WHERE emp_no > 10015 ORDER BY emp_no DESC LIMIT 2 OFFSET 2

本质上sqlbuilder就是在生成SQL语句字符串。

args参数化

builder := sqlbuilder.Select("emp_no", "first_name", "last_name", "gender", "birth_date").From("employees")
builder.Where(
    builder.In("emp_no", 10008, 10010, 10020), // 参数化
)
query, args := builder.Build()
fmt.Printf("%s\n%v\n", query, args) // args是参数
SELECT emp_no, first_name, last_name, gender, birth_date FROM employees
WHERE emp_no IN (?, ?, ?) [10008 10010 10020]
rows, err := db.Query(query, args...) // 这样使用

ORM

对象关系映射,值得是对象和关系之间的银蛇,使用面相对象的方式操作数据库。

关系模型和Go对象之间的映射
table => struct   ,表映射为结构体
row   => object   ,行映射为实例
column => property ,字段映射为属性

举例,有表student,字段为id int 、name varchar、age int

type Student struct {
 id   int
 name string
 age  int
}
Student{100, "Tom", 20}
Student{101, "Jerry", 18}

可以认为ORM是一种高级抽象,对象的操作最终还是会转换成对应关系数据库操作的SQL语句,数据库操作的结构会被封装成对象

GORM

GORM是一个友好的、功能全面的、性能不错的基于GO语言实现的ORM库。

安装

gorm.io/dirver/mysql 依赖github.com/go-sql-driver/mysql,可以认为它是对驱动的再封装。

$ go get -u github.com/go-sql-driver/mysql
$ go get -u gorm.io/gorm
$ go get -u gorm.io/driver/mysql
文档
连接

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

package main
import (
 "fmt"
 "log"
 // _ "github.com/go-sql-driver/mysql" // 不要驱动了吗?
 "gorm.io/driver/mysql"
 "gorm.io/gorm"
)
var db *gorm.DB
func init() {
 var err error
 // dsn := "wayne:wayne@/test"
 dsn := "wayne:wayne@tcp(localhost:3306)/test?charset=utf8mb4"
 db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) // 不要用:=
 if err != nil {
 log.Panicln(err)
 }
 fmt.Println(db) }

上面的代码其实还是需要驱动的

gorm.io/driver/mysql/mysql.go

  • import了"github.com/go-sql-driver/mysql" 也就是说驱动也导入了
  • Dialector的Initalize方法中使用了sql.Open

image

image

模型定义

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

模型是标准的struct,由Go的基本数据类型、实现了Scanner和Valuer接口的自定义类型及其指针或别名组成

例如:

type User struct {
  ID           uint
  Name         string
  Email        *string
  Age          uint8
  Birthday     *time.Time
  MemberNumber sql.NullString
  ActivatedAt  sql.NullTime
  CreatedAt    time.Time
  UpdatedAt    time.Time
}

GORM倾向于约定优先配置

  • 约定使用名为ID的属性会作为主键
  • 约定使用snake_cases作为表名
    • 结构体命名为employee,那么数据库表名就是employees
  • 约定使用snake_case作为字段名,字段首字母大写采用大驼峰
    • 属性名为FirstName,默认对应数据库表的字段名为first_name

如果不遵循以上约定就要自定义配置

// 不符合约定的定义,很多都需要配置,直接用不行
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
}
gorm.Model

GORM定义一个gorm.Model 结构体,其包括字段ID、CreateAt、UpdateAt、DeleteAt

// gorm.Model 的定义
type Model struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
}
表名配置
// 表名并没有遵守约定
func (Emp) TableName() string {
 return "employees"
}
字段配置
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 := "wayne:wayne@/test"
 dsn := "wayne:wayne@tcp(localhost:3306)/test?charset=utf8mb4"
 db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
 Logger: logger.Default.LogMode(logger.Info), // 日志级别,默认为Silent
即打印慢SQL和错误
 }) // 不要用:=
 if err != nil {
 log.Panicln(err)
 }
 fmt.Println(db) }
type Emp struct { // 默认表名emps
 EmpNo     int    `gorm:"primaryKey"` // 默认约束是id为主键,而这里将EmpNo设置为主键,对应库里的字段为emp_no
 FirstName string // 首字母大写,对应字段first_name
 LastName  string
 Gender    byte
 BirthDate string
}
// 表名并没有遵守约定
func (Emp) TableName() string {
 return "employees"
}
func main() {
 var e Emp
 result := db.Take(&e) // 等价于Limit 1,取1条
 fmt.Println(result)
 fmt.Println(result.Error)
 fmt.Println(e)
}

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

列名

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

BirthDate string `gorm:"column:birth_date"` // 可以使用column指定数据库表中的对应字段名
Xyz string `gorm:"column:birth_date"` // 字段名可以不符合约定,但字段名首字母一定要大写
迁移

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

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

// 迁移后,主键默认不为空,其他字段默认都是能为空的
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"`
}
// db.Migrator().DropTable(&Student{})
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是用来生成迁移

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

type Student struct {
 ID       int        // 缺省主键bigint AUTO_INCREMENT
 Name     string     `gorm:"size:48"` //`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 %d", s.ID, s.Name, s.Age) }
新增一条
t := time.Now()
s := Student{Name: "Tom", Age: 20, Birthday: &t}
result := db.Create(&s)
fmt.Println(s)
fmt.Println(result.RowsAffected)
新增多条
// 新增多条
n := time.Now()
s := Student{Name: "Tom", Age: 20, Birthday: &n}
fmt.Println(s)
result := db.Create([]*Student{&s, &s, &s}) // 传入指针的切片
fmt.Println(s)
fmt.Println(result.Error)
fmt.Println(result.RowsAffected)
查询一条

Take 被转换为Limit 1

var s Student
fmt.Println(s) // 零值
r := db.Take(&s) // LIMIT 1
fmt.Println(s) // 被填充
fmt.Println(r)
fmt.Println(r.Error)
r := db.First(&s) // ORDER BY `students`.`id` LIMIT 1
r := db.Last(&s) // ORDER BY `students`.`id` DESC LIMIT 1

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

r := db.First(&s, 15)
var a = Student{Id: 25}
results := db.Take(&a)
fmt.Println(results.RowsAffected)
fmt.Println(a)
时间相关错误

1、时间类型字段

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

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类型
    • dsn := "wayne:wayne@tcp(localhost:3306)/test?charset=utf8mb4&parseTime=true"
  • 也可以Birthday string ,拿到Birthday字符串后,必要时,转换成时间类型,time.Parse()

2、UTC时间

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

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

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

如果使用了loc=Local

  • 存入时,数据库字段中的时间就是当前时区的时间值
  • 读取时,数据库字段中的时间就被解读为当前时区
dsn := "wayne:wayne@tcp(localhost:3306)/test?
charset=utf8mb4&parseTime=true&loc=Local"
// time/zoneinfo.go
func LoadLocation(name string) (*Location, error) {
 if name == "" || name == "UTC" {
 return UTC, nil
 }
 if name == "Local" {
 return Local, nil
 }
 ...省略
}

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

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

查询所有
var students []*Student
r := db.Find(&students)
fmt.Println(r)
fmt.Println(r.Error)
fmt.Println(students)
distinct
var students []*Student
r := db.Distinct("name").Find(&students) // 投影的字段是什么?
fmt.Println(students) // 容器里每个实例是什么样子?

// 输出
2023/07/31 14:34:13 /Users/dujie/Desktop/go12/goproject/mysql-test/main.go:75
[2.602ms] [rows:9] SELECT DISTINCT `name` FROM `students`
[<Id:0 Tom 0 <nil> 0> <Id:0 张三 0 <nil> 0> <Id:0 李四 0 <nil> 0> <Id:0 收到 0 <nil> 0> <Id:0 陈飞 0 <nil> 0> <Id:0 周正 0 <nil> 0> <Id:0 Tom1 0 <nil> 0> <Id:0 Tom2 0 <nil> 0> <Id:0 Tom3 0 <nil> 0>]
投影

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

var students []*Student
r := db.Select("id", "name", "age").Find(&students) r := db.Select([]string{"id", "name", "age"}).Find(&students)
fmt.Println(students)
// 输出
[2.580ms] [rows:27] SELECT `id`,`name`,`age` FROM `students`
[<Id:1 Tom 32 <nil> 0> <Id:2 Tom 32 <nil> 0> <Id:3 Tom 32 <nil> 0> <Id:4 Tom 32 <nil> 0> <Id:5 Tom 32 <nil> 0> <Id:6 Tom 32 <nil> 0> <Id:7 Tom 32 <nil> 0> <Id:8 Tom 32 <nil> 0> <Id:9 Tom 32 <nil> 0> <Id:10 Tom 32 <nil> 0> <Id:11 Tom 0 <nil> 0> <Id:12 Tom 0 <nil> 0> <Id:13 Tom 0 <nil> 0> <Id:14 张三 33 <nil> 0> <Id:15 李四 39 <nil> 0> <Id:16 李四 39 <nil> 0> <Id:17 李四 39 <nil> 0> <Id:18 李四 39 <nil> 0> <Id:19 收到 39 <nil> 0> <Id:20 陈飞 32 <nil> 0> <Id:21 周正 18 <nil> 0> <Id:22 周正 18 <nil> 0> <Id:23 周正 18 <nil> 0> <Id:24 Tom 20 <nil> 0> <Id:25 Tom1 201 <nil> 0> <Id:26 Tom2 202 <nil> 0> <Id:27 Tom3 203 <nil> 0>]
Limit和Offset
    var students []*Student
    db.Limit(2).Offset(2).Find(&students)
    fmt.Println(students) // 容器里每个实例是什么样子?

// 输出
[1.534ms] [rows:2] SELECT * FROM `students` LIMIT 2 OFFSET 2
[<Id:3 Tom 32 <nil> 1> <Id:4 Tom 32 <nil> 1>]
条件查询

1、字符串条件

var stu []*Student
db.Where("name = ?", "陈飞").Find(&stu)
db.Where("name <> ?", "Tom").Find(&stu)                              // 不等于
db.Where("name in (?)", []string{"张三", "陈飞", "周正"}).Find(&stu) //  在...中
db.Where("name like ?", "周%").Find(&stu) // 模糊查询
db.Where("name like binary ?", "T%").Find(&stu)
db.Where("name like ? or name like ?", "张三", "李四").Find(&stu)
db.Where("name like ? or id = ?", "陈飞", 9).Find(&stu)
db.Where("id between ? and ?", 15, 17).Find(&stu)
fmt.Println(stu)

2、struct或map条件

var stu []*Student
db.Where("name = ?", "陈飞").Find(&stu)
db.Where("name <> ?", "Tom").Find(&stu)                              // 不等于
db.Where("name in (?)", []string{"张三", "陈飞", "周正"}).Find(&stu) //  在...中
db.Where("name like ?", "周%").Find(&stu)                            // 模糊查询
db.Where("name like binary ?", "T%").Find(&stu)
db.Where("name like ? or name like ?", "张三", "李四").Find(&stu)
db.Where("name like ? or id = ?", "陈飞", 9).Find(&stu)
db.Where("id between ? and ?", 15, 17).Find(&stu)
db.Where([]int{14, 15, 16}).Find(&stu)
db.Where(&Student{}).Find(&stu)
db.Where(&Student{Id: 18}).Find(&stu)
db.Where(&Student{Id: 19, Name: "陈飞"}).Find(&stu)                   // And
db.Where(map[string]interface{}{"name": "陈飞", "id": 20}).Find(&stu) // And
fmt.Println(stu)

struct条件中出现了零值,例如db.Where(&Student{Name:"tom",Age: 0}) ,Age是零值,就不会出现在条件中。

r := db.Where(&Student{Name: "Tom", Age: 20}, "name", "age").Find(&students) 
// 指定使用结构体里面的name和age字段作为条件,and

3、Not

将Where换成Not即可,表示条件取反

db.Not("id =? or id in (?)", 1, []int{2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}).Find(&stu)
fmt.Println(stu)

4、Or

Or的用法和Where一样

Where().Where() 是And关系,Where().Or()是Or关系

db.Where("name=?", "周正").Or("name=?", "陈飞").Find(&stu)
db.Where("name=?", "周正").Or(&Student{Name: "Tom"}).Find(&stu)
排序
r := db.Order("id desc").Find(&students) // ORDER BY id desc
r := db.Order("name, id desc").Find(&students)         // ORDER BY name,id desc
r := db.Order("name").Order("id desc").Find(&students) // ORDER BY name,id desc
分组
r := db.Group("id").Find(&students) // GROUP BY `id`
r := db.Group("name").Find(&students) // GROUP BY `name`
r := db.Group("id").Group("name").Find(&students) // GROUP BY `id`,`name`
// SELECT name, count(id) as c FROM `students` GROUP BY `name`
r := db.Select("name, count(id) as c").Group("name").Find(&students)
// 但是students中没有属性来保存count的值
// 使用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").Rows()
fmt.Println(err)
// 遍历每一行,填充2个属性的结构体实例
for rows.Next() {
    rows.Scan(&r.name, &r.count)
    fmt.Println(r, "@@@") }
type Result struct { // 和Select的投影字段对应
    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, "@@@") }
// 使用Scan填充容器,注意字段名要大写开头
type Result struct {
    Name string
    C    int // 或Count int `gorm:"column:c"`
}
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) 
}
Join
SELECT
 employees.emp_no, 
 employees.first_name, 
 employees.last_name, 
 salaries.salary
FROM
 employees
 INNER JOIN
 salaries
 ON
 employees.emp_no = salaries.emp_no
type Result struct {
    EmpNo     int
    FirstName string
    LastName  string
    Salary    int
}
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, "###") }
type Result struct {
 EmpNo     int
 FirstName string
 LastName  string
 Salary    int
}
var results = []*Result{}
r := 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(r)
fmt.Println(r.Error)
fmt.Println(r.RowsAffected)
fmt.Println("~~~~~~~~~~~~~~~~~~~~~~~~~~~")
for i, row := range results {
 fmt.Println(i, row) }
type Result struct {
    EmpNo     int
    FirstName string
    LastName  string
    Salary    int
}
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。

// 先查
var student Student
db.First(&student)
fmt.Println(student)
student.Age += 10
student.Name = "Sam"
// 后修改
db.Save(&student)
fmt.Println(student)

Update 单个字段

r := db.Model(&Student{ID: 13}).Update("age", 11) // 更新符合条件的所有记录的一个
字段
// UPDATE `students` SET `age`=11 WHERE `id` = 13
r := db.Model(&Student{}).Update("age", 11) // 没有指定ID或Where条件,是全表更新
age字段,这是非常危险的
fmt.Println(r.Error) // 会报WHERE conditions required错误,更新失败,这是一种保护

Update更新多列

多个键值对,使用map或结构体实例传参

同样,没有指定ID或where条件,是全表更新age字段,这是非常危险的,报WHERE conditions required错误

r := db.Model(&Student{}).Where("age < ?", 20).Updates(map[string]interface{}{"name": "John", "age": 23})
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

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

result := db.Delete(&Student{})
fmt.Println(result.Error)
// 报WHERE conditions required错误,这是全表删除,危险
result := db.Delete(&Student{}, 15) // 指定主键
fmt.Println(result.Error)
db.Delete(&Student{}, []int{15, 16, 18}) // DELETE FROM `students` WHERE `students`.`id` IN (15,16,18)
result := db.Where("id > ?", 15).Delete(&Student{}) // 删除符合条件的一批
fmt.Println(result.Error)