Bootstrap

GO进阶之旅(一)GORM入门

GORM是用于Golang的出色的ORM(对象关系映射(Object-Relational Mapping))库旨在对开发人员友好。用于在关系数据库和面向对象编程语言之间建立映射。

特性

  • 全功能 ORM
  • 关联 (Has One、Has Many、Belongs To、Many To Many、多态、单表继承)
  • Create、Save、Update、Delete、Find 前/后的勾子
    • 在执行特定数据库操作(如创建、保存、更新、删除、查找)前后执行的功能或操作。这些功能可以包括数据验证、日志记录、触发器等,通过勾子可以在ORM操作的不同阶段插入自定义逻辑。
  • 基于Preload、Joins的预加载
    • 在执行查询时同时加载相关联的数据。这可以减少数据库往返次数,提高性能。Joins则是通过SQL语句中的JOIN操作来实现关联查询,ORM库提供了这些功能以便于在面向对象的代码中处理关联数据。
  • 事务、嵌套事务、保存点、回滚至保存点
  • Context、Prepared Statment 模式、DryRun 模式
    • Context在ORM中用于管理请求的上下文,如超时、取消等。Prepared Statement 模式允许ORM预编译SQL语句,以提高多次执行相同查询的效率。DryRun 模式是一种模拟执行SQL语句的模式,用于调试或生成SQL而不实际执行。
  • 批量插入、FindInBatches、查询至 Map
    • 批量插入是将多条记录一次性插入到数据库中,以减少数据库往返次数和提高性能。FindInBatches允许按批次查找大量记录,以避免一次性加载大量数据到内存中。查询至 Map是将查询结果映射到键值对(Map)中,以便更方便地处理和访问数据。
  • SQL Builder, Upsert, Locking, Optimizer/Index/Comment Hints
    • SQL Builder是ORM库提供的用于动态构建复杂SQL查询的工具。Upsert是一种合并操作,当插入数据已存在时更新,不存在时插入新数据。Locking允许在查询时锁定行或表,以避免并发问题。Optimizer/Index/Comment Hints允许在生成SQL语句时指定数据库优化器、索引使用和SQL注释。
  • 复合主键
  • 自动迁移
    • 自动迁移是ORM库提供的功能,用于自动创建或更新数据库表结构,使其与定义的模型(数据对象)保持同步。
  • 自定义 Logger
  • 灵活的可扩展插件 API:Database Resolver(读写分离)、Prometheus…
  • 所有特性都通过了测试
  • 开发者友好

安装

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

快速入门

package main

import (
    "gorm.io/gorm"
    "gorm.io/driver/sqlite"
)

// Product 是一个产品模型,包含了基本的 gorm.Model 字段和自定义的字段。
type Product struct {
    gorm.Model
    Code  string // 产品的代码
    Price uint   // 产品的价格,无符号整数
}

func main() {
    // 连接到 SQLite 数据库
    db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
    if err != nil {
        panic("failed to connect database")
    }

    // 自动迁移数据库结构,确保 Product 表已创建
    db.AutoMigrate(&Product{})

    // 创建一条记录
    db.Create(&Product{Code: "D42", Price: 100})

    // 查询记录
    var product Product
    db.First(&product, 1)                      // 根据整型主键查找
    db.First(&product, "code = ?", "D42")      // 查找 code 字段值为 D42 的记录

    // 更新记录 - 将 product 的 price 更新为 200
    db.Model(&product).Update("Price", 200)
    // 更新记录 - 更新多个字段
    db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // 仅更新非零值字段
    db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})

    // 删除记录 - 删除 product
    db.Delete(&product, 1)
}

定义模型

模型一般基于 Go 的基本数据类型、实现了 Scanner 和 Valuer 接口的自定义类型以及它们的指针/别名

在 Go 中,有两个接口与数据库交互时特别有用:

  1. Scanner 接口:这是一个接口,定义了从数据库读取数据时如何将其扫描到 Go 类型的方法**(数据库–>实体类)**。它通常与自定义类型一起使用,用于将数据库中的原始数据转换为 Go 的特定类型。
  2. Valuer 接口:这是一个接口,定义了将 Go 类型转换为数据库可以存储的值的方法**(实体类型–>数据库)**。ORM 库会在将结构体保存到数据库之前调用这个方法。

Scanner:

type User struct {
    ID      uint
    Name    string
    Age     int
    Address Address
}

type Address struct {
    Street     string
    City       string
    PostalCode string
}

// Scan 将传入的 JSON 数据(作为 []byte)解析并映射到 Info 结构体中。
// 这个方法假设传入的数据是 Info 结构体的 JSON 表示形式。
// 如果 value 不是 []byte 类型,将返回错误。
func (i *Info) Scan(value interface{}) error {
    // 将 value 转换为 []byte
    v, ok := value.([]byte)
    if !ok {
        return errors.New("expected []byte")
    }

    // 定义一个临时结构体 receiver,用于暂存解析后的数据
    var receiver struct {
        ID      int           `json:"id"`
        Name    string        `json:"name"`
        Details json.RawMessage `json:"details"`
    }

    // 解析 JSON 数据到 receiver 结构体
    err := json.Unmarshal(v, &receiver)
    if err != nil {
        return err
    }

    // 将 receiver 中的字段赋值给 Info 结构体
    i.ID = receiver.ID
    i.Name = receiver.Name

    // 解析嵌套的 DetailsInfo 结构体
    var details DetailsInfo
    err = json.Unmarshal(receiver.Details, &details)
    if err != nil {
        return err
    }

    // 将解析后的 details 赋值给 Info 结构体中的 Details 字段
    i.Details = details

    return nil
}

Valuer

import (
    "database/sql/driver"
    "encoding/json"
    "errors"
)

// Address 是一个示例结构体,其中包含了嵌套的自定义类型 Location
type Address struct {
    Street   string   `json:"street"`
    City     string   `json:"city"`
    Location Location `json:"location"`
}

// Location 是另一个示例结构体,用于演示嵌套自定义类型
type Location struct {
    Latitude  float64 `json:"latitude"`
    Longitude float64 `json:"longitude"`
}

// Value 方法将 Address 结构体转换为数据库可以存储的格式
func (a Address) Value() (driver.Value, error) {
    // 定义一个 map 用于存储最终要存储到数据库的数据
    dbValue := make(map[string]interface{})

    // 直接将 Address 中的基本字段存储到 map 中
    dbValue["street"] = a.Street
    dbValue["city"] = a.City

    // 处理嵌套的 Location 结构体
    locationJSON, err := json.Marshal(a.Location)
    if err != nil {
        return nil, err
    }
    dbValue["location"] = locationJSON

    // 将 map 转换为 JSON 格式的 []byte,并返回作为 driver.Value
    return json.Marshal(dbValue)
}

约定

GORM 倾向于约定,而不是配置。默认情况下,GORM** 使用 ID 作为主键**,你可以通过标签 primaryKey 将其它字段设为主键,使用结构体名的 蛇形复数 作为表名,对于结构体 User,根据约定,其表名为 users。

type User struct {
  ID   string // 默认情况下,名为 `ID` 的字段会作为表的主键
  Name string
}

// 将 `AnimalID` 设为主键
type Animal struct {
  ID     int64
  UUID   string `gorm:"primaryKey"`
  Name   string
  Age    int64
}

//通过将多个字段设为主键,以创建复合主键
type Product struct {
  ID           string `gorm:"primaryKey"`
  LanguageCode string `gorm:"primaryKey"`
  Code         string
  Name         string
}
type Tabler interface {
    TableName() string
}

// TableName 会将 User 的表名重写为 `profiles`
func (User) TableName() string {
  return "profiles"
}

-------------------------------------------------------------------------------------

//TableName不支持动态变化,它会缓存下来以便后续使用。使用动态表名,你可以使用下面的代码
func UserTable(user User) func (db *gorm.DB) *gorm.DB {
  return func (db *gorm.DB) *gorm.DB {
    if user.Admin {
      return db.Table("admin_users")
    }

    return db.Table("users")
  }
}

DB.Scopes(UserTable(user)).Create(&user)

------------------------------------------------------------------------------------

// 您可以使用 Table 方法临时指定表名
// 根据 User 的字段创建 `deleted_users` 表
db.Table("deleted_users").AutoMigrate(&User{})

// 从另一张表查询数据
var deletedUsers []User
db.Table("deleted_users").Find(&deletedUsers)
// SELECT * FROM deleted_users;

db.Table("deleted_users").Where("name = ?", "jinzhu").Delete(&User{})
// DELETE FROM deleted_users WHERE name = 'jinzhu';

------------------------------------------------------------------------------------
//通过指定临时表名可以构建复杂的子查询
// 在 FROM 子句中使用子查询
db.Table("(?) as u", db.Model(&User{}).Select("name", "age")).Where("age = ?", 18).Find(&User{})
// SQL: SELECT * FROM (SELECT `name`,`age` FROM `users`) as u WHERE `age` = 18

// 在 FROM 子句中结合多个子查询
subQuery1 := db.Model(&User{}).Select("name")
subQuery2 := db.Model(&Pet{}).Select("name")
db.Table("(?) as u, (?) as p", subQuery1, subQuery2).Find(&User{})
// SQL: SELECT * FROM (SELECT `name` FROM `users`) as u, (SELECT `name` FROM `pets`) as p

------------------------------------------------------------------------------------
//也可以通过创建连接时指定
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  NamingStrategy: schema.NamingStrategy{
    TablePrefix: "t_",   // table name prefix, table for `User` would be `t_users`
    SingularTable: true, // use singular table name, table for `User` would be `user` with this option enabled
    NoLowerCase: true, // skip the snake_casing of names
    NameReplacer: strings.NewReplacer("CID", "Cid"), // use name replacer to change struct/field name before convert it to db name
  },
})

字段名的 蛇形 作为列名,并使用 CreatedAt、UpdatedAt 字段追踪创建、更新时间,您可以使用标签 column 或 命名策略 来覆盖列名

type User struct {
  ID        uint      // 列名是 `id`
  Name      string    // 列名是 `name`
  Birthday  time.Time // 列名是 `birthday`
  CreatedAt time.Time // 列名是 `created_at`
}

---------------------------------------------------------------------------------------

type Animal struct {
  AnimalID int64     `gorm:"column:beast_id"`         // 将列名设为 `beast_id`
  Birthday time.Time `gorm:"column:day_of_the_beast"` // 将列名设为 `day_of_the_beast`
  Age      int64     `gorm:"column:age_of_the_beast"` // 将列名设为 `age_of_the_beast`
}

对于有 CreatedAt 字段的模型,创建记录时,如果该字段值为零值,则将该字段的值设为当前时间;对于有 UpdatedAt 字段的模型,更新记录时,将该字段的值设为当前时间。创建记录时,如果该字段值为零值,则将该字段的值设为当前时间

db.Create(&user) // 将 `CreatedAt` 设为当前时间

// 想要修改该字段的值,你可以使用 `Update`
db.Model(&user).Update("CreatedAt", time.Now())

db.Save(&user) // 将 `UpdatedAt` 设为当前时间

db.Model(&user).Update("name", "jinzhu") // 也会将 `UpdatedAt` 设为当前时间

遵循 GORM 已有的约定,可以减少您的配置和代码量。如果约定不符合您的需求,GORM 允许您自定义配置它们

Model

GORM 定义一个 gorm.Model 结构体,其包括字段 ID、CreatedAt、UpdatedAt、DeletedAt,您可以将它嵌入到您的结构体中,以包含这几个字段

type User struct {
  // 匿名字段
  gorm.Model
  Name string
}
// 等效于
type User struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
  Name string
}

字段权限控制

可导出的字段在使用 GORM 进行 CRUD 时拥有全部的权限,此外,GORM 允许您用标签控制字段级别的权限。这样您就可以让一个字段的权限是只读、只写、只创建、只更新或者被忽略

type User struct {
  Name string `gorm:"<-:create"` // 允许读和创建
  Name string `gorm:"<-:update"` // 允许读和更新
  Name string `gorm:"<-"`        // 允许读和写(创建和更新)
  Name string `gorm:"<-:false"`  // 允许读,禁止写
  Name string `gorm:"->"`        // 只读(除非有自定义配置,否则禁止写)
  Name string `gorm:"->;<-:create"` // 允许读和写
  Name string `gorm:"->:false;<-:create"` // 仅创建(禁止从 db 读)
  Name string `gorm:"-"`  // 读写操作均会忽略该字段
}

创建/更新时间追踪(纳秒、毫秒、秒、Time)

GORM 约定使用 CreatedAt、UpdatedAt 追踪创建/更新时间。如果您定义了他们,GORM 在创建/更新时会自动填充 当前时间 至这些字段
要使用不同名称的字段,您可以配置 autoCreateTim、autoUpdateTim 标签
如果您想要保存纳秒、毫秒、秒级 UNIX 时间戳,而不是 time,您只需简单地将 time.Time 修改为 int 即可

type User struct {
  CreatedAt time.Time // 在创建时,如果该字段值为零值,则使用当前时间填充
  UpdatedAt int       // 在创建时该字段值为零值或者在更新时,使用当前秒级时间戳填充
  Updated   int64 `gorm:"autoUpdateTime:nano"` // 使用纳秒级时间戳填充更新时间
  Updated   int64 `gorm:"autoUpdateTime:milli"` // 使用毫秒级时间戳填充更新时间
  Created   int64 `gorm:"autoCreateTime"`      // 使用秒级时间戳填充创建时间
}

字段标签

标签名说明使用实例示例
column指定 db 列名column: user_id
type列数据类型type: int
size指定列大小size: 10
primaryKey指定列为主键primaryKey
unique指定列为唯一unique
default指定列的默认值default: 0
precision指定列的精度precision: 2
not null指定列为 NOT NULLnot null
autoIncrement指定列为自动增长autoIncrement
embedded嵌套字段embedded
embeddedPrefix嵌套字段的前缀embeddedPrefix: details
autoCreateTime创建时追踪当前时间autoCreateTime: milli
autoUpdateTime创建/更新时追踪当前时间autoUpdateTime: nano
index根据参数创建索引index: user_id
uniqueIndex与 index 相同,但创建唯一索引uniqueIndex: email
check创建检查约束check: (age >= 18)
<-设置字段写入的权限<-: create
->设置字段读取权限->: read
-忽略此字段-

链接数据库

GORM 官方支持的数据库类型有: MySQL, PostgreSQL, SQlite, SQL Server,这里仅展示MySQL

import (
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
)

func main() {
  // 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
  dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
  db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
}

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

想要 Gorm 正确的处理 time.Time ,您需要带上 parseTime 参数。 (查看更多参数)想要支持完整的 UTF-8 编码,您需要将 charset=utf8 更改为 charset=utf8mb4。MySQl 驱动程序提供了 一些高级配置 可以在初始化过程中使用。

操作

创建

普通创建

user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}

result := db.Create(&user) // 通过数据的指针来创建

user.ID             // 返回插入数据的主键
result.Error        // 返回 error
result.RowsAffected // 返回插入记录的条数

用选定字段的来创建

db.Select("Name", "Age", "CreatedAt").Create(&user)
// INSERT INTO `users` (`name`,`age`,`created_at`) VALUES ("jinzhu", 18, "2020-07-04 11:05:21.775")

排除字段创建

db.Omit("Name", "Age", "CreatedAt").Create(&user)
// INSERT INTO `users` (`birthday`,`updated_at`) VALUES ("2020-01-01 00:00:00.000", "2020-07-04 11:05:21.775")

批量插入:将切片数据传递给 Create 方法,GORM 将生成一个单一的 SQL 语句来插入所有数据,并回填主键的值,钩子方法也会被调用。

var users = []User{{Name: "jinzhu1"}, {Name: "jinzhu2"}, {Name: "jinzhu3"}}
DB.Create(&users)

for _, user := range users {
  user.ID // 1,2,3
}

钩子函数:钩子是在创建、查询、更新、删除等操作之前、之后调用的函数。如果您已经为模型定义了指定的方法,它会在创建、更新、查询、删除时自动被调用。如果任何回调返回错误,GORM 将停止后续的操作并回滚事务。钩子方法的函数签名应该是 func(*gorm.DB) error。钩子函数的调用不需要显示指定,需要遵从命名规范系统会自动调用。
在 GORM 中保存、删除操作会默认运行在事务上, 因此在事务完成之前该事务中所作的更改是不可见的,如果您的钩子返回了任何错误,则修改将被回滚。

// 开始事务
BeforeSave
BeforeCreate
// 关联前的 save
// 插入记录至 db
// 关联后的 save
AfterCreate
AfterSave
// 提交或回滚事务


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 提供 First, Take, Last 方法,以便从数据库中检索单个对象。当查询数据库时它添加了 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)
检索多个对象

在类型后+s

// 获取全部记录
result := db.Find(&users)
// SELECT * FROM users;

result.RowsAffected // 返回找到的记录数,相当于 `len(users)`
result.Error        // returns error
选择特定字段
db.Select("name", "age").Find(&users)
// SELECT name, age FROM users;

db.Select([]string{"name", "age"}).Find(&users)
// SELECT name, age FROM users;

db.Table("users").Select("COALESCE(age,?)", 42).Rows()
// SELECT COALESCE(age,'42') FROM users;
条件查询
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(&User{Name: "jinzhu", Age: 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu";
// map 零值作为条件
db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;
内联条件

用法与 Where类似,形式不同

// 根据主键获取记录(仅适用于整型主键)
db.First(&user, 23)
// SELECT * FROM users WHERE id = 23;
// 根据主键获取记录,如果是非整型主键
db.First(&user, "id = ?", "string_primary_key")
// SELECT * FROM users WHERE id = 'string_primary_key';

// Plain SQL
db.Find(&user, "name = ?", "jinzhu")
// SELECT * FROM users WHERE name = "jinzhu";

db.Find(&users, "name <> ? AND age > ?", "jinzhu", 20)
// SELECT * FROM users WHERE name <> "jinzhu" AND age > 20;

// Struct
db.Find(&users, User{Age: 20})
// SELECT * FROM users WHERE age = 20;

// Map
db.Find(&users, map[string]interface{}{"age": 20})
// SELECT * FROM users WHERE age = 20;
not条件
db.Not("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE NOT name = "jinzhu" ORDER BY id LIMIT 1;

// Not In
db.Not(map[string]interface{}{"name": []string{"jinzhu", "jinzhu 2"}}).Find(&users)
// SELECT * FROM users WHERE name NOT IN ("jinzhu", "jinzhu 2");

// Struct
db.Not(User{Name: "jinzhu", Age: 18}).First(&user)
// SELECT * FROM users WHERE name <> "jinzhu" AND age <> 18 ORDER BY id LIMIT 1;

// 不在主键切片中的记录
db.Not([]int64{1,2,3}).First(&user)
// SELECT * FROM users WHERE id NOT IN (1,2,3) ORDER BY id LIMIT 1;
or条件
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)
// SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin';

// Struct
db.Where("name = 'jinzhu'").Or(User{Name: "jinzhu 2", Age: 18}).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' OR (name = 'jinzhu 2' AND age = 18);

// Map
db.Where("name = 'jinzhu'").Or(map[string]interface{}{"name": "jinzhu 2", "age": 18}).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2';
group,having条件
type result struct {
  Date  time.Time
  Total int
}

db.Model(&User{}).Select("name, sum(age) as total").Where("name LIKE ?", "group%").Group("name").First(&result)
// SELECT name, sum(age) as total FROM `users` WHERE name LIKE "group%" GROUP BY `name` LIMIT 1


db.Model(&User{}).Select("name, sum(age) as total").Group("name").Having("name = ?", "group").Find(&result)
// SELECT name, sum(age) as total FROM `users` GROUP BY `name` HAVING name = "group"

rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Rows()
defer rows.Close()
for rows.Next() {
  ...
}

rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Rows()
defer rows.Close()
for rows.Next() {
  ...
}

type Result struct {
  Date  time.Time
  Total int64
}
db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Scan(&results)
Order

指定从数据库检索记录时的排序方式

db.Order("age desc, name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;

// Multiple orders
db.Order("age desc").Order("name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;
limit和offset

Limit 指定获取记录的最大数量 Offset 指定在开始返回记录之前要跳过的记录数量

db.Limit(3).Find(&users)
// SELECT * FROM users LIMIT 3;

// 通过 -1 消除 Limit 条件
db.Limit(10).Find(&users1).Limit(-1).Find(&users2)
// SELECT * FROM users LIMIT 10; (users1)
// SELECT * FROM users; (users2)

db.Offset(3).Find(&users)
// SELECT * FROM users OFFSET 3;

db.Limit(10).Offset(5).Find(&users)
// SELECT * FROM users OFFSET 5 LIMIT 10;

// 通过 -1 消除 Offset 条件
db.Offset(10).Find(&users1).Offset(-1).Find(&users2)
// SELECT * FROM users OFFSET 10; (users1)
// SELECT * FROM users; (users2)
distinct

从模型中选择不相同的值

db.Distinct("name", "age").Order("name, age desc").Find(&results)
joins
type result struct {
  Name  string
  Email string
}
db.Model(&User{}).Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&result{})
// SELECT users.name, emails.email FROM `users` left join emails on emails.user_id = users.id

rows, err := db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Rows()
for rows.Next() {
  ...
}

db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&results)

// 带参数的多表连接
db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "[email protected]").Joins("JOIN credit_cards ON credit_cards.user_id = users.id").Where("credit_cards.number = ?", "411111111111").Find(&user)
Joins 预加载

您可以使用 Joins 实现单条 SQL 预加载关联记录。

db.Joins("Company").Find(&users)
// SELECT `users`.`id`,`users`.`name`,`users`.`age`,`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name` FROM `users` LEFT JOIN `companies` AS `Company` ON `users`.`company_id` = `Company`.`id`;

// inner join
db.InnerJoins("Company").Find(&users)
// SELECT `users`.`id`,`users`.`name`,`users`.`age`,`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name` FROM `users` INNER JOIN `companies` AS `Company` ON `users`.`company_id` = `Company`.`id`;

db.Joins("Company", db.Where(&Company{Alive: true})).Find(&users)
// SELECT `users`.`id`,`users`.`name`,`users`.`age`,`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name` FROM `users` LEFT JOIN `companies` AS `Company` ON `users`.`company_id` = `Company`.`id` AND `Company`.`alive` = true;

Scan

Scan 结果至 struct,用法与 Find 类似。在 GORM 中,当使用 Scan 方法将查询结果映射到结构体时,如果查询返回多行结果,而结构体只定义了单个变量(而非切片或数组),GORM 会将最后一行的结果映射到结构体变量中,而忽略其他行的结果。

type Result struct {
  Name string
  Age  int
}

var result Result
db.Table("users").Select("name", "age").Where("name = ?", "Antonio").Scan(&result)

// 原生 SQL
db.Raw("SELECT name, age FROM users WHERE name = ?", "Antonio").Scan(&result)

更新

更新所有字段

Save 会保存所有的字段,即使字段是零值

db.First(&user)

user.Name = "jinzhu 2"
user.Age = 100
db.Save(&user)
// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111;

// 通过 `RowsAffected` 得到更新的记录数
result := db.Model(User{}).Where("role = ?", "admin").Updates(User{Name: "hello", Age: 18})
// UPDATE users SET name='hello', age=18 WHERE role = 'admin;

result.RowsAffected // 更新的记录数
result.Error        // 更新的错误
更新选定字段
  • update:更新单个字段
  • updates:更新多个字段
    • struct:更新非零值字段
    • map:更新所有给定字段
// 更新单个字段
// the user of `Model(&user)` needs to have primary key value, it is `111` in this example
db.Model(&user).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;

// 根据条件更新单个字段
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` 更新多个字段,不会更新零值字段
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, "actived": false})
// UPDATE users SET name='hello', age=18, actived=false, updated_at='2013-11-17 21:34:10' WHERE id=111;

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

// Select 与 Map
// the user of `Model(&user)` needs to have primary key value, it is `111` in this example
db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false})
// UPDATE users SET name='hello' WHERE id=111;

db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false})
// UPDATE users SET age=18, actived=false, updated_at='2013-11-17 21:34:10' WHERE id=111;

// Select 与 Struct
DB.Model(&result).Select("Name", "Age").Updates(User{Name: "new_name"})
// UPDATE users SET name='new_name', age=0 WHERE id=111;
批量更新

如果您尚未通过 Model 指定记录的主键,则 GORM 会执行批量更新

// 通过 struct 只能更新非零值,若要更新零值,可以使用 map[string]interface{}
db.Model(User{}).Where("role = ?", "admin").Updates(User{Name: "hello", Age: 18})
// UPDATE users SET name='hello', age=18 WHERE role = 'admin;

db.Table("users").Where("id IN (?)", []int{10, 11}).Updates(map[string]interface{}{"name": "hello", "age": 18})
// UPDATE users SET name='hello', age=18 WHERE id IN (10, 11);

如果在没有任何条件的情况下执行批量更新,GORM 不会执行该操作,并返回ErrMissingWhereClause错误
您可以使用 = 之类的条件来强制全局更新

db.Model(&User{}).Update("name", "jinzhu").Error // gorm.ErrMissingWhereClause

db.Model(&User{}).Where("1 = 1").Update("name", "jinzhu")
// UPDATE users SET `name` = "jinzhu" WHERE 1=1
高级用法

GORM 允许通过 SQL 表达式更新列

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" = '2';

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" = '2';

DB.Model(&product).UpdateColumn("quantity", gorm.Expr("quantity - ?", 1))
// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = '2';

DB.Model(&product).Where("quantity > 1").UpdateColumn("quantity", gorm.Expr("quantity - ?", 1))
// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = '2' AND quantity > 1;

如果您想在更新时跳过 钩子 方法和自动更新时间追踪, 您可以使用 UpdateColumn、UpdateColumns

// 更新单列,用法类似于 `Update`
db.Model(&user).UpdateColumn("name", "hello")
// UPDATE users SET name='hello' WHERE id = 111;

// 更新多列,用法类似于 `Updates`
db.Model(&user).UpdateColumns(User{Name: "hello", Age: 18})
// UPDATE users SET name='hello', age=18 WHERE id = 111;

// 配合 Select 更新多列,用法类似于 `Updates`
db.Model(&user).Select("name", "age").UpdateColumns(User{Name: "hello"})
// UPDATE users SET name='hello', age=0 WHERE id = 111;
更新钩子
// 开始事务
BeforeSave
BeforeUpdate
// 关联前的 save
// 更新 db
// 关联后的 save
AfterUpdate
AfterSave
// 提交或回滚事务



func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
  if u.readonly() {
    err = errors.New("read only user")
  }
  return
}

// 在同一个事务中更新数据
func (u *User) AfterUpdate(tx *gorm.DB) (err error) {
  if u.Confirmed {
    tx.Model(&Address{}).Where("user_id = ?", u.ID).Update("verfied", true)
  }
  return
}

删除

删除一条记录
// 删除一条已有的记录(email 的主键值为 10)
db.Delete(&email)
// DELETE from emails where id=10;

// 通过内联条件删除记录
db.Delete(&Email{}, 20)
// DELETE from emails where id=20;

// 带上其它条件
db.Where("name = ?", "jinzhu").Delete(&email)
// DELETE FROM emails WHERE id=10 AND name = 'jinzhu'
批量删除

如果没有指定带有主键值的记录,GORM 将执行批量删除,删除所有匹配的记录

db.Where("email LIKE ?", "%jinzhu%").Delete(Email{})
// DELETE from emails where email LIKE "%jinzhu%";

db.Delete(Email{}, "email LIKE ?", "%jinzhu%")
// DELETE from emails where email LIKE "%jinzhu%";

如果在没有任何条件的情况下执行批量删除,GORM 不会执行该操作,并返回ErrMissingWhereClause错误
您可以使用 = 之类的条件来强制全局删除

db.Delete(&User{}).Error // gorm.ErrMissingWhereClause

db.Where("1 = 1").Delete(&User{})
// DELETE `users` WHERE 1=1
软删除(逻辑删除)

如果您的模型包含了一个 gorm.deletedat 字段(gorm.Model 已经包含了该字段),它将自动获得软删除的能力!
拥有软删除能力的模型调用 Delete 时,记录不会被数据库。但 GORM 会将 DeletedAt 置为当前时间, 并且你不能再通过普通的查询方法找到该记录。

type User struct {
	ID   int32 `gorm:"primary_key;auto_increment"`
	Name string
	Age  int
	Sex  string
	gorm.Model
}
//如果你并不想嵌套gorm.Model,你也可以像下方例子那样开启软删除特性:
type User struct {
  ID      int
  Deleted gorm.DeletedAt
  Name    string
}

// user's ID is `111`
db.Delete(&user)
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111;

// Batch Delete
db.Where("age = ?", 20).Delete(&User{})
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;

// Soft deleted records will be ignored when querying
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;

你可以使用 Unscoped来永久删除匹配的记录

db.Unscoped().Delete(&order)
// DELETE FROM orders WHERE id=10;

混合模式可以使用 0,1或者unix时间戳来标记数据是否被软删除,并同时可以保存被删除时间

type User struct {
  ID        uint
  Name      string
  DeletedAt time.Time
  IsDel     soft_delete.DeletedAt `gorm:"softDelete:flag,DeletedAtField:DeletedAt"` // use `1` `0`
  // IsDel     soft_delete.DeletedAt `gorm:"softDelete:,DeletedAtField:DeletedAt"` // use `unix second`
  // IsDel     soft_delete.DeletedAt `gorm:"softDelete:nano,DeletedAtField:DeletedAt"` // use `unix nano second`
}

// 查询
SELECT * FROM users WHERE is_del = 0;

// 软删除
UPDATE users SET is_del = 1, deleted_at = /* current unix second */ WHERE ID = 1;
钩子函数
// 开始事务
BeforeDelete
// 删除 db 中的数据
AfterDelete
// 提交或回滚事务

// 在同一个事务中更新数据
func (u *User) AfterDelete(tx *gorm.DB) (err error) {
  if u.Confirmed {
    tx.Model(&Address{}).Where("user_id = ?", u.ID).Update("invalid", false)
  }
  return
}

“gorm.io/gen/field”

;