GORM 框架入门 | 青训营笔记

141 阅读8分钟

这是我参与「第五届青训营 」笔记创作活动的第 14 天

前言

前两篇笔记分别介绍了 Golang 微服务 HTTP 框架 Hertz 和 Golang 微服务 RPC 框架 Kitex,本文将要介绍面向golang语言的一种ORM(持久层)框架 GORM。

重点内容

  • GORM 简介
  • GORM 快速入门
  • GORM CRUD 接口介绍

知识点介绍

GORM 简介

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

特性:

  • 全功能 ORM
  • 关联 (Has One,Has Many,Belongs To,Many To Many,多态,单表继承)
  • Create,Save,Update,Delete,Find 中钩子方法
  • 支持 PreloadJoins 的预加载
  • 事务,嵌套事务,Save Point,Rollback To Saved Point
  • Context,预编译模式,DryRun 模式
  • 批量插入,FindInBatches,Find/Create with Map,使用 SQL 表达式、Context Valuer 进行 CRUD
  • SQL 构建器,Upsert,数据库锁,Optimizer/Index/Comment Hint,命名参数,子查询
  • 复合主键,索引,约束
  • Auto Migration
  • 自定义 Logger
  • 灵活的可扩展插件 API:Database Resolver(多数据库,读写分离)、Prometheus…
  • 每个特性都经过了测试的重重考验
  • 开发者友好

GORM 快速入门

安装

GORM 支持多种数据库的接入,这里以 MySQL 为例:

 go get -u gorm.io/gorm
 go get -u gorm.io/driver/mysql

快速入门

 package main
 ​
 import (
     "fmt"
     "github.com/jinzhu/gorm"
     _ "github.com/jinzhu/gorm/dialects/mysql"
 )
 ​
 type Product struct { 
    Code  string
    Price uint
 }
 ​
 func (p Product) TableName() string { 
    return "product" // 返回的字符串即表名
 }
 ​
 func main() {
    // 连接数据库(需要传递一个 dsn)
    db, err := gorm.Open(
        mysql.Open(dsn:"user:pass@(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"),
       &gorm.Config{})
    if err != nil {
       panic("failed to connect database")
    }
 ​
    // 创建数据 Create() 方法支持创建一条(传递对象)和多条数据(传递数组)
    db.Create(&Product{Code: "D42", Price: 100})
 ​
    // 查询数据 先声明一个结构体, 然后使用 First() 方法(注意是传递指针)
    var product Product
    db.First(&product, 1)                 // 根据整型主键查找
    db.First(&product, "code = ?", "D42") // 查找 code 字段值为 D42 的记录
 ​
    // 更新数据 - 用 Update() 方法将 product 的 price 更新为 200
    db.Model(&product).Update("Price", 200)
    // 更新数据 - 用 Updates() 方法更新多个字段
    db.Model(&product).Updates(Product{Price: 200, Code: "F42"})                    // 仅更新非零值字段
    db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"}) // 传递 map 可以更新零值
 ​
    // 删除数据
    db.Delete(&product, 1)
 }

GORM 支持的数据库

GORM 官方支持的数据库类型有: MySQL, PostgreSQL, SQlite, SQL Server

连接 SQLServer 数据库为例:

 import (
   "gorm.io/driver/sqlserver"
   "gorm.io/gorm"
 )
 ​
 // github.com/denisenkom/go-mssqldb
 dsn := "sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm"
 db, err := gorm.Open(sqlserver.Open(dsn), &gorm.Config{})

GORM 通过驱动来连接数据库,如果需要连接其他类型的数据库,可以复用或自行开发驱动。

GORM CRUD 接口介绍

创建

  • 创建一条记录

     p := &Product{Code: "D42"}
     res := db.Create(p)    // 因为 gorm 是链式调用, 使用 Creat() 后会返回一个gorm对象
     fmt.Println(res.Error) // 获取 err
     fmt.Println(p.ID)      // 返回插入数据的主键
    
  • 创建多条记录

     products := []*Product{{Code: "D41"}, {Code: "D42"}, {Code: "D43"}}
     res = db.Create(products) // 批量创建数据
     fmt.Println(res.Error)    // 获取 err
     for _, p := range products {
        fmt.Println(p.ID)
     }
    
  • 可以通过标签 default 为字段定义默认值

     type User struct {
       ID         int64
       Name       string `gorm:"default:galeone"`
       Age        int64  `gorm:"default:18"`
       uuid.UUID  UUID   `gorm:"type:uuid;default:gen_random_uuid()"` // 数据库函数
     }
    
  • GORM 为不同数据库提供了兼容的 Upsert 支持

     // 不处理冲突
     DB.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)
    

查询

  • 查询单个对象:GORM 提供了 FirstTakeLast 方法,以便从数据库中检索单个对象。当查询数据库时它添加了 LIMIT 1 条件,且没有找到记录时,它会返回 ErrRecordNotFound 错误

     // 获取第一条记录(主键升序)
     db.First(&user)
     // SELECT * FROM users ORDER BY id LIMIT 1;
     ​
     // 获取一条记录,没有指定排序字段
     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
     ​
     // 检查 ErrRecordNotFound 错误
     errors.Is(result.Error, gorm.ErrRecordNotFound)
    

    根据主键检索

    db.First(&user, 10)
    // SELECT * FROM users WHERE id = 10;
    
    db.First(&user, "10")
    // SELECT * FROM users WHERE id = 10;
    
    db.Find(&users, []int{1,2,3})
    // SELECT * FROM users WHERE id IN (1,2,3);
    
  • 条件查询

    • String 条件

      // 获取第一条匹配的记录
      db.Where("name = ?", "jinzhu").First(&user)
      // SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;
      
      // 获取全部匹配的记录
      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;
      
      // Time
      db.Where("updated_at > ?", lastWeek).Find(&users)
      // SELECT * FROM users WHERE updated_at > '2000-01-01 00:00:00';
      
      // BETWEEN
      db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
      // SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00';
      
    • Struct & Map 条件:注意 当使用结构作为条件查询时,GORM 只会查询非零值字段。这意味着如果您的字段值为 0''false 或其他零值,该字段不会被用于构建查询条件

      // 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);
      
  • 子查询

    db.Where("amount > ?", db.Table("orders").Select("AVG(amount)")).Find(&orders)
    // SELECT * FROM "orders" WHERE amount > (SELECT AVG(amount) FROM "orders");
    
    subQuery := db.Select("AVG(age)").Where("name LIKE ?", "name%").Table("users")
    db.Select("AVG(age) as avgage").Group("name").Having("AVG(age) > (?)", subQuery).Find(&results)
    // SELECT AVG(age) as avgage FROM `users` GROUP BY `name` HAVING AVG(age) > (SELECT AVG(age) FROM `users` WHERE name LIKE "name%
    

更新

  • 更新单个列

    // 根据条件和 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 更新属性

      // 根据 `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更新属性

      // 根据 `map` 更新属性
      db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false})
      // UPDATE users SET name='hello', age=18, actived=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
      
  • SQL 表达式更新

    // product 的 ID 是 `3`
    DB.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 100))
    // UPDATE "products" SET "price" = price * 2 + 100, "updated_at" = '2013-11-17 21:34:10' WHERE "id" = 3;
    
    DB.Model(&product).Updates(map[string]interface{}{"price": gorm.Expr("price * ? + ?", 2, 100)})
    // UPDATE "products" SET "price" = price * 2 + 100, "updated_at" = '2013-11-17 21:34:10' WHERE "id" = 3;
    
    DB.Model(&product).UpdateColumn("quantity", gorm.Expr("quantity - ?", 1))
    // UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = 3;
    
    DB.Model(&product).Where("quantity > 1").UpdateColumn("quantity", gorm.Expr("quantity - ?", 1))
    // UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = 3 AND quantity > 1;
    

删除

  • 物理删除

    db.Delete(&User{}, 10)
    // DELETE FROM users WHERE id = 10;
    
    db.Delete(&User{}, "10")
    // DELETE FROM users WHERE id = 10;
    
    db.Delete(&users, []int{1,2,3})
    // DELETE FROM users WHERE id IN (1,2,3);
    
  • 软删除:如果模型包含了一个 gorm.deletedat 字段(gorm.Model 已经包含了该字段),将自动获得软删除的能力!拥有软删除能力的模型调用 Delete 时,记录不会被从数据库中真正删除。但 GORM 会将 DeletedAt 置为当前时间, 并且你不能再通过正常的查询方法找到该记录。

    type User struct {
      ID      int
      Deleted gorm.DeletedAt
      Name    string
    }
    
    // user 的 ID 是 `111`
    db.Delete(&user)
    // 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;
    
    // 在查询时会忽略被软删除的记录
    db.Where("age = 20").Find(&user)
    // SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL;
    

    可以使用 Unscoped 找到被软删除的记录

    db.Unscoped().Where("age = 20").Find(&users)
    // SELECT * FROM users WHERE age = 20;
    

事务

GORM 提供了 Begin、Commit、Rollback 方法用于使用事务

tx := db.Begin() // 开始事务

if err := tx.Error; err != nil {
    return err
}

if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
    tx.Rollback() // 遇到错误时回滚事务
    return err
}

if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
    tx.Rollback()
    return err
}
// 提交事务
tx.Commit()

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

db.Transaction(func(tx *gorm.DB) error {
  // 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
  if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
    // 返回任何错误都会回滚事务
    return err
  }

  if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
    return err
  }

  // 返回 nil 提交事务
  return nil
})

Hook

Hook 是在创建、查询、更新、删除等操作之前、之后调用的函数。

如果您已经为模型定义了指定的方法,它会在创建、更新、查询、删除时自动被调用。如果任何回调返回错误,GORM 将停止后续的操作并回滚事务。

钩子方法的函数签名应该是 func(*gorm.DB) error

代码示例:

func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
  u.UUID = uuid.New()

  if !u.IsValid() {
    err = errors.New("can't save invalid data")
  }
  return
}

func (u *User) AfterCreate(tx *gorm.DB) (err error) {
  if u.ID == 1 {
    tx.Model(u).Update("role", "admin")
  }
  return
}

总结

本文主要介绍了 GORM 框架的相关基础知识,介绍了常用的接口函数,具体的细节还是需要仔细研究官方文档。这三个框架的学习都可以看出官方文档的重要性,同时学习框架还是要多上手练习才行。

引用

  1. GORM 官方文档 GORM 指南 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.