网站首页 > 技术文章 正文
从入门到精通Gorm框架
目录
- Gorm框架简介
- Gorm框架能做什么
- 实现原理
- 使用Gorm框架的步骤
- 最佳实践及注意事项
- 代码示例
- 数据库的连接和关闭
- 数据库连接池
- 基础的CRUD操作
- 进阶使用经验总结
- 使用事物
- 查询无结果的处理
- 使用链式操作
- 使用预加载
- 查询区分指针和值类型
Gorm框架简介
Gorm是一个用于Go语言的ORM(对象关系映射)库,它使用简洁的API,对数据库进行操作。在Gorm出现之前,Go开发者需要手动编写SQL语句,处理数据类型的转换问题以及异常处理等繁琐操作。使用了Gorm后,它可以自动完成CRUD的操作,简化了数据库操作过程,降低了开发难度
Gorm框架能做什么
- 支持多种主流的数据库:我们可以使用Gorm来操作多种主流数据库,包括MySQL, PostgreSQL, SQLite等。Gorm能够保持对各种数据库一致的操作方式,这在很大程度上简化了我们的代码,并提高了开发效率
- 功能强大:Gorm支持众多特性,比如模型定义、数据关联、事务处理和自动迁移等。它支持全功能CRUD操作、对关联模型进行查询和更新,还能快速生成聚合和条件查询
- 自动映射:Gorm可以自动将数据库中的表和字段映射为Go语言的结构体和字段,这简化了我们写复杂查询的工作。它同时还支持数据库和Go语言之间类型的自动转换
- 链式操作:使用Gorm,我们可以通过链式API来进行数据库操作。链式API让我们的代码看起来更清晰,易读,并且更符合Go语言的编程风格
- 容错处理:Gorm的每个操作都会返回一个error对象。当操作失败时,我们可以通过检查这个对象来处理错误,这对于我们编写健壮的代码来说非常重要
- 开箱即用的预加载和关联关系处理:我们可以通过使用Gorm的预加载(Preload)和关联关系处理(Association)特性,方便地处理数据库中的一对一,一对多,多对多的关联关系
- 扩展性:Gorm具有良好的扩展性,允许我们自定义插件,钩子(Hooks)以及自定义SQL
- 性能优化:Gorm通过延迟加载、乐观锁以及批量插入等多种方式,提高了我们程序的运行效率
实现原理
Gorm是基于Go语言实现的对象关系映射(ORM)库。它实际上是在Go语言的database/sql包之上构建的,而不是直接操作底层数据库。通过实现抽象接口,Gorm能够支持多种数据库,例如MySQL、PostgreSQL、SQLite等
- 对象和数据库的映射:这部分解决了我们在程序中的数据结构和数据库表相互转化的问题。Gorm通过Go的reflect包,获取和解析结构体标签,然后通过结构体对应的实例来操作数据库。当我们定义了一个Go语言的结构体,这个结构体可以通过标签来设置对应的数据库表名,字段名,约束等。当我们操作这个结构体和其实例的时候,Gorm会将我们的操作转化为对数据库的操作
- CRUD操作:我们可以调用Gorm的方法来进行插入、查询、更改、删除等操作。在后台,Gorm将这些方法转化为对应的SQL语句,然后调用database/sql包的功能执行这些SQL语句,并将结果反馈给我们
- 连接池管理:Gorm内置了连接池管理功能,我们可以设置最大的并发连接数等参数,Gorm会自动帮助我们管理连接池,包括打开和关闭连接
- 事务处理:Gorm内置了事务处理功能,之后我们可以开启一个新的事务,提交和回滚事务。在这个过程中,Gorm会保证我们的数据库操作具有原子性和一致性
- 钩子:Gorm内含丰富的钩子(Hooks)功能,我们可以自定义在进行CRUD操作前后的行为,比如插入前填充创建时间,插入后清空敏感字段等
- 预加载和关联关系处理:Gorm提供预加载(Preload)和关联关系处理(Association)的方法,可以方便地处理数据库中的一对一,一对多,多对多的关联关系
使用Gorm框架的步骤
- 在使用Gorm框架之前,我们首先需要导入Gorm代码库,执行go get -u gorm.io/gorm进行安装
- 连接数据库:利用Gorm的Open方法建立与数据库的连接。一般来说,Open方法需要两个参数,首先是需要连接的数据库类型,如"mysql"或"postgres",然后是具体的数据库连接信息,如用户名、密码、数据库名称等
- 定义模型:在Gorm中,模型是用Go的结构体来表示的。对于每个数据库的表,我们通常会定义一个与其对应的模型。模型中字段的类型和名称将对应到数据库表的列。我们还可以在模型中定义一些Gorm特定的标签,例如以定义字段的默认值,或者定义关联关系
- 进行CRUD操作:在成功连接到数据库并定义了模型之后,我们就可以使用Gorm提供的CRUD方法对数据进行操作了。在做完数据库操作之后,我们还可以检查Error来查看是否有错误发生
- 关闭数据库连接:虽然GORM为我们管理了数据库连接池,在每个查询完成后,连接会自动放回连接池,无需手动关闭连接,但是在程序退出时,我们依旧可以通过调用sql.DB的Close方法来确保所有的连接都被正确关闭
最佳实践及注意事项
最佳实践:
- 尽可能给模型定义专用的类型。例如,如果你经常使用某个类型的切片,就最好定义一个新的类型,这样将使模型更易于维护和管理
- 使用预加载(Preload)来获取关联数据。gorm提供了预加载功能,通过这个功能可以在查询主要数据的同时一起获取关联数据,这样可以避免后续再次进行查询,提高性能
- 尽可能使用数据库迁移功能。如果你的数据模型发生变化,使用迁移功能可以很方便地更新数据库结构,无须手动修改
- 使用gorm的Debug函数打印SQL语句。这个功能在调试阶段非常有用,可以帮助理解gorm的行为并查找潜在问题
注意事项:
- 需要注意gorm返回的错误信息,在一些情况下,gorm不会返回错误,而会直接panic,所以在使用gorm时要注意对错误进行处理
- 由于Go的特性,指针类型和非指针类型在数据库操作中会有一些不同,需要根据具体情况选择合适的类型
代码示例
数据库的连接和关闭
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
// 定义数据库连接串,包括用户名、密码、localhost、数据库名等
dsn := "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
// 使用指定的数据库连接信息连接数据库
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
// 如果连接失败,报告panic错误信息
if err != nil {
panic("failed to connect database")
}
}
在这个示例中,首先定义了一个包含用户名、密码、localhost和数据库名等信息的数据库连接串。然后使用这个连接串和指定的数据库连接信息(这里使用的是MySQL)连接到数据库。如果连接失败,代码将会抛出panic并报告错误
数据库连接池
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"time"
)
var db *gorm.DB // 定义一个全局的db变量,供我们在其他地方使用
func init() {
var err error
dsn := "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local" //设置连接字符串
// 建立连接
db, err = gorm.Open(mysql.New(mysql.Config{
DSN: dsn,
DefaultStringSize: 256, // 设置string类型字段的默认长度
DisableDatetimePrecision: true, // 禁用datetime精度,使用time.Time代替
DontSupportRenameIndex: true, // 重命名索引时采用删除后新建的方式
DontSupportRenameColumn: true, // 用`change`重命名列时采用删除并新建的方式
SkipInitializeWithVersion: false, //连接数据库时,自动根据版本进行初始化
}), &gorm.Config{
// 设置日志级别
Logger: logger.Default.LogMode(logger.Info),
// 设置表名前缀、单数表名、替换名称
NamingStrategy: schema.NamingStrategy{
TablePrefix: "prefix_", // 设置全局表名前缀
SingularTable: true, // 使用单数表名,如果设置为false,则表名会是结构体名的复数形式
NameReplacer: strings.NewReplacer("CID", "Cid"), // 在转换结构体名称到表名或者列名时,将"CID"替换为"Cid"
},
})
if err != nil {
panic("Failed to connect to database: " + err.Error())
}
// 设置连接池参数
sqlDB, err := db.DB()
if err != nil {
panic("Failed to get db: " + err.Error())
}
sqlDB.SetMaxIdleConns(10) // 设置数据库的最大空闲连接
sqlDB.SetMaxOpenConns(100) // 设置数据库的最大连接数量
sqlDB.SetConnMaxLifetime(time.Hour) // 设置连接的最大可复用时间
fmt.Println("Database connection successfully initialized.")
}
func main() {
// 不需要手动关闭数据库连接,由gorm管理
}
在这个示例中,定义了一个全局变量db,用于在其他地方使用数据库连接。init函数在程序启动时自动运行,它打开数据库连接,并设置了连接池参数。在main函数中,我们不需要手动关闭数据库连接,由gorm管理
基础的CRUD操作
// 定义模型
type Product struct {
gorm.Model // Gorm提供的基础模型,包含ID, CreatedAt, UpdatedAt, DeletedAt等常用字段
Code string
Price uint
}
func main() {
db, err := gorm.Open("sqlite3", "test.db")
if err != nil {
panic("failed to connect database")
}
defer db.Close()
// 迁移模型,确保数据表结构与模型一致
db.AutoMigrate(&Product{})
// 创建
db.Create(&Product{Code: "F01", Price: 100})
// 读取
var product Product
db.First(&product, 1) // 查找 ID 为 1 的 product
db.Find(&product, "code = ?", "F01") // 查找 code 为 F01 的 product
// 更新指定记录
db.Model(&product).Update("Price", 200) // 更新 product 的 price
db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F02"}) // 同时更新多个字段
db.Model(&product).Updates(Product{Price: 200, Code: "F02"}) // 使用模型更新多个字段,只会更新非空字段
// 通过where更新记录
db.Model(&Product{}).Where("Code = ?", "F01").Update("Price", 200) // 基于条件更新整个表的记录
db.Model(&Product{}).Where("Code = ?", "F01").Updates(map[string]interface{}{"Price": 200, "Code": "F02"}) // 同时更新多个字段
db.Model(&Product{}).Where("Code = ?", "F01").Updates(Product{Price: 200, Code: "F02"}) // 使用模型更新多个字段,只会更新非空字段
// 删除
db.Delete(&product, 1)
}
在这个示例中,定义了一个Product模型并进行了一些数据库操作。我们先使用AutoMigrate自动迁移数据库表结构。然后,我们创建了一个新的Product,读取了Product的信息,更新了Product的价格和代码,最后删除了这个Product
进阶使用经验总结
总结使用过程中碰到的一些问题,以及一些进阶用法
使用事物
- 如果在一个事务中有很多数据库操作,我们可以在每个操作之后都检查是否有错误,如果有错误就立即回滚事务,并且提早结束函数
func CreateOrder(db *gorm.DB, order *Order) error {
// 开始一个新的事务
tx := db.Begin()
// 尝试创建一个新的订单
if err := tx.Create(&order).Error; err != nil {
tx.Rollback()
return err
}
// 尝试创建新的订单项
for _, item := range order.Items {
// 使用事务创建订单项
if err := tx.Create(&item).Error; err != nil {
tx.Rollback()
return err
}
}
// 尝试更新用户的余额
if err := tx.Model(&User{ID: order.UserID}).Update("balance", gorm.Expr("balance - ?", order.Total)).Error; err != nil {
tx.Rollback()
return err
}
// 如果所有操作都成功则提交事务
return tx.Commit().Error
}
首先使用tx := db.Begin()开始一个事务,然后在接下来的所有数据库操作中都使用这个tx。在所有操作成功后,使用tx.Commit()提交事务,如果任何一个操作失败,使用tx.Rollback()回滚事务
- 执行事务的函数 - Transaction,可以让事务的处理变得更简洁
func ProcessOrder(tx *gorm.DB, order *Order) error {
if err := tx.Create(&order).Error; err != nil {
return err
}
for _, item := range order.Items {
if err := tx.Create(&item).Error; err != nil {
return err
}
}
if err := tx.Model(&User{ID: order.UserID}).Update("balance", gorm.Expr("balance - ?", order.Total)).Error; err != nil {
return err
}
return nil
}
然后你可以用GORM的Transaction函数来调用这个函数,实现事务处理,如果ProcessOrder函数返回错误,那么事务将会回滚,否则事务将会被提交:
err := db.Transaction(func(tx *gorm.DB) error {
return ProcessOrder(tx, order)
})
查询无结果的处理
- 对于First,Last,Take函数
- 如果在数据库中找不到相关的记录,First函数会返回一个gorm.ErrRecordNotFound的错误
- RowsAffected字段此时将返回0,表示没有任何行受到影响或找到
- 因此,要判断是否查找到记录,你可以根据Error字段是否为gorm.ErrRecordNotFound来做判断
res := db.First(&user)
if errors.Is(res.Error, gorm.ErrRecordNotFound) {
// 处理没有找到记录的情况
} else if res.Error != nil {
// 处理其他错误
}
- 对于Find函数
- Find函数在查找不到任何记录时不会返回gorm.ErrRecordNotFound错误,Error字段是nil
- 如果没有找到任何记录,RowsAffected返回值将是0
- 要判断Find操作是否找到了记录,可以根据RowsAffected字段是否为0来做判断
res := db.Find(&users)
if res.RowsAffected == 0 {
// 处理没有找到任何记录的情况
} else if res.Error != nil {
// 处理其他错误
}
使用链式操作
- 辅助类的方法:包括 Where、Order 等方法,它们主要用于构建查询条件,顺序不会影响最终的SQL查询语句
- 执行类的方法:如 Find,First,Last,Create,Update,Save 和 Delete 等方法,它们会立即执行数据库查询或修改。这些方法需要在链式调用的最后执行
- 逻辑操作方法:如 Not 和 Or 需要注意顺序,因为这可能会影响到最终的查询逻辑
使用预加载
Preload 方法用来自动加载模型的关联对象
例如一个 User 可能有多个 Order,我们可以将模型定义为:
type User struct {
gorm.Model
Name string
Orders []Order
}
type Order struct {
gorm.Model
UserID uint
Price float64
}
在查询 User 的时候,如果想预加载出相关的 Order,可以这样做:
var users []User
db.Preload("Orders").Find(&users)
注意事项:
- 预加载字段的名称要与定义的字段名称一致,不能拼写错误或大小写错误,也就是上述Preload的Orders要与模型定义的字段名称一致,对大小写敏感,并且首字母要大写,否则其他包无法访问
- 在循环中使用预加载需要特别小心,因为每次循环都会执行一次查询,可能会导致性能问题
数据库操作对指针和值类型的区别
在 GORM 中,指针类型和值类型在查询时会有一些不同,需要根据具体情况选择合适的类型
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type User struct {
ID int
Name *string // 指针类型字段
Age int // 值类型字段
}
func main() {
// 连接数据库
db, err := gorm.Open(mysql.New(mysql.Config{
DSN: "gorm:gorm@tcp(localhost)/gorm?charset=utf8&parseTime=True&loc=Local",
}), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
name := ""
age := 0
// 创建的User的Name字段存在一个指向空字符串的指针,Age字段为0
// 插入数据库,Name将会插入空字符串,Age未给出非零值,其值会是NULL 或者默认值(如果有设置)
user := User{Name: &name, Age: age}
db.Create(&user)
// 使用struct进行查询,Name为指针类型字段,其值为零值,会参与查询;Age为值类型字段,其值为0,不会参与查询
// 查询结果会是我门插入的那条数据,例如:{1 "" 0}
var result User
db.Where(&User{Name: &name, Age: age}).First(&result)
fmt.Println(result)
age = 25
// 此时Age为值类型字段,其值非零,会参与查询
// 但我们数据库中并没有Age为25 的记录,所以会返回零值对象
db.Where(&User{Name: &name, Age: age}).First(&result)
fmt.Println(result) // 结果会是一个零值对象,例如:{0 <nil> 0}
}
- 对于值类型,如果你尝试用0值插入,则 GORM 会认为这个字段是默认值,并不会插入到数据库中(也就是说,数据库实际接收到的值是所对应列的默认值,这通常是NULL,除非你为这个列显式设置了其他默认值)
- 对于指针类型,比如你的 Name 字段,如果你尝试用 nil 插入,则 GORM 会认为这个字段是没有值的,并插入NULL到数据库中。但是如果这个字段的值是指向0值的指针(比如你的代码中的 name 变量),GORM 会把这个0值(也就是空字符串)插入到数据库中
猜你喜欢
- 2024-09-09 混合云资产管理项目(二)(混合云存储产品有哪些)
- 2024-09-09 Go语言进阶之Go语言高性能Web框架Iris项目实战-完善用户管理EP04
- 2024-09-09 数据库与 Go 的交互(go数据库和kegg数据库)
- 2024-09-09 七爪源码:N+1 查询如何烧毁您的数据库
- 2024-09-09 Go的安全编程和防御性编程(防止代码注入)
- 2024-09-09 Vue3+Go 仿抖音项目架构设计与实现
- 2024-09-09 腾讯Go安全指南(腾讯官网最新安全公告)
- 2024-09-09 Grails指南24查询高阶(grails中文参考手册)
- 2024-09-09 Redis优化高并发下的秒杀性能(redis秒杀高并发代码)
- 2024-09-09 10.Go语言编写个人博客 文章分类(基于golang的个人博客系统)
- 最近发表
- 标签列表
-
- cmd/c (57)
- c++中::是什么意思 (57)
- sqlset (59)
- ps可以打开pdf格式吗 (58)
- phprequire_once (61)
- localstorage.removeitem (74)
- routermode (59)
- vector线程安全吗 (70)
- & (66)
- java (73)
- org.redisson (64)
- log.warn (60)
- cannotinstantiatethetype (62)
- js数组插入 (83)
- resttemplateokhttp (59)
- gormwherein (64)
- linux删除一个文件夹 (65)
- mac安装java (72)
- reader.onload (61)
- outofmemoryerror是什么意思 (64)
- flask文件上传 (63)
- eacces (67)
- 查看mysql是否启动 (70)
- java是值传递还是引用传递 (58)
- 无效的列索引 (74)