优秀的编程知识分享平台

网站首页 > 技术文章 正文

从入门到精通Gorm框架(go 框架)

nanyue 2024-09-09 04:58:03 技术文章 8 ℃


从入门到精通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框架的步骤

  1. 在使用Gorm框架之前,我们首先需要导入Gorm代码库,执行go get -u gorm.io/gorm进行安装
  2. 连接数据库:利用Gorm的Open方法建立与数据库的连接。一般来说,Open方法需要两个参数,首先是需要连接的数据库类型,如"mysql"或"postgres",然后是具体的数据库连接信息,如用户名、密码、数据库名称等
  3. 定义模型:在Gorm中,模型是用Go的结构体来表示的。对于每个数据库的表,我们通常会定义一个与其对应的模型。模型中字段的类型和名称将对应到数据库表的列。我们还可以在模型中定义一些Gorm特定的标签,例如以定义字段的默认值,或者定义关联关系
  4. 进行CRUD操作:在成功连接到数据库并定义了模型之后,我们就可以使用Gorm提供的CRUD方法对数据进行操作了。在做完数据库操作之后,我们还可以检查Error来查看是否有错误发生
  5. 关闭数据库连接:虽然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


进阶使用经验总结

总结使用过程中碰到的一些问题,以及一些进阶用法

使用事物

  1. 如果在一个事务中有很多数据库操作,我们可以在每个操作之后都检查是否有错误,如果有错误就立即回滚事务,并且提早结束函数

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()回滚事务

  1. 执行事务的函数 - 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)
})

查询无结果的处理

  1. 对于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 {
  // 处理其他错误
}
  1. 对于Find函数
  • Find函数在查找不到任何记录时不会返回gorm.ErrRecordNotFound错误,Error字段是nil
  • 如果没有找到任何记录,RowsAffected返回值将是0
  • 要判断Find操作是否找到了记录,可以根据RowsAffected字段是否为0来做判断
res := db.Find(&users)
if res.RowsAffected == 0 {
  // 处理没有找到任何记录的情况
} else if res.Error != nil {
  // 处理其他错误
}

使用链式操作

  1. 辅助类的方法:包括 Where、Order 等方法,它们主要用于构建查询条件,顺序不会影响最终的SQL查询语句
  2. 执行类的方法:如 Find,First,Last,Create,Update,Save 和 Delete 等方法,它们会立即执行数据库查询或修改。这些方法需要在链式调用的最后执行
  3. 逻辑操作方法:如 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值(也就是空字符串)插入到数据库中

Tags:

最近发表
标签列表