深入Gorm框架

2023年 8月 21日 105.3k 0

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处理冲突数据

image.png

image.png

image.png

多条数据创建:

	//创建多条
	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)
	}

image.png

4.数据查询:

主要使用函数:

  • Find 函数:用于查询满足条件的多条数据记录,查询不到数据不返回错误;
  • First 函数:用于查询满足条件的第一条数据记录,查询不到数据则返回ErrRecordNotFound容易造成线上问题,换用where,未找到返回空数组;
  • Take 函数:用于随机获取一条数据记录;
  • Where 函数:用于添加条件查询;
  • Order 函数:用于指定查询结果的排序方式;
  • Limit 函数:用于限制查询结果的数量;
  • Offset 函数:用于设置查询结果的偏移量;
  • Select 函数:用于选择要查询的字段;
  • Joins 函数:用于关联查询。

FindFirst获取第一条记录:

image.png

多条数据和复合条件查询:

	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传递查询的零值处理区别:

image.png

联表查询:

查询成绩大于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)
	}

image.png

5.数据更新:

主要使用函数:

  • Model 函数 + Update 函数:通过Model函数指定表名和条件,然后使用 Update函数更新单列数据;
  • Updates函数:更新多列数据,使用结构体更新时只会更新非零值,如果要更新零值需要使用map传递或者用Select选择字段;
  • Save函数:用于创建或更新数据记录,根据主键(或唯一索引)是否存在来决定操作;
  • UpdatesColumns函数:用于更新指定列的数据记录。

更新单列数据:

image.png

更新多列数据:

	//更新多个列
	//根据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))

image.png

image.png

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%'

image.png

image.png

软删除:

注意此时需要在数据库表中增加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可以查询被软删的数据

image.png

image.png

7.事务:

主要使用函数:

  • Begin 函数:用于开始一个事务;
  • Commit 函数:用于提交事务,将操作永久保存到数据库;
  • Rollback 函数:用于回滚事务,取消之前的操作;
  • Transaction 函数:执行事务操作。在这个函数中执行数据库操作,并根据每个操作的错误情况来决定是否提交事务或回滚事务。如果返回的错误为 nil,事务将被提交;如果返回的错误不为 nil,事务将被回滚,用于自动提交事务,避免漏写CommitRollback

普通事务:

	//数据一致性要求
	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() //提交事务

image.png

image.png

使用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
	}

image.png

image.png

8.钩子函数:

Gorm 提供一系列的钩子函数(Hook Functions),可以在执行数据库操作的不同阶段插入自定义的逻辑,这些钩子函数允许在创建、更新、删除等数据库操作之前或之后执行一些额外的操作,例如验证、审计、日志记录、触发器等,命名一般为操作名前加Before或After,如BeforeCreateAfterUpdate;

测试定义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
}

image.png

image.png

image.png

image.png

9.更多支持操作:

除了上述常用操作外,Gorm还支持的操作有:

  • 模型关联操作:

    • 一对一关联(HasOneBelongsTo
    • 一对多关联(HasManyBelongsToMany
    • 多对多关联(ManyToMany
  • 数据库迁移操作:

    • 创建表(AutoMigrateMigrator
    • 修改表结构(添加、修改、删除字段)
    • 删除表(DropTable
  • 查询构建器操作:

    • 排序(Order
    • 分页(LimitOffset
    • 聚合函数(CountSumMinMaxAvg
    • 原生 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可以提前解决后续项目开发的问题,留下这篇笔记也方便自己后续复习和资料查询,还是很有价值的!

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论