ORM教程 ---以GORM为例&go的数据库操作

GORM是Go语言中常用的ORM框架,它简化了数据库操作,提供了对象关系映射功能。本文从安装、连接数据库、模型定义、单表操作、查询、事务、一对多关系等方面全面解析GORM的使用,并展示了如何通过预加载、事务处理、自定义日志等高级特性来提高开发效率。此外,还讨论了GORM在处理SQL注入、性能优化和事务一致性等方面的优势和注意事项。
摘要由CSDN通过智能技术生成

参考资料:
【GORM简明教程】关于GORM你看这一个就够了

【gorm文档】

【golang最简单的gorm教程】

【官方中文文档】

gorm v2中文文档

一对一、多对多、自定义类型的 目前懒得写 == 下次补上吧 https://docs.fengfengzhidao.com/#/docs/gorm%E6%96%87%E6%A1%A3/1.%E8%BF%9E%E6%8E%A5

介绍

其实官方中文文档已经很全了,但是顺序比较奇怪,所以按照【golang最简单的gorm教程】整理如下
在这里插入图片描述

什么是 ORM

ORM 是 Object Relational Mapping 的缩写,译为“对象关系映射”,它解决了对象和关系型数据库之间的数据交互问题。

简单说就是 使用一个类表示一张表,类中的属性表示表的字段,类的实例化对象表示一条记录,我们可以通过使用对象的方法操作数据库

特别是对强类型语言而言

demo-直观印象

orm操作

在这里插入图片描述
相当于sql语句的insert into
这样 整个编程就是面向对象的思维了。而且不要求程序员既会sql语句又会语言本身。很多程序员也不爱写sql语句
在这里插入图片描述

在进行下面的学习之前 我们先看看原生的方法有多么麻烦!

《golang 非orm数据库的操作与对比 database/sql、sqlx 和 sqlc》

本文后面的 原生方法对比orm 一节也有所展示

优缺点

和自动生成 SQL语句相比,手动编写 SQL语句的缺点是非常明显的,主要体现在以下几个方面:

  • 对象的属性名和数据表的字段名往往不一致,我们在编写 SQL语句时需要非常小心,要逐一核对属性名和字段名,确保它们不会出错,而且彼此之间要一一对应。
  • 此外,当SQL语句出错时,数把库的提示信息往往也不精准,这给排错带来了不小的困难
  • 不同的数据库,对应的 sql语句也不太一样
  • sql注入问题
  • orm可以提高开发效率

当然,使用 orm 也不全是优点,ORM 缺点:

  • ORM 增加了大家的学习成本,为了使用 ORM 技术,您至少需要掌握一种 ORM 框架
  • 自动生成 SQL语句会消耗计算资源,这势必会对程序性能造成一定的影响。
  • 对于复杂的教据障操作,ORM 通常难以处理,即使能处理,自动生成的SQL语句在性能方面也不如手写的原生 SQL。
  • 弱化sql能力

每一门语言都有对应的 ORM 柜架

python: SQLAlchemy DjangoORM
Java: Hibernate Mybatis
Golang: GORM

版本区别

新出了个V2版 部分和v1可能不兼容 比如v2版取消了close方法

插播–如何找资料

如果搜索引擎查不到 去github搜 然后里面会有介绍、官网之类的信息

连接

需要下载mysql的驱动

go get gorm.io/driver/mysql
go get gorm.io/gorm

简单连接

username := "root"  //账号
password := "root"  //密码
host := "127.0.0.1" //数据库地址,可以是Ip或者域名
port := 3306        //数据库端口
Dbname := "gorm"   //数据库名
timeout := "10s"    //连接超时,10秒

// root:root@tcp(127.0.0.1:3306)/gorm?
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=%s", username, password, host, port, Dbname, timeout)
//连接MYSQL, 获得DB类型实例,用于后面的数据库读写操作。
db, err := gorm.Open(mysql.Open(dsn))
if err != nil {
  panic("连接数据库失败, error=" + err.Error())
}
// 连接成功
fmt.Println(db)

高级配置

close

GORM决定在1.20版中取消Close()方法,因为GORM支持连接池,因此正确的用法是打开连接并在应用程序中共享它。

如果您的特定用例仍然需要使用Close()方法,GORM提供了返回db generic_interface的方法DB,您可以在其中使用它。

示例

sqlDB, err := db.DB()
// Close
defer sqlDB.Close()

跳过默认事务

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

db, err := gorm.Open(mysql.Open(dsn_str), &gorm.Config{
  SkipDefaultTransaction: true,
})

命名策略与自动建表

gorm采用的命名策略是,表名是蛇形复数,字段名是蛇形单数
结构体名 大驼峰->小蛇形复数
字段名 大驼峰->小蛇形单数

例如

type Student struct {
  Name      string
  Age       int
  MyStudent string
}

gorm会为我们这样生成表结构

CREATE TABLE `students` (`name` longtext,`age` bigint,`my_student` longtext)
CopyErrorOK!

我们也可以修改这些策略

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
  NamingStrategy: schema.NamingStrategy{
    TablePrefix:   "f_",  // 表名前缀
    SingularTable: false, // 单数表名
    NoLowerCase:   false, // 关闭小写转换
  },
})
//例如 
DB.AutoMigrate(&Student{})

相当于把结构体变成数据库中的表,如果数据库中不存在表就创建;如果存在就更新;同时还会自动调整约束索引之类
gorm帮你创建表的时候 比如UserInfo 会自动变成 user_infos 下划线连接和加一个s。你也可以自己设置规则。还会自动将id设置为主键
在这里插入图片描述

更详细的看后面的 自动生成表结构AutoMigrate 一节

显示日志

gorm的默认日志是只打印错误和慢SQL

我们可以自己设置

var mysqlLogger logger.Interface
// 要显示的日志等级
mysqlLogger = logger.Default.LogMode(logger.Info)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
  Logger: mysqlLogger,
})

如果你想自定义日志的显示

那么可以使用如下代码

newLogger := logger.New(
  log.New(os.Stdout, "\r\n", log.LstdFlags), // (日志输出的目标,前缀和日志包含的内容)
  logger.Config{
    SlowThreshold:             time.Second, // 慢 SQL 阈值
    LogLevel:                  logger.Info, // 日志级别
    IgnoreRecordNotFoundError: true,        // 忽略ErrRecordNotFound(记录未找到)错误
    Colorful:                  true,        // 使用彩色打印
  },
)

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
  Logger: newLogger,
})

部分展示日志

var model Student
session := DB.Session(&gorm.Session{Logger: newLogger})
session.First(&model)
// SELECT * FROM `students` ORDER BY `students`.`name` LIMIT 1

如果只想某些语句显示日志

DB.Debug().First(&model)

模型定义

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

定义一张表

type Student struct {
  ID    uint // 默认使用ID作为主键
  Name  string
  Email *string // 使用指针是为了存空值
}

常识:小写属性是不会生成字段的

自动生成表结构AutoMigrate

结合连接高级配置那里看

// 可以放多个
DB.AutoMigrate(&Student{})

AutoMigrate的逻辑是只新增,不删除,不修改(大小会修改)

例如将Name修改为Name1,进行迁移,会多出一个name1的字段

生成的表结构如下

CREATE TABLE `f_students` (`id` bigint unsigned AUTO_INCREMENT,`name` longtext,`email` longtext,PRIMARY KEY (`id`))

默认的类型太大了

修改字段大小

我们可以使用gorm的标签进行修改

有两种方式

Name  string  `gorm:"type:varchar(12)"`
Name  string  `gorm:"size:2"`

字段标签

type 定义字段类型
size 定义字段大小
column 自定义列名
primaryKey 将列定义为主键
unique 将列定义为唯一键
default 定义列的默认值
not null 不可为空
embedded 嵌套字段
embeddedPrefix 嵌套字段前缀
comment 注释

多个标签之前用 ; 连接

type StudentInfo struct {
  Email  *string `gorm:"size:32"` // 使用指针是为了存空值
  Addr   string  `gorm:"column:y_addr;size:16"`
  Gender bool    `gorm:"default:true"`
}
type Student struct {
  Name string      `gorm:"type:varchar(12);not null;comment:用户名"`
  UUID string      `gorm:"primaryKey;unique;comment:主键"`
  Info StudentInfo `gorm:"embedded;embeddedPrefix:s_"`
}

// 建表语句
CREATE TABLE `students` (
    `name` varchar(12) NOT NULL COMMENT '用户名',
    `uuid` varchar(191) UNIQUE COMMENT '主键',
    `s_email` varchar(32),
    `s_y_addr` varchar(16),
    `s_gender` boolean DEFAULT true,
    PRIMARY KEY (`uuid`)
)

内置的默认model

在这里插入图片描述
模型定义示例

type User struct {
  gorm.Model
  Name         string
  Age          sql.NullInt64
  Birthday     *time.Time
  Email        string  `gorm:"type:varchar(100);unique_index"`
  Role         string  `gorm:"size:255"` // 设置字段大小为255
  MemberNumber *string `gorm:"unique;not null"` // 设置会员号(member number)唯一并且不为空
  Num          int     `gorm:"AUTO_INCREMENT"` // 设置 num 为自增类型
  Address      string  `gorm:"index:addr"` // 给address字段创建名为addr的索引
  IgnoreMe     int     `gorm:"-"` // 忽略本字段
}

在这里插入图片描述

单表操作

先使用gorm对单张表进行增删改查

表结构

type Student struct {
  ID     uint   `gorm:"size:3"`
  Name   string `gorm:"size:8"`
  Age    int    `gorm:"size:3"`
  Gender bool
  Email  *string `gorm:"size:32"`
}

添加记录

email := "xxx@qq.com"
// 创建记录
student := Student{
  Name:   "枫枫",
  Age:    21,
  Gender: true,
  Email:  &email,
}
DB.Create(&student)

有两个地方需要注意

  • 指针类型是为了更好的存null类型,但是传值的时候,也记得传指针
  • Create接收的是一个指针,而不是值

由于我们传递的是一个指针,调用完Create之后,student这个对象上面就有该记录的信息了,如创建的id

DB.Create(&student)
fmt.Printf("%#v\n", student)  
// main.Student{ID:0x2, Name:"zhangsan", Age:23, Gender:false, Email:(*string)(0x11d40980)}

批量插入

Create方法还可以用于插入多条记录

var studentList []Student
for i := 0; i < 100; i++ {
  studentList = append(studentList, Student{
    Name:   fmt.Sprintf("机器人%d号", i+1),
    Age:    21,
    Gender: true,
    Email:  &email,
  })
}
DB.Create(&studentList)

查询单条记录Take、First、Last

不推荐使用find(sql同名方法)

如果你想避免ErrRecordNotFound错误(后文会详细讲),你可以使用Find,比如db.Limit(1).Find(&user),Find方法可以接受struct和slice的数据。

对单个对象使用Find而不带limit,db.Find(&user)将会查询整个表并且只返回第一个对象,这是性能不高并且不确定的。

感觉设计的不太好

简单示例

获取单条记录的方法很多,我们对比sql就很直观了

DB = DB.Session(&gorm.Session{Logger: Log})
var student Student
DB.Take(&student)  
// SELECT * FROM `students` LIMIT 1
DB.First(&student) 
// SELECT * FROM `students` ORDER BY `students`.`id` LIMIT 1
DB.Last(&student)  
// SELECT * FROM `students` ORDER BY `students`.`id` DESC LIMIT 1

根据主键查询

var student Student
DB.Take(&student, 2)
fmt.Println(student)

student = Student{} // 重新赋值
DB.Take(&student, "4")
fmt.Println(student)

Take的第二个参数,默认会根据主键查询,可以是字符串,可以是数字

根据其他条件查询Take

var student Student
DB.Take(&student, "name = ?", "机器人27号")
fmt.Println(student)

使用?作为占位符,将查询的内容放入?

SELECT * FROM `students` WHERE name = '机器人27号' LIMIT 1

这样可以有效的防止sql注入

他的原理就是将参数全部转义,如

DB.Take(&student, "name = ?", "机器人27号' or 1=1;#")

SELECT * FROM `students` WHERE name = '机器人27号\' or 1=1;#' LIMIT 1

根据struct查询

var student Student
// 只能有一个主要值
student.ID = 2
//student.Name = "枫枫"
DB.Take(&student)
fmt.Println(student)

获取查询结果

获取查询的记录数
count := DB.Find(&studentList).RowsAffected
是否查询失败
err := DB.Find(&studentList).Error

查询失败有:

  • 查询为空
  • 查询条件错误
  • sql语法错误

可以使用判断

var student Student
err := DB.Take(&student, "xx").Error
switch err {
case gorm.ErrRecordNotFound:
  fmt.Println("没有找到")
default:
  fmt.Println("sql错误")
}


//或者
result := db.First(&user)
result.RowsAffected // 返回找到的记录数
result.Error        // returns error or nil

// 检查 ErrRecordNotFound 错误
errors.Is(result.Error, gorm.ErrRecordNotFound)


如果你想避免ErrRecordNotFound错误,你可以使用Find,比如db.Limit(1).Find(&user),Find方法可以接受struct和slice的数据。

查询多条记录Find

var studentList []Student
DB.Find(&studentList)
for _, student := range studentList {
  fmt.Println(student)
}

// 由于email是指针类型,所以看不到实际的内容
// 但是序列化之后,会转换为我们可以看得懂的方式
var studentList []Student
DB.Find(&studentList)
for _, student := range studentList {

  data, _ := json.Marshal(student)
  fmt.Println(string(data))
}
根据主键列表查询
var studentList []Student
DB.Find(&studentList, []int{1, 3, 5, 7})
DB.Find(&studentList, 1, 3, 5, 7)  // 一样的
fmt.Println(studentList)
根据其他条件查询
DB.Find(&studentList, "name in ?", []string{"枫枫", "zhangsan"})

更新

更新的前提的先查询到记录

单个记录的全字段更新Save

它会保存所有字段,即使零值也会保存

var student Student
DB.Take(&student)
student.Age = 23
// 全字段更新
DB.Save(&student)
// UPDATE `students` SET `name`='枫枫',`age`=23,`gender`=true,`email`='xxx@qq.com' WHERE `id` = 1

零值也会更新

var student Student
DB.Take(&student)
student.Age = 0
// 全字段更新
DB.Save(&student)
// UPDATE `students` SET `name`='枫枫',`age`=0,`gender`=true,`email`='xxx@qq.com' WHERE `id` = 1

更新指定字段Select

可以使用select选择要更新的字段

var student Student
DB.Take(&student)
student.Age = 21
// 全字段更新
DB.Select("age").Save(&student)
// UPDATE `students` SET `age`=21 WHERE `id` = 1

批量更新Update

例如我想给年龄21的学生,都更新一下邮箱

var studentList []Student
DB.Find(&studentList, "age = ?", 21).Update("email", "is21@qq.com")

还有一种更简单的方式

DB.Model(&Student{}).Where("age = ?", 21).Update("email", "is21@qq.com")
// UPDATE `students` SET `email`='is22@qq.com' WHERE age = 21

这样的更新方式也是可以更新零值的

更新多列

如果是结构体,它默认不会更新零值

email := "xxx@qq.com"
DB.Model(&Student{}).Where("age = ?", 21).Updates(Student{
  Email:  &email,
  Gender: false,  // 这个不会更新
})

// UPDATE `students` SET `email`='xxx@qq.com' WHERE age = 21

如果想让他更新零值,用select就好

email := "xxx1@qq.com"
DB.Model(&Student{}).Where("age = ?", 21).Select("gender", "email").Updates(Student{
  Email:  &email,
  Gender: false,
})
// UPDATE `students` SET `gender`=false,`email`='xxx1@qq.com' WHERE age = 21

如果不想多写几行代码,则推荐使用map

DB.Model(&Student{}).Where("age = ?", 21).Updates(map[string]any{
  "email":  &email,
  "gender": false,
})
更新选定字段

Select选定字段
Omit忽略字段

删除Delete

根据结构体删除

// student 的 ID 是 `10`
db.Delete(&student)
// DELETE from students where id = 10;

删除多个

db.Delete(&Student{}, []int{1,2,3})

// 查询到的切片列表
db.Delete(&studentList)

创建hook

在插入一条记录到数据库的时候,我希望做点事情

type Student struct {
  ID     uint   `gorm:"size:3"`
  Name   string `gorm:"size:8"`
  Age    int    `gorm:"size:3"`
  Gender bool
  Email  *string `gorm:"size:32"`
}

func (user *Student) BeforeCreate(tx *gorm.DB) (err error) {
  email := fmt.Sprintf("%s@qq.com", user.Name)
  user.Email = &email
  return nil
}

高级查询

重新构造一些数据用于查询

func main(){
  var studentList []Student
  DB.Find(&studentList).Delete(&studentList)
  studentList = []Student{
    {ID: 1, Name: "李元芳", Age: 32, Email: PtrString("lyf@yf.com"), Gender: true},
    {ID: 2, Name: "张武", Age: 18, Email: PtrString("zhangwu@lly.cn"), Gender: true},
    {ID: 3, Name: "枫枫", Age: 23, Email: PtrString("ff@yahoo.com"), Gender: true},
    {ID: 4, Name: "刘大", Age: 54, Email: PtrString("liuda@qq.com"), Gender: true},
    {ID: 5, Name: "李武", Age: 23, Email: PtrString("liwu@lly.cn"), Gender: true},
    {ID: 6, Name: "李琦", Age: 14, Email: PtrString("liqi@lly.cn"), Gender: false},
    {ID: 7, Name: "晓梅", Age: 25, Email: PtrString("xiaomeo@sl.com"), Gender: false},
    {ID: 8, Name: "如燕", Age: 26, Email: PtrString("ruyan@yf.com"), Gender: false},
    {ID: 9, Name: "魔灵", Age: 21, Email: PtrString("moling@sl.com"), Gender: true},
  }
  DB.Create(&studentList)
}

func PtrString(email string) *string {
  return &email
}

Where

等价于sql语句中的where

var users []Student
// 查询用户名是枫枫的
DB.Where("name = ?", "枫枫").Find(&users)
fmt.Println(users)
// 查询用户名不是枫枫的
DB.Where("name <> ?", "枫枫").Find(&users)
fmt.Println(users)
// 查询用户名包含 如燕,李元芳的
DB.Where("name in ?", []string{"如燕", "李元芳"}).Find(&users)
fmt.Println(users)
// 查询姓李的
DB.Where("name like ?", "李%").Find(&users)
fmt.Println(users)
// 查询年龄大于23,是qq邮箱的
DB.Where("age > ? and email like ?", "23", "%@qq.com").Find(&users)
fmt.Println(users)
// 查询是qq邮箱的,或者是女的
DB.Where("gender = ? or email like ?", false, "%@qq.com").Find(&users)
fmt.Println(users)

使用结构体查询

使用结构体查询,会过滤零值

并且结构体中的条件都是and关系

// 会过滤零值
DB.Where(&Student{Name: "李元芳", Age: 0}).Find(&users)
fmt.Println(users)

使用map查询

不会过滤零值

DB.Where(map[string]any{"name": "李元芳", "age": 0}).Find(&users)
// SELECT * FROM `students` WHERE `age` = 0 AND `name` = '李元芳'
fmt.Println(users)

Not条件

和where中的not等价

// 排除年龄大于23的
DB.Not("age > 23").Find(&users)
fmt.Println(users)

Or条件

和where中的or等价

DB.Or("gender = ?", false).Or(" email like ?", "%@qq.com").Find(&users)
fmt.Println(users)

Select 选择字段与Scan

DB.Select("name", "age").Find(&users)
fmt.Println(users)
// 没有被选中,会被赋零值

可以使用扫描Scan,将选择的字段存入另一个结构体中

type User struct {
  Name string
  Age  int
}
var students []Student
var users []User
DB.Select("name", "age").Find(&students).Scan(&users)
fmt.Println(users)

这样写也是可以的,不过最终会查询两次,还是不这样写

SELECT `name`,`age` FROM `students`
SELECT `name`,`age` FROM `students`

这样写就只查询一次了

type User struct {
  Name string
  Age  int
}
var users []User
DB.Model(&Student{}).Select("name", "age").Scan(&users)
fmt.Println(users)

还可以这样

var users []User
DB.Table("students").Select("name", "age").Scan(&users)
fmt.Println(users)

Scan是根据column列名进行扫描的

type User struct {
  Name123 string `gorm:"column:name"`
  Age     int
}
var users []User
DB.Table("students").Select("name", "age").Scan(&users)
fmt.Println(users)

排序

根据年龄倒序

var users []Student
DB.Order("age desc").Find(&users)
fmt.Println(users)
// desc    降序
// asc     升序

注意order的顺序

分页查询

var users []Student
// 一页两条,第1页
DB.Limit(2).Offset(0).Find(&users)
fmt.Println(users)
// 第2页
DB.Limit(2).Offset(2).Find(&users)
fmt.Println(users)
// 第3页
DB.Limit(2).Offset(4).Find(&users)
fmt.Println(users)

通用写法

var users []Student
// 一页多少条
limit := 2
// 第几页
page := 1
offset := (page - 1) * limit
DB.Limit(limit).Offset(offset).Find(&users)
fmt.Println(users)

去重

var ageList []int
DB.Table("students").Select("age").Distinct("age").Scan(&ageList)
fmt.Println(ageList)

或者

DB.Table("students").Select("distinct age").Scan(&ageList)

分组查询

var ageList []int
// 查询男生的个数和女生的个数
DB.Table("students").Select("count(id)").Group("gender").Scan(&ageList)
fmt.Println(ageList)

有个问题,哪一个是男生个数,那个是女生个数

所以我们应该精确一点

type AggeGroup struct {
  Gender int
  Count  int `gorm:"column:count(id)"`
}
var agge []AggeGroup
// 查询男生的个数和女生的个数
DB.Table("students").Select("count(id)", "gender").Group("gender").Scan(&agge)
fmt.Println(agge)

如何再精确一点,具体的男生名字,女生名字

type AggeGroup struct {
  Gender int
  Count  int    `gorm:"column:count(id)"`
  Name   string `gorm:"column:group_concat(name)"`
}

var agge []AggeGroup
// 查询男生的个数和女生的个数
DB.Table("students").Select("count(id)", "gender", "group_concat(name)").Group("gender").Scan(&agge)
fmt.Println(agge)

总之,使用gorm不会让你忘记原生sql的编写

这一点我还是很喜欢的

执行原生sql

type AggeGroup struct {
  Gender int
  Count  int    `gorm:"column:count(id)"`
  Name   string `gorm:"column:group_concat(name)"`
}

var agge []AggeGroup
DB.Raw(`SELECT count(id), gender, group_concat(name) FROM students GROUP BY gender`).Scan(&agge)

fmt.Println(agge)

子查询

查询大于平均年龄的用户

# 原生sql
select * from students where age > (select avg(age) from students);

使用gorm编写

var users []Student
DB.Model(Student{}).Where("age > (?)", DB.Model(Student{}).Select("avg(age)")).Find(&users)
fmt.Println(users)

命名参数

var users []Student

DB.Where("name = @name and age = @age", sql.Named("name", "枫枫"), sql.Named("age", 23)).Find(&users)
DB.Where("name = @name and age = @age", map[string]any{"name": "枫枫", "age": 23}).Find(&users)
fmt.Println(users)


find到map

var res []map[string]any
DB.Table("students").Find(&res)
fmt.Println(res)

查询引用Scope

可以再model层写一些通用的查询方式,这样外界就可以直接调用方法即可

func Age23(db *gorm.DB) *gorm.DB {
  return db.Where("age > ?", 23)
}

func main(){
  var users []Student
  DB.Scopes(Age23).Find(&users)
  fmt.Println(users)
}

事务

事务就是用户定义的一系列数据库操作,这些操作可以视为一个完成的逻辑处理工作单元,要么全部执行,要么全部不执行,是不可分割的工作单元。

很形象的一个例子,张三给李四转账100元,在程序里面,张三的余额就要-100,李四的余额就要+100 整个事件是一个整体,哪一步错了,整个事件都是失败的

gorm事务默认是开启的。为了确保数据一致性,GORM 会在事务里执行写入操作(创建、更新、删除)。

如果没有这方面的要求,您可以在初始化时禁用它,这将获得大约 30%+ 性能提升。

一般不推荐禁用

// 全局禁用
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  SkipDefaultTransaction: true,
})

本节课表结构

type User struct {
  ID    uint   `json:"id"`
  Name  string `json:"name"`
  Money int    `json:"money"`
}

// InnoDB引擎才支持事务,MyISAM不支持事务
// DB.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{})

普通事务

以张三给李四转账为例,不使用事务的后果

var zhangsan, lisi User
DB.Take(&zhangsan, "name = ?", "张三")
DB.Take(&lisi, "name = ?", "李四")
// 张三给李四转账100元
// 先给张三-100
zhangsan.Money -= 100
DB.Model(&zhangsan).Update("money", zhangsan.Money)
// 模拟失败的情况

// 再给李四+100
lisi.Money += 100
DB.Model(&lisi).Update("money", lisi.Money)

在失败的情况下,要么张三白白损失了100,要么李四凭空拿到100元

这显然是不合逻辑的,并且不合法的

那么,使用事务是怎样的

var zhangsan, lisi User
DB.Take(&zhangsan, "name = ?", "张三")
DB.Take(&lisi, "name = ?", "李四")
// 张三给李四转账100元
DB.Transaction(func(tx *gorm.DB) error {

  // 先给张三-100
  zhangsan.Money -= 100
  err := tx.Model(&zhangsan).Update("money", zhangsan.Money).Error
  if err != nil {
    fmt.Println(err)
    return err
  }

  // 再给李四+100
  lisi.Money += 100
  err = tx.Model(&lisi).Update("money", lisi.Money).Error
  if err != nil {
    fmt.Println(err)
    return err
  }
  // 提交事务
  return nil
})

使用事务之后,他们就是一体,一起成功,一起失败

手动事务

// 开始事务
tx := db.Begin()

// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
tx.Create(...)

// ...

// 遇到错误时回滚事务
tx.Rollback()

// 否则,提交事务
tx.Commit()
CopyErrorOK!
刚才的代码也可以这样实现

var zhangsan, lisi User
DB.Take(&zhangsan, "name = ?", "张三")
DB.Take(&lisi, "name = ?", "李四")

// 张三给李四转账100元
tx := DB.Begin()

// 先给张三-100
zhangsan.Money -= 100
err := tx.Model(&zhangsan).Update("money", zhangsan.Money).Error
if err != nil {
  tx.Rollback()
}

// 再给李四+100
lisi.Money += 100
err = tx.Model(&lisi).Update("money", lisi.Money).Error
if err != nil {
  tx.Rollback()
}
// 提交事务
tx.Commit()

一对多关系

我们先从一对多开始多表关系的学习

因为一对多的关系生活中到处都是

例如

老板与员工
女神和舔狗
老师和学生
班级与学生
用户与文章
...

一对多关系 表结构建立

在gorm中,官方文档是把一对多关系分为了两类,

  • Belongs To 属于谁
  • Has Many 我拥有的

他们本来是一起的,本教程把它们合在一起讲

我们以用户和文章为例

一个用户可以发布多篇文章,一篇文章属于一个用户

type User struct {
  ID       uint      `gorm:"size:4"`
  Name     string    `gorm:"size:8"`
  Articles []Article // 用户拥有的文章列表
}

type Article struct {
  ID     uint   `gorm:"size:4"`
  Title  string `gorm:"size:16"`
  UserID uint   // 属于   这里的类型要和引用的外键类型一致,包括大小
  User   User   // 属于
}

关于外键命名,外键名称就是关联表名+ID,类型是uint

重写外键关联

type User struct {
  ID       uint      `gorm:"size:4"`
  Name     string    `gorm:"size:8"`
  Articles []Article `gorm:"foreignKey:UID"` // 用户拥有的文章列表
}

type Article struct {
  ID    uint   `gorm:"size:4"`
  Title string `gorm:"size:16"`
  UID   uint   // 属于
  User  User   `gorm:"foreignKey:UID"` // 属于
}

这里有个地方要注意

我改了Article 的外键,将UID作为了外键,那么User这个外键关系就要指向UID

与此同时,User所拥有的Articles也得更改外键,改为UID

重写外键引用

type User struct {
  ID       uint      `gorm:"size:4"`
  Name     string    `gorm:"size:8"`
  Articles []Article `gorm:"foreignKey:UserName;references:Name"` // 用户拥有的文章列表
}

type Article struct {
  ID       uint   `gorm:"size:4"`
  Title    string `gorm:"size:16"`
  UserName string
  User     User `gorm:"references:Name"` // 属于
}

这一块的逻辑比较复杂

比如有1个用户

id	name
1	枫枫

之前的外键关系是这样表示文章的

id	title	user_id
1	python	1
2	javascript	1
3	golang	1

如果改成直接关联Name,那就变成了这样

id	title	user_name
1	python	枫枫
2	javascript	枫枫
3	golang	枫枫

虽然这样很方便,但是非常不适合在实际项目中这样用

我们还是用第一版的表结构做一对多关系的增删改查

一对多的添加
创建用户,并且创建文章

a1 := Article{Title: "python"}
a2 := Article{Title: "golang"}
user := User{Name: "枫枫", Articles: []Article{a1, a2}}
DB.Create(&user)

gorm自动创建了两篇文章,以及创建了一个用户,还将他们的关系给关联上了

创建文章,关联已有用户

a1 := Article{Title: "golang零基础入门", UserID: 1}
DB.Create(&a1)
CopyErrorOK!
var user User
DB.Take(&user, 1)
DB.Create(&Article{Title: "python零基础入门", User: user})

外键添加

给现有用户绑定文章

var user User
DB.Take(&user, 2)

var article Article
DB.Take(&article, 5)

user.Articles = []Article{article}
DB.Save(&user)

也可以用Append方法

var user User
DB.Take(&user, 2)

var article Article
DB.Take(&article, 5)

//user.Articles = []Article{article}
//DB.Save(&user)

DB.Model(&user).Association("Articles").Append(&article)

给现有文章关联用户

var article Article
DB.Take(&article, 5)

article.UserID = 2
DB.Save(&article)

也可用Append方法

var user User
DB.Take(&user, 2)

var article Article
DB.Take(&article, 5)

DB.Model(&article).Association("User").Append(&user)

查询

查询用户,显示用户的文章列表

var user User
DB.Take(&user, 1)
fmt.Println(user)

直接这样,是显示不出文章列表

预加载

我们必须要使用预加载来加载文章列表

var user User
DB.Preload("Articles").Take(&user, 1)
fmt.Println(user)

预加载的名字就是外键关联的属性名

查询文章,显示文章用户的信息

同样的,使用预加载

var article Article
DB.Preload("User").Take(&article, 1)
fmt.Println(article)

嵌套预加载

查询文章,显示用户,并且显示用户关联的所有文章,这就得用到嵌套预加载了

var article Article
DB.Preload("User.Articles").Take(&article, 1)
fmt.Println(article)

带条件的预加载
查询用户下的所有文章列表,过滤某些文章

var user User
DB.Preload("Articles", "id = ?", 1).Take(&user, 1)
fmt.Println(user)

这样,就只有id为1的文章被预加载出来了

自定义预加载

var user User
DB.Preload("Articles", func(db *gorm.DB) *gorm.DB {
  return db.Where("id in ?", []int{1, 2})
}).Take(&user, 1)
fmt.Println(user)

删除

级联删除
删除用户,与用户关联的文章也会删除

var user User
DB.Take(&user, 1)
DB.Select("Articles").Delete(&user)

清除外键关系
删除用户,与将与用户关联的文章,外键设置为null

var user User
DB.Preload("Articles").Take(&user, 2)
DB.Model(&user).Association("Articles").Delete(&user.Articles)

原生方法对比orm

查询数据

我们就查询一张表,并将查询的结果以json的形式,返回给客户端。

示例代码:

//定义User类型结构
type User struct {
    Id       int    `json:"id"`
    Username string `json:"username"`
    Password string `json:"password"`
}


//定义一个getALL函数用于回去全部的信息
func getAll() (users []User, err error) {
    //1.操作数据库
    db, _ := sql.Open("mysql", "root:hanru1314@tcp(127.0.0.1:3306)/mytest?charset=utf8")
    //错误检查
    if err != nil {
        log.Fatal(err.Error())
    }
    //推迟数据库连接的关闭
    defer db.Close()

    //2.查询
    rows, err := db.Query("SELECT id, username, password FROM user_info")
    if err != nil {
        log.Fatal(err.Error())
    }

    for rows.Next() {
        var user User
        //遍历表中所有行的信息
        rows.Scan(&user.Id, &user.Username, &user.Password)
        //将user添加到users中
        users = append(users, user)
    }
    //最后关闭连接
    defer rows.Close()
    return
}

插入数据

我们可以设计一个方法用于向数据库中添加数据:

//插入数据
func add(user User) (Id int, err error) {

    //1.操作数据库
    db, _ := sql.Open("mysql", "root:hanru1314@tcp(127.0.0.1:3306)/mytest?charset=utf8")
    //错误检查
    if err != nil {
        log.Fatal(err.Error())
    }
    //推迟数据库连接的关闭
    defer db.Close()
    stmt, err := db.Prepare("INSERT INTO user_info(username, password) VALUES (?, ?)")
    if err != nil {
        return
    }
    //执行插入操作
    rs, err := stmt.Exec(user.Username, user.Password)
    if err != nil {
        return
    }
    //返回插入的id
    id, err := rs.LastInsertId()
    if err != nil {
        log.Fatalln(err)
    }
    //将id类型转换
    Id = int(id)
    defer stmt.Close()
    return
}

然后我们添加一个POST的路由,当通过post请求的时候,我们向数据库中插入数据:

//利用post方法新增数据
    router.POST("/add", func(c *gin.Context) {
        var  u User
        err := c.Bind(&u)
        if err != nil {
            log.Fatal(err)
        }
        Id, err := add(u)
        fmt.Print("id=", Id)
        c.JSON(http.StatusOK, gin.H{
            "message": fmt.Sprintf("%s 插入成功", u.Username),
        })
    })

修改数据

添加一个update方法用于修改数据,我们实现的是根据id修改其他的字段:

//修改数据
func update(user User) (rowsAffected int64, err error) {

    //1.操作数据库
    db, _ := sql.Open("mysql", "root:hanru1314@tcp(127.0.0.1:3306)/mytest?charset=utf8")
    //错误检查
    if err != nil {
        log.Fatal(err.Error())
    }
    //推迟数据库连接的关闭
    defer db.Close()
    stmt, err := db.Prepare("UPDATE  user_info SET username=?, password=? WHERE id=?")
    if err != nil {
        return
    }
    //执行修改操作
    rs, err := stmt.Exec(user.Username, user.Password,user.Id)
    if err != nil {
        return
    }
    //返回插入的id
    rowsAffected,err =rs.RowsAffected()
    if err != nil {
        log.Fatalln(err)
    }
    defer stmt.Close()
    return
}

删除数据

我们可以根据Id删除一条数据,删除刚刚修改的id为10的数据,先添加一个delete方法:

//通过id删除
func del(id int) (rows int, err error) {
    //1.操作数据库
    db, _ := sql.Open("mysql", "root:hanru1314@tcp(127.0.0.1:3306)/mytest?charset=utf8")
    //错误检查
    if err != nil {
        log.Fatal(err.Error())
    }
    //推迟数据库连接的关闭
    defer db.Close()
    stmt, err := db.Prepare("DELETE FROM user_info WHERE id=?")
    if err != nil {
        log.Fatalln(err)
    }

    rs, err := stmt.Exec(id)
    if err != nil {
        log.Fatalln(err)
    }
    //删除的行数
    row, err := rs.RowsAffected()
    if err != nil {
        log.Fatalln(err)
    }
    defer stmt.Close()
    rows = int(row)
    return
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值