Gorm框架知识整理 | 青训营笔记

475 阅读11分钟

这是我参与【第五届青训营】伴学笔记创作活动的第5天

Gorm

简介

gorm是面向golang语言的一种ORM(持久层)框架,支持多种数据库的接入,例如MySQL,PostgreSQL,SQLite,SQL Server,Clickhouse。此框架的特点,弱化了开发者对于sql语言的掌握程度,使用提供的API进行底层数据库的访问。

约定

  • GORM 倾向于约定,而不是配置。
  • 默认情况下,GORM 使用 ID 作为主键,使用结构体名的 蛇形复数 作为表名,字段名的 蛇形 作为列名,
  • 并使用 CreatedAt、UpdatedAt 字段追踪创建、更新时间

gorm.Model

GORM 定义一个 gorm.Model 结构体,其包括字段 ID、CreatedAt、UpdatedAt、DeletedAt

// gorm.Model 的定义
type Model struct {
 ID        uint           `gorm:"primaryKey"`
 CreatedAt time.Time
 UpdatedAt time.Time
 DeletedAt gorm.DeletedAt `gorm:"index"`
}

您可以将它嵌入到您的结构体中,以包含这几个字段

连接数据库

package main
​
import (
 "gorm.io/gorm"
 "gorm.io/driver/mysql"
)
​
type Product struct {
 gorm.Model
 Code  string
 Price uint
}
​
func main() {
 dsn := "root:123456@tcp(127.0.0.1:3306)/golang_db?charset=utf8mb4&parseTime=True&loc=Local"
 if err != nil {
   panic("failed to connect database")
}
​
 // 迁移 schema
 db.AutoMigrate(&Product{})
​
 // Create
 db.Create(&Product{Code: "D42", Price: 100})
​
 // Read
 var product Product
 db.First(&product, 1) // 根据整形主键查找
 db.First(&product, "code = ?", "D42") // 查找 code 字段值为 D42 的记录// Update - 将 product 的 price 更新为 200
 db.Model(&product).Update("Price", 200)
 // Update - 更新多个字段
 db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // 仅更新非零值字段
 db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})
​
 // Delete - 删除 product
 db.Delete(&product, 1)
}

其中 dsn 中的 user,pass,dbname 分别替换成你自己的数据库连接账号,密码,以及默认连接的哪个数据库。ip,port 则替换成数据库实例的 ip地址与端口号。

基本用法

import (
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
)
​
func main() {
  // ex: root:rootpass@tcp(127.0.0.1:3306)/mydb?charset=utf8mb4&parseTime=True&loc=Local&timeout=10s
  dsn := "user:pass@tcp(ip:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
  db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ })
}

进阶用法 - 支持各种高级配置,以及自定义数据库驱动

import (
  "my_mysql_driver"
  "gorm.io/gorm"
)
​
func main() {
      db, err := gorm.Open(mysql.New(mysql.Config{
      DSN: "gorm:gorm@tcp(127.0.0.1:3306)/gorm?charset=utf8&parseTime=True&loc=Local", 
      DefaultStringSize: 256, // string 类型字段的默认长度
      DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL5.6之前的数据库不支持
      DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL5.7之前的数据库和 MariaDB 不支持重命名索引
      DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
      SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置
      DriverName: "my_mysql_driver",
    }), &gorm.Config{})
}

连接池

数据库操作都是通过连接去执行的,频繁创建与销毁连接,是需要花费较大代价的,因此一般都采用连接池对连接进行复用。GORM 使用 database/sql 维护连接池

sqlDB, err := db.DB()
​
// SetMaxIdleConns 设置空闲连接池中连接的最大数量
sqlDB.SetMaxIdleConns(10)
​
// SetMaxOpenConns 设置打开数据库连接的最大数量。
sqlDB.SetMaxOpenConns(100)
​
// SetConnMaxLifetime 设置了连接可复用的最大时间。
sqlDB.SetConnMaxLifetime(time.Hour)

模型定义

模型定义就是将数据库中的表结构映射为代码层面的model 例如数据库表 user

CREATE TABLE `sys_user_info` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `user_id` varchar(32) COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户id',
  `user_name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户名',
  `user_addr` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '住址',
  `user_age` int NOT NULL COMMENT '年龄',
  `user_sex` tinyint NOT NULL DEFAULT '0' COMMENT '性别0男1女',
  `sys_ctime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `sys_utime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `is_delete` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

对应的model

type BaseModel struct {
   Id       int64     `gorm:"primary_key"`
   SysCtime time.Time `gorm:"autoCreateTime"` //在新增记录时可以自动填充当前时间
   SysUtime time.Time `gorm:"autoUpdateTime"` //在新增和更新记录时可以自动填充当前时间
   IsDelete int8
}
​
type SysUserInfo struct {
   BaseModel
   UserID   string
   UserName string
   UserAddr string
    UserAge  int16 `gorm:"default:18"`   //通过使用default标签为字段定义默认值
   UserSex  int8 
}
​
func (SysUserInfo) TableName() string {
   //实现TableName接口,以达到结构体和表对应,如果不实现该接口,并未设置全局表名禁用复数,gorm会自动扩展表名为sys_user_infos(结构体+s)
   return "sys_user_info"
}

SQL操作

新增记录

常规指针创建

user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
​
result := db.Create(&user) // 通过数据的指针来创建
​
user.ID             // 返回插入数据的主键
result.Error        // 返回 error
result.RowsAffected // 返回插入记录的条数

批量创建

// 批量创建
//users = []User{{Name: "zhz1"}, {Name: "zhz2"}, {Name: "zhz3"}}
//db.Create(&users)
users := []*User{{Name: "zhz1"}, {Name: "zhz2"}, {Name: "zhz3"}}
db.Create(users)
// 数量为 100
db.CreateInBatches(users, 100)
for _, user := range users {
   print(user.ID)
}

指定某些字段插入

//指定插入某些字段插入
db.Select("Name", "Age", "CreatedAt").Create(&user)
//指定某些字段不插入
db.Omit("Name", "is_delete", "CreatedAt").Create(&user)
//针对,创建时间与更新时间,也可以使用模型定义tag来定义默认值

删除记录

物理删除

一旦执行删除操作,该数据真没有了

db.Delete(&User{},"10") // DELETE FROM users WHERE id = 10:
db.Delete(&User{},[]int{123)) // DELETE FROM users WHERE id IN (1,2,3);
db.where("name LIKE ?","%jinzh%").Delete(User{}) // DELETE from users where name LIKE "%iinzhu%".
db.Delete(User{},"email LIKE ?","%iinzhu%") // DELETE from users where name LIKE "%jinzhu%".

软删除

GORM 提供了 gorm.DeletedAt 用于帮助用户实现软删

拥有软删除能力的 Model 调用 Delete 时,记录不会被从数据库中真正删除。但 GORM 会将 DeletedAt 置为当前时间并且你不能再通过正常的查询方法找到该记录。使用 Unscoped 可以查询到被软删的数据

type User struct {
    ID int64 
    Name string `gorm:"default:galeone"`
    Age int64  `gorm:"default:18"`
    Deleted gorm.DeletedAt
}
func main() {
    db,err := gorm.0pen(mysql.0pen( dsn: "username:password@tcp(locahost:9910)/database?charset=utf8")
    &gorm,Config{})
    if err != nil {
        panic( v:"failed to connect database")
    }
    // 删除一条
    u:= User{ID: 111} // user 的ID是111
    db.Delete(&u)// UPDATE users SET deleted_at="2013-10-29 10:23” WHERE id = 111;
    // 批量到除
    db.Where("age = ?",20).Delete(&User())// UPDATE users SET deleted-at-"2013-10-29 10:23" WHERE age = 20
    users := make([]+User,0)//在查询时会忽略被软别除的记录
    db.Where("age = 20").Find(&users) // SELECT + FRON users WHERE oge = 20 AND deleted_at IS NULL;
    //在查询时不会忽路被软删除的记录
    db.Unscoped().Where("age = 20").Find(&users) // SELECT * FROM USePS WHERE age = 20;
}

更新记录

根据主键修改

user := &model.SysUserInfo{}
user.ID = 1
// UPDATE `sys_user_info` SET `sys_utime`='2021-08-08 16:46:15.752',`user_name`='小麻皮',`user_addr`='深圳' WHERE `id` = 1
db.Model(&user).Updates(model.SysUserInfo{UserName: "小麻皮", UserAddr: "深圳"})

更新单列

// 条件更新
db.Model(&User{}).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE active=true;
​
// User 的 ID 是 `111`
db.Model(&user{ID:111}).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;
​
// 根据条件和 model 的值进行更新
db.Model(&user).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true;

更新多列

当通过 struct 更新时,GORM 只会更新非零字段。如果您想确保指定字段被更新,你应该使用 Select 更新选定字段,或使用 map 来完成更新操作

// 根据 `struct` 更新属性,只会更新非零值的字段
db.Model(&user).Updates(User{Name: "hello", Age: 18, Active: false})
// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111;
​
// 根据 `map` 更新属性
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;

更新选定字段

如果您想要在更新时选定、忽略某些字段,您可以使用 Select、Omit

// 使用 Map 进行 Select
// User's ID is `111`:
db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello' WHERE id=111;
​
db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;// 使用 Struct 进行 Select(会 select 零值的字段)
db.Model(&user).Select("Name", "Age").Updates(User{Name: "new_name", Age: 0})
// UPDATE users SET name='new_name', age=0 WHERE id=111;// Select 所有字段(查询包括零值字段的所有字段)
db.Model(&user).Select("*").Update(User{Name: "jinzhu", Role: "admin", Age: 0})
​
// Select 除 Role 外的所有字段(包括零值字段的所有字段)
db.Model(&user).Select("*").Omit("Role").Update(User{Name: "jinzhu", Role: "admin", Age: 0})
​
// SQL 表达式更新
// UPDATE "products" SET "price" = price * 2 + 100,"updated_at" = '2013-11-17 21:34:10' WHERE "id" = 3
db.Model(&User{ID: 111)).Update("age",gorm.Expr( expr: "age * ? + ?" args....2, 100))

查询记录

  • 检索单个对象

GORM 提供了 First、Take、Last 方法,以便从数据库中检索单个对象。需要注意查询不到数据会返回 ErrRecordNotFound。

// 获取第一条记录(主键升序)  
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;
​
db.First(&user,"code=?","D42")  //查找code字段值为42的记录
​
// 获取一条记录,没有指定排序字段
db.Take(&user)
// SELECT * FROM users LIMIT 1;
​
// 获取最后一条记录(主键降序)
db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;
​
result := db.First(&user)
result.RowsAffected // 返回找到的记录数
result.Error        // returns error or nil
​
// 检查 ErrRecordNotFound 错误
errors.Is(result.Error, gorm.ErrRecordNotFound)

First 和 Last 会根据主键排序,分别查询第一条和最后一条记录。只有在目标 struct 是指针或者通过 db.Model() 指定 model 时,该方法才有效。此外,如果相关 model 没有定义主键,那么将按 model 的第一个字段进行排序。例如:

var user User
var users []User  
​
// 有效,因为目标 struct 是指针
db.First(&user)
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1// 有效,因为通过 `db.Model()` 指定了 model
result := map[string]interface{}{}
db.Model(&User{}).First(&result)
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1// 无效
result := map[string]interface{}{}
db.Table("users").First(&result)
​
// 配合 Take 有效
result := map[string]interface{}{}
db.Table("users").Take(&result)
​
// 未指定主键,会根据第一个字段排序(即:`Code`)
type Language struct {
 Code string
 Name string
}
db.First(&Language{})
// SELECT * FROM `languages` ORDER BY `languages`.`code` LIMIT 1
  • 检索全部对象

使用 Find 查询多条数据,查询不到数据不会返回错误

// 获取全部记录
result := db.Find(&users)
// SELECT * FROM users;
  • 附加条件
var userList []*model.SysUserInfo
//返回的是全部字段  使用 Find 查询多条数据,查询不到数据不会返回错误。
result:=db.Where("user_name = ?", userName).Find(&userList)  
//SELECT * FROM sys_user_info WHERE name = ?
​
// 获取全部匹配的记录
db.Where("name <> ?", "jinzhu").Find(&users)
// SELECT * FROM users WHERE name <> 'jinzhu';
​
// IN
db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
// SELECT * FROM users WHERE name IN ('jinzhu','jinzhu 2');
​
// LIKE
db.Where("name LIKE ?", "%jin%").Find(&users)
// SELECT * FROM users WHERE name LIKE '%jin%';
​
// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;

scan类似Find都是用于执行查询语句,然后把查询结果赋值给结构体变量,区别在于scan不会从传递进来的结构体变量提取表名。使用 Scan 方法的时候需要我们显示指定数据库的表名。

// 原生 SQL
db.Raw("SELECT * FROM sys_user_info WHERE name = ?", userName).Scan(& userList)
​
// Struct
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 ORDER BY id LIMIT 1;
​
// Map
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;
​
// 主键切片条件
db.Where([]int64{20, 21, 22}).Find(&users)
// SELECT * FROM users WHERE id IN (20, 21, 22);

注意 当使用结构作为条件查询时,GORM 只会查询非零值字段。这意味着如果您的字段值为 0、‘’、false 或其他 零值,该字段不会被用于构建查询条件,例如:

db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu";

如果想要包含零值查询条件,你可以使用 map,其会包含所有 key-value 的查询条件,例如:

db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;

Gorm hook

GORM 在 提供了 CURD 的 Hook 能力 Hook 是在创建、查询、更新、删除等操作之前、之后自动调用的函数。如果任何 Hook 返错误,GORM 将停止后续的操作并回滚事务。

hook只能定义在model上。

假设我们有User表,对应model如下,则可以定义BeforeCreate hook,用于插入数据前的检查。

type User struct {
    ID int64
    Name string
    Age int32
    IsAdmin bool
    IsValid bool
    LoginTime time.Time
}
​
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
    if u.Age < 10 || u.Name == ""{
        return errors.New("invalid Age or Name")
    }
    return nil
}

能定义的所有hook接口:

//gorm/callbacks/interface.go
type BeforeCreateInterface interface {
    BeforeCreate(*gorm.DB) error
}
​
type AfterCreateInterface interface {
    AfterCreate(*gorm.DB) error
}
​
type BeforeUpdateInterface interface {
    BeforeUpdate(*gorm.DB) error
}
​
type AfterUpdateInterface interface {
    AfterUpdate(*gorm.DB) error
}
​
type BeforeSaveInterface interface {
    BeforeSave(*gorm.DB) error
}
​
type AfterSaveInterface interface {
    AfterSave(*gorm.DB) error
}
​
type BeforeDeleteInterface interface {
    BeforeDelete(*gorm.DB) error
}
​
type AfterDeleteInterface interface {
    AfterDelete(*gorm.DB) error
}
​
type AfterFindInterface interface {
    AfterFind(*gorm.DB) error
}
方法调用hoook触发次数
SaveBeforeCreate/AfterCreate/BeforeSave/AfterSave一次
CreateBeforeCreate/AfterCreate/BeforeSave/AfterSave数组形式插入触发多次,create from map方式不会触发
UpdateBeforeUpdate/AfterUpdate/BeforeSave/AfterSave一次
DeleteBeforeDelete/AfterDelete一次
Find/First/Last/TakeAfterFind查出几条数据则触发几次
  • AfterFind只在Find时可能调多次,因为只有Find可能返回多条数据。
  • 在没查出数据时,AfterFind不会触发。
  • BeforeSave,AfterSave在Create和Update时也会调用。这意味着,如果你同时定义了BeforeSave和BeforeCreate,那么在执行Create时,两者都会被触发。

Gorm事务

db,err ;= gorm.0pen(
    mysql.0pen("username:password@tcp(localhost:9910)/databasecharset=utf8"),&gorm.Config{})
if err != nil {
    panic("failed to connect database")
}
tx := db.Begin() // 开始事务
//在事务中执行一些 db 操作 (从这里开始,您应该使用tx而不是db)
if err = tx.Create(&User{Name: "name"}).Error; err != nil {
    tx.Rollback()//遇到错误时回滚事务
    return
}
if err = tx.Create(&User(Name: "name1"}).Error; err != nil{ 
    tx.Rollback()
    return
}
//以上语句都没有问题的话进行提交事务
tx.Commit()

以上写法当遇到情况复杂的时候可能会漏掉写Rollback或Commit,导致数据库链接泄露,因此

Gorm 提供了 Tansaction 方法用于自动提交事务,避免用户漏写 Commit、Rollbcak.

db,err ;= gorm.0pen(
    mysql.0pen("username:password@tcp(localhost:9910)/databasecharset=utf8"),&gorm.Config{})
if err != nil {
    panic("failed to connect database")
}
if err=db.Transaction(func(tx *gorm.DB) error {
  // 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
  if err := tx.Create(&User{Name: "zhz"}).Error; err != nil {
    // 返回任何错误都会回滚事务
    return err
  }
​
  if err := tx.Create(&User{Name: "zhz1"}).Error; err != nil {
    return err
  }
  // 返回 nil时自动提交事务
  return nil
    
});err!=nil{
    return
}

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

// 全局禁用
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
    SkipDefaultTransaction: true,   //关闭默认事务
    PrepareStmt:true,  //缓存预编译
})
​
// 持续会话模式
tx := db.Session(&Session{SkipDefaultTransaction: true})
tx.First(&user, 1)
tx.Find(&users)
tx.Model(&user).Update("Age", 18)