Gorm框架使用总结
一、基本介绍和总结思路
1.Gorm简介:
Gorm是Go语言的开源 ORM(对象关系映射)框架,ORM框架用于帮助处理关系型数据库的操作,提供一种面向对象的编程方式,简化数据库操作和数据模型管理的过程;Gorm框架能够通过Go语言结构体来定义数据库模型,它自动处理了数据库表和结构体之间的映射,允许通过操作结构体来进行数据查询、插入、更新和删除等操作,而无需直接编写SQL语句;
- Gorm提供了强大的查询构建器,可以使用链式方法构建复杂的数据库查询;
- 支持事务,可以使用Gorm提供的事务管理功能来保证数据库操作的原子性和一致性。
- Gorm支持数据库迁移,可以自动创建或更新数据库结构,而不需要手动编写 SQL 脚本。
- 可以定义在模型上执行的钩子函数,用于在保存、更新、删除等操作前后执行特定的逻辑,实现类似触发器的功能。
2.总结思路:
通过连接MySQL数据库,对一个Student表和Grade表分别进行数据的增删查改以及定义事务、钩子函数等操作熟悉Gorm语法,并使用Navicat(数据库可视化工具)验证总结Gorm的数据操作特性,在课程的基础上进一步了解细节,总结Gorm的基本用法,总结在项目实战中Gorm的注意事项,通过与其他ORM框架比较学习深入了解Gorm的优缺点,以供日后复习开发。
二、数据操作
1.数据库连接:
Gorm通过驱动连接数据库,连接必须使用DSN格式,DSN是一种用于指定数据库连接信息的字符串格式,通常用于在应用程序中配置和传递数据库连接参数,以便建立与数据库的连接,DSN包含了连接数据库所需的各种信息,如数据库类型、主机地址、端口、用户名、密码、数据库名称等,具体的 DSN 格式和参数取决于所使用的数据库类型和相应的数据库驱动,Gorm支持MySQL、SQL sever,postgresql,Sqlite,其他数据库可以复用或自行开发驱动;
Gorm连接数据库支持很多自定义配置,包含开启关闭默认事务,缓存预编译语句,启用日志输出,数据库连接池配置,自动迁移配置,钩子函数配置,表名列名映射配置等;
示例:
//连接数据库
dsn := "root:123456@tcp(localhost:3307)/gorm?&parseTime=True&loc=Local"
db, err := gorm.Open(
mysql.Open(dsn), //根据不同驱动支持不同数据库,dsn格式,编码格式和时区
&gorm.Config{
Logger: logger.Default.LogMode(logger.Info), // 启用日志输出,输出包含SQL查询语句
SkipDefaultTransaction: true, //关闭默认事务
PrepareStmt: true, //缓存预编译语句
}) //传递自定义配置
if err != nil {
panic("failed")
}
2.model定义:
Gorm通过定义结构体来定义Gorm model,同时通过实现TableName
接口来为model定义表名,如果不定义表名直接使用结构体名进行操作,则在创建数据库表时,按照Gorm的约定表名需要使用结构体名+s,否则会在操作表时报错数据库中不存在表;
在定义Gorm model时可以使用各种标签(tags)和方法来为模型添加不同的操作和功能:
- 表名和列名映射:使用
gorm:"column:name"
标签来映射模型字段到数据库表的列名; - 主键设置:使用
gorm:"primaryKey"
标签来指定主键字段; - 自增字段:使用
gorm:"autoIncrement"
标签来指定自增字段; - 唯一索引:使用
gorm:"unique"
标签来为字段添加唯一索引; - 默认值:使用
gorm:"default:value"
标签来为字段指定默认值; - 关联关系:使用
gorm:"foreignKey"
、gorm:"references"
和其他关联关系标签来定义模型之间的关联关系; - 自定义方法:你可以在模型上定义自己的方法,用于执行特定的数据库操作或业务逻辑;
- 查询作用域:使用
scope
方法创建查询作用域,以便在查询中复用特定的条件; - 忽略字段:使用
gorm:"-"
标签来忽略模型中的某些字段,使其不参与数据库操作; - 表名设置:使用
gorm:"tableName"
标签来指定数据库表的名称; - 软删除设置:使用
Deleted gorm.DeletedAt
标志字段,表示对该模型进行删除操作时,不直接进行物理删除,删除时并不会从数据库中移除数据记录,而是在记录中添加一个标志字段(deleted_at
),表示该记录已被删除,在查询时一般的查询语句会直接忽略软删除的数据进行查询,同时也可以使用Unscoped
不忽略软删除数据进行查询; - 结构体嵌套:通过在模型中嵌套其他结构体来表示更复杂的数据库关系。
type Student struct {
ID uint `gorm:"primaryKey" gorm:"autoIncrement" `
Name12 string `gorm:"column:name"`
Age uint `gorm:"column:age"`
Dream string `gorm:"default:bytedance"`
}
// TableName 为model定义表名,接口返回表名
func (s Student) TableName() string {
return "student"
}
type Grade struct {
ID uint `gorm:"primaryKey" gorm:"autoIncrement" `
StudentID uint
Num uint
Student Student `gorm:"foreignKey:StudentID"` //定义关联的Student模型
}
3.数据创建:
主要使用函数:
Create
函数:用于创建新的数据记录,需要使用指针传递;Save
函数:用于创建或更新数据记录,根据主键(或唯一索引)是否存在来决定操作;FirstOrCreate
函数:查找第一条满足条件的记录,如果不存在则创建;
单条数据创建:
//创建一条
s := &Student{Name12: "Alic", Age: 18}
res := db.Create(s) //传递结构体,链式调用,返回对象
fmt.Println(res.Error) //获取error,用链式调用返回的对象获取
fmt.Println(s.ID) //返回插入数据的主键,自增
g := &Grade{StudentID: s.ID, Num: 95}
res = db.Create(g)
fmt.Println(res.Error)
fmt.Println(g.ID)
s = &Student{ID: 2, Name12: "John", Age: 22}
res = db.Save(s)
fmt.Println(res.Error)
fmt.Println(s.ID)
//不冲突创建数据
s2 := &Student{Name12: "pp", ID: 1}
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&s2) //使用clause.OnConflict处理冲突数据
多条数据创建:
//创建多条
students := []*Student{{Name12: "aa", Age: 18}, {Name12: "bb", Age: 20}, {Name12: "cc", Age: 22}}
res := db.Create(students)
fmt.Println(res.Error)
for _, p := range students {
fmt.Println(p.ID)
}
4.数据查询:
主要使用函数:
Find
函数:用于查询满足条件的多条数据记录,查询不到数据不返回错误;First
函数:用于查询满足条件的第一条数据记录,查询不到数据则返回ErrRecordNotFound容易造成线上问题,换用where,未找到返回空数组;Take
函数:用于随机获取一条数据记录;Where
函数:用于添加条件查询;Order
函数:用于指定查询结果的排序方式;Limit
函数:用于限制查询结果的数量;Offset
函数:用于设置查询结果的偏移量;Select
函数:用于选择要查询的字段;Joins
函数:用于关联查询。
Find
和First
获取第一条记录:
多条数据和复合条件查询:
student := make([]*Student, 0)
//SELECT * FROM `student` WHERE age>18
result := db.Where("age>18").Find(&student) //链式调用返回对象打印调试信息
fmt.Println(result.RowsAffected) //返回找到的记录数,相当于len(users)
fmt.Println(result.Error) //returns error
//SELECT * FROM `student` WHERE name IN ('Alic','John')
db.Where("name IN ?", []string{"Alic", "John"}).Find(&student)
//SELECT * FROM `student` WHERE name LIKE '%Y%'
db.Where("name LIKE ?", "%Y%").Find(&student)
//SELECT * FROM `student` WHERE name='Alic' AND age>='18'
db.Where("name=? AND age>=?", "Alic", "18").Find(&student)
结构体传递和map传递查询的零值处理区别:
联表查询:
查询成绩大于90的学生:
//联表查询
var list []struct {
Student
Grade
}
result := db.Table("student").
Select("student.*, grades.*").
Joins("INNER JOIN grades ON student.id = grades.sid").
Where("grades.grade > ?", 90).
Find(&list)
if result.Error != nil {
panic("无法查询数据")
}
fmt.Println("成绩大于90的学生:")
for _, record := range list {
fmt.Println("学生姓名:", record.Name12)
fmt.Println("年龄:", record.Age)
fmt.Println("成绩:", record.Num)
}
5.数据更新:
主要使用函数:
Model
函数 +Update
函数:通过Model
函数指定表名和条件,然后使用Update
函数更新单列数据;Updates
函数:更新多列数据,使用结构体更新时只会更新非零值,如果要更新零值需要使用map传递或者用Select
选择字段;Save
函数:用于创建或更新数据记录,根据主键(或唯一索引)是否存在来决定操作;UpdatesColumns
函数:用于更新指定列的数据记录。
更新单列数据:
更新多列数据:
//更新多个列
//根据struct更新属性。只会更新非零值的字段
//UPDATE `student` SET `name`='Henry',`age`=18 WHERE `id` = 4
db.Model(&Student{ID: 4}).Updates(Student{Name12: "Henry", Age: 18}) //只更新非零值
//根据map更新属性
//UPDATE `student` SET `age`=18,`name`='Lucy' WHERE `id` = 5
db.Model(&Student{ID: 5}).Updates(map[string]interface{}{"name": "Lucy", "age": 18}) //零值处理
//更新选定字段
//UPDATE `student` SET `name`='hello' WHERE `id` = 6
db.Model(&Student{ID: 6}).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false})
//sql表达式更新
//UPDATE `student` SET `age`=age * 2 +100 WHERE `id` = 7
db.Model(&Student{ID: 7}).Update("age", gorm.Expr("age * ? +?", 2, 100))
6.数据删除:
主要使用函数:
Deleted gorm.DeletedAt
标志字段,设置模型删除模式为软删除;Delete
函数:用于删除数据记录;Where
函数 +Delete
函数:使用Where
函数指定删除的条件,然后调用Delete
函数删除数据;Unscoped
函数 +Delete
函数:使用Unscoped
函数删除包括软删除在内的记录;Model
函数 +Where
函数 +Delete
函数:通过Model
函数指定模型和条件,然后使用Delete
函数删除数据。
物理删除:
//物理删除
db.Delete(&Student{}, 10) //DELETE FROM `student` WHERE `student`.`id` = 10
db.Delete(&Student{}, "11") //DELETE FROM `student` WHERE `student`.`id` = '11'
db.Delete(&Student{}, []int{1, 2, 3}) //DELETE FROM `student` WHERE `student`.`id` IN (1,2,3)
db.Where("name LIKE ?", "%he%").Delete(Student{}) // DELETE FROM `student` WHERE name LIKE '%he%'
db.Delete(Student{}, "name LIKE ?", "%Y%") //DELETE FROM `student` WHERE name LIKE '%Y%'
软删除:
注意此时需要在数据库表中增加deleted
字段,类型为TIMESTAMP
,用于保存delete_at
软删除时间,作为软删除的标志,没有该字段会报错!
type Student struct {
ID uint `gorm:"primaryKey" gorm:"autoIncrement" `
Name12 string `gorm:"column:name"`
Age uint `gorm:"column:age"`
Dream string `gorm:"default:bytedance"`
Deleted gorm.DeletedAt //用于软删
}
//软删除,普通查询会忽略软删数据
//删除一条
s2 := Student{ID: 15}
db.Delete(&s2)
//批量删
db.Where("age = ?", 21).Delete(&Student{})
students := make([]*Student, 0)
//忽略软删数据查询
db.Where("age>20").Find(&students)
for _, record := range students {
fmt.Println(record.ID, record.Name12, record.Age)
}
//不忽略软删数据查询
db.Unscoped().Where("age>20").Find(&students)
for _, record := range students {
fmt.Println(record.ID, record.Name12, record.Age)
}
//拥有软删能力的model调用delete时记录不会在数据库真正的删除,但gorm会把deleteat设置为当前时间,并且不能再通过正常的查询方法找到该记录
//使用unscoped可以查询被软删的数据
7.事务:
主要使用函数:
Begin
函数:用于开始一个事务;Commit
函数:用于提交事务,将操作永久保存到数据库;Rollback
函数:用于回滚事务,取消之前的操作;Transaction
函数:执行事务操作。在这个函数中执行数据库操作,并根据每个操作的错误情况来决定是否提交事务或回滚事务。如果返回的错误为 nil,事务将被提交;如果返回的错误不为 nil,事务将被回滚,用于自动提交事务,避免漏写Commit
和Rollback
。
普通事务:
//数据一致性要求
tx := db.Begin() //开始事务
//在事务中执行一些db操作(从这里开始,应该使用tx而不是db)
if err = tx.Create(&Student{Name12: "name1", Age: 23}).Error; err != nil {
tx.Rollback() //遇到错误回滚事务
return
}
if err = tx.Create(&Student{Name12: "name2", Age: 25}).Error; err != nil {
tx.Rollback() //遇到错误回滚事务
return
}
tx.Commit() //提交事务
使用Transaction
的事务:
//提供了transaction方法用于自动提交事务
if err = db.Transaction(func(tx *gorm.DB) error {
if err = tx.Create(&Student{Name12: "name3", Age: 22}).Error; err != nil {
return err
}
if err = tx.Create(&Student{Name12: "name4", Age: 21}).Error; err != nil {
return err
}
return nil
}); err != nil {
return
}
8.钩子函数:
Gorm 提供一系列的钩子函数(Hook Functions),可以在执行数据库操作的不同阶段插入自定义的逻辑,这些钩子函数允许在创建、更新、删除等数据库操作之前或之后执行一些额外的操作,例如验证、审计、日志记录、触发器等,命名一般为操作名前加Before或After,如BeforeCreate
、AfterUpdate
;
测试定义BeforeCreate
用于在创建数据之前检查age是否大于10,小于10回滚返回错误,大于则正常创建;定义AfterCreate
用于创建一条Student记录后自动创建一条成绩为0的成绩字段,类似触发器实现;
func (s *Student) BeforeCreate(tx *gorm.DB) (err error) {
if s.Age < 0 {
return errors.New("the age should be greater than 10")
}
return
}
func (s *Student) AfterCreate(tx *gorm.DB) (err error) {
return tx.Create(&Grade{StudentID: s.ID, Num: 0}).Error
}
9.更多支持操作:
除了上述常用操作外,Gorm还支持的操作有:
-
模型关联操作:
- 一对一关联(
HasOne
、BelongsTo
) - 一对多关联(
HasMany
、BelongsToMany
) - 多对多关联(
ManyToMany
)
- 一对一关联(
-
数据库迁移操作:
- 创建表(
AutoMigrate
、Migrator
) - 修改表结构(添加、修改、删除字段)
- 删除表(
DropTable
)
- 创建表(
-
查询构建器操作:
- 排序(
Order
) - 分页(
Limit
、Offset
) - 聚合函数(
Count
、Sum
、Min
、Max
、Avg
) - 原生 SQL 查询(
Raw
)
- 排序(
三、对比MyBatis
对比Java常用的MyBatis框架,Gorm的使用会更加简易,直接使用dsn就可以直接连接数据库,提供自动映射,还可以进行很多自定义设置,MyBatis则需要使用XML或注解指定,需要进行手动映射;
Gorm会更加支持使用框架设置的函数进行数据操作,MyBatis则更提倡使用原生的SQL语句,Gorm使用方法链式的方式构建查询,以及基于结构体的查询构建器,MyBatis通过配置文件或注解来映射数据模型和数据库表;
Gorm提供了一些方便的特性,如事务、钩子等,但是不能直接的控制性能,在某些情况可能会造成性能缓慢,如复杂的联表查询;MyBatis由于手动编写SQL,可以更精细地控制性能,但也需要更多的代码编写;
综上Gorm作为Go语言的专属ORM,是比较年轻的ORM框架,提供的数据库操作方法会更加丰富,使用也更加简洁方便,上手也更简单,但是存在一定的局限性,性能方面需要通过其他手段调优;MyBatis作为老牌的ORM框架,仍然有巨大优势和优越的生态,仍然是功能很强大的框架,他们都是程序员需要好好学习的优秀ORM框架!
四、总结
该总结笔记是基于青训营课程Go框架三件套中Gorm框架部分进行的总结拓展,在老师课程讲解的基础上自己连接数据库进行了数据操作的实验,深入的了解了其中的细节,并在此基础上拓展了更多其他的用法,感受了Gorm框架的易用性,结合Gin框架和Hertz以及Kitex框架的学习,表示对Golang后端服务的开发有了更加整体性的认识;
通过老师的讲解和自己的实践,对Gorm框架的使用和特性变得更加熟悉,能够通过该框架进行完整的后端数据库交互,同时通过与其他ORM框架的对比学习,感受了它的优缺点,收获了很多,系统的学习使用了Gorm框架,在大项目开发中Gorm方面的很多问题也得到了解答;
数据交互是后端服务核心的一部分,学好ORM框架是解决后端问题的基础,虽然慢慢总结下来花了不少时间,但是非常值得,能系统的掌握Gorm可以提前解决后续项目开发的问题,留下这篇笔记也方便自己后续复习和资料查询,还是很有价值的!