Gorm v2全解

目录

文档

Gorm官方文档地址:Gorm

工具

  1. Gorm v2:go get -u gorm.io/gorm
  2. mysql:gorm.io/driver/mysql

为啥选择Gorm

在这里插入图片描述
可以留意一下:gen,和Gorm是一个作者。
在这里插入图片描述

连接到数据库

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

GORM 配置

GORM 提供的配置可以在初始化时使用.

type Config struct {
  SkipDefaultTransaction   bool
  NamingStrategy           schema.Namer
  Logger                   logger.Interface
  NowFunc                  func() time.Time
  DryRun                   bool
  PrepareStmt              bool
  DisableNestedTransaction bool
  AllowGlobalUpdate        bool
  DisableAutomaticPing     bool
  DisableForeignKeyConstraintWhenMigrating bool
}

跳过默认事务

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

&gorm.Config{
  SkipDefaultTransaction: true,
}

命名策略

GORM 允许用户通过覆盖默认的NamingStrategy来更改命名约定。

// NamingStrategy tables, columns naming strategy
NamingStrategy schema.Namer

type Namer interface {
    TableName(table string) string
    SchemaName(table string) string
    ColumnName(table, column string) string
    JoinTableName(table string) string
    RelationshipFKName(Relationship) string
    CheckerName(table, column string) string
    IndexName(table, column string) string
}
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  NamingStrategy: schema.NamingStrategy{
    TablePrefix: "t_",   // 表前缀, table for `User` would be `t_users`
    SingularTable: true, // 表单数, 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(一般不用)
  },
})

Logger

实现gorm.logger.Interface接口以覆盖gorm的日志功能

// Logger
Logger logger.Interface

Gorm 有一个 默认 logger 实现,默认情况下,它会打印慢 SQL 和错误

newLogger := logger.New(
  log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
  logger.Config{
    SlowThreshold: time.Second,   // 慢 SQL 阈值
    LogLevel:      logger.Silent, // 日志级别
    IgnoreRecordNotFoundError: true,   // 忽略ErrRecordNotFound(记录未找到)错误
    Colorful:      false,         // 禁用彩色打印
  },
)

// 全局模式
&gorm.Config{
  Logger: newLogger,
}

// 新建会话模式
tx := db.Session(&Session{Logger: newLogger})
tx.First(&user)
tx.Model(&user).Update("Age", 18)
日志级别

GORM 定义了这些日志级别:Silent、Error、Warn、Info

&gorm.Config{
  Logger: logger.Default.LogMode(logger.Silent),
}

Debug 单个操作,将当前操作的 log 级别调整为 logger.Info

db.Debug().Where("name = ?", "jinzhu").First(&User{})

NowFunc

更改创建时间使用的函数

 &gorm.Config{
  NowFunc: func() time.Time {
    return time.Now().Local()
  },
}

CreateBatchSize

批量插入的数量,

CreateBatchSize:1000

DryRun

生成 SQL 但不执行,可以用于准备或测试生成的 SQL,

&gorm.Config{
  DryRun: false,
}

PrepareStmt

PreparedStmt 在执行任何 SQL 时都会创建一个 prepared statement 并将其缓存,以提高后续的效率。

&gorm.Config{
  // 禁止
  PrepareStmt: false,
}

AllowGlobalUpdate

启用全局 update/delete。

禁用嵌套事务

在一个事务中使用 Transaction 方法,GORM 会使用 SavePoint(savedPointName),RollbackTo(savedPointName) 提供嵌套事务支持,可以通过 DisableNestedTransaction 选项关闭它。

DisableAutomaticPing

在完成初始化后,GORM 会自动 ping 数据库以检查数据库的可用性,若要禁用该特性,可将其设置为 true。

DisableForeignKeyConstraintWhenMigrating

在 AutoMigrate 或 CreateTable 时,GORM 会自动创建外键约束,若要禁用该特性,可将其设置为 true。

禁止该选项,实际开发中一般不用创建外键约束,外键只用代码约束。

连接Mysql并测试连接

DSN:[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]

var handler *gorm.DB

func init() {
	db, err := gorm.Open(mysql.New(mysql.Config{
		DSN:                       "root:@tcp(127.0.0.1:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local", // DSN data source name
		DefaultStringSize:         191,                                                                       // string 类型字段的默认长度
		DisableDatetimePrecision:  true,                                                                      // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持
		DontSupportRenameIndex:    true,                                                                      // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
		DontSupportRenameColumn:   true,                                                                      // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
		SkipInitializeWithVersion: false,                                                                     // 根据当前 MySQL 版本自动配置
	}), &gorm.Config{
		// 不使用默认的事务
		SkipDefaultTransaction: false,
		// 不建立实际的外键约束,约束靠代码实现
		DisableForeignKeyConstraintWhenMigrating: true,
		// 表名迁移为单数
		NamingStrategy: schema.NamingStrategy{
			SingularTable: true,
		},
		// 日志
		Logger: logger.Default.LogMode(logger.Info),
	})

	if err != nil {
		log.Println(err)
	} else {
		fmt.Println(db)
	}
}

// &{0xc0000dc630 <nil> 0 0xc0001a0380 1}

常规数据库接口 sql.DB

GORM 提供了 DB 方法,可用于从当前 *gorm.DB 返回一个通用的数据库接口 *sql.DB

// 获取通用数据库对象 sql.DB,然后使用其提供的功能
sqlDB, err := db.DB()

// Ping
sqlDB.Ping()

// Close
sqlDB.Close()

// 返回数据库统计信息
sqlDB.Stats()

// 获取通用数据库对象 sql.DB ,然后使用其提供的功能
sqlDB, err := db.DB()

// SetMaxIdleConns 用于设置连接池中空闲连接的最大数量。
sqlDB.SetMaxIdleConns(10)

// SetMaxOpenConns 设置打开数据库连接的最大数量。
sqlDB.SetMaxOpenConns(100)

// SetConnMaxLifetime 设置了连接可复用的最大时间。
sqlDB.SetConnMaxLifetime(time.Hour)

模型定义

  1. GORM 倾向于约定优于配置默认情况下,GORM 使用 ID 作为主键,使用结构体名的 蛇形复数 作为表名,字段名的 蛇形 作为列名,并使用 CreatedAt、UpdatedAt 字段追踪创建、更新时间。
  2. 如果同意以上约定,配置文件就不需要写一堆了,否则就可以自定义约定。

约定

  1. 使用 ID 作为主键

  2. GORM 使用结构体名的 蛇形命名 作为表名。对于结构体 User,根据约定,其表名为 users

  3. TableName 不支持动态变化,它会被缓存下来以便后续使用,可以使用 Table 方法临时指定表名

  4. GORM 允许用户通过覆盖默认的命名策略更改默认的命名约定,命名策略被用于构建: TableName、ColumnName、JoinTableName、RelationshipFKName、CheckerName、IndexName。

    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`
    }
    
  5. 对于有 CreatedAt 字段的模型,创建记录时,如果该字段值为零值,则将该字段的值设为当前时间;对于有 UpdatedAt 字段的模型,更新记录时,将该字段的值设为当前时间。创建记录时,如果该字段值为零值,则将该字段的值设为当前时间;可以通过将 autoUpdateTime 标签置为 false 来禁用时间戳追踪

    type User struct {
      UpdatedAt time.Time `gorm:"autoUpdateTime:false"`
    }
    

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"`
}

type User struct {
  gorm.Model
  Name string
}

定义指针与非指针的区别

问题原文:

The docs of gorm https://gorm.io/docs/models.html present an example below.
The field Name and Email are described with string and *string. What is the main difference here?
Also how to provide the datatype for the images field storing a list of images link?
Should it be []string or []*string?

答案:

Go has default values for every primitive data types.

int -> 0, string -> “”, bool -> false likewise. So if you need to add null value, or load null value to a variable, it should be a pointer. Otherwise it is defaulted.

Default value of a pointer is nil in Go.

And complex data types such as slices, maps keep references. So their default value is nil. So, Images []string here images can be nil.

Below code with pointer types User1 and without pointer types User2 show the difference in default values.

package main

import (
    "fmt"
    "time"
)

type User1 struct {
    Email        *string
    Images       []string
    Birthday     *time.Time
}

type User2 struct {
    Email        string
    Images       []string
    Birthday     time.Time
}

func main() {
    user1 := User1{}
    user2 := User2{}

    fmt.Printf("user1 := %+v \n", user1)
    //Output : user1 := {Email:<nil> Images:[] Birthday:<nil>}
    fmt.Printf("user2 := %+v \n", user2)
    //Output : user2 := {Email: Images:[] Birthday:0001-01-01 00:00:00 +0000 UTC}
}

假设定义如下模型:

type Password struct {
	gorm.Model
	Name      string
	ManagerID *uint
	// Team不会出现在表中
	Team []Password `gorm:"foreignkey:ManagerID"`
}

执行新增时:

db.Create(&Password{Name:"p1"}
[11.708ms] [rows:1] INSERT INTO `password` (`created_at`,`updated_at`,`deleted_at`,`name`,`manager_id`) VALUES ('2023-03-29 20:17:20.553','2023-03-29 20:17:20.553',NULL,'p1',NULL) 

高级选项(标签)

tag 名大小写不敏感,但建议使用 camelCase 风格

标签名说明
column指定 db 列名
type列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes 并且可以和其他标签一起使用,例如:not nullsize, autoIncrement… 像 varbinary(8) 这样指定数据库数据类型也是支持的。在使用指定数据库数据类型时,它需要是完整的数据库数据类型,如:MEDIUMINT UNSIGNED not NULL AUTO_INCREMENT
serializer指定将数据序列化或反序列化到数据库中的序列化器, 例如: serializer:json/gob/unixtime
size定义列数据类型的大小或长度,例如 size: 256
primaryKey将列定义为主键
unique将列定义为唯一键
default定义列的默认值
precision指定列的精度
scale指定列大小
not null指定列为 NOT NULL
autoIncrement指定列为自动增长
autoIncrementIncrement自动步长,控制连续记录之间的间隔
embedded嵌套字段
embeddedPrefix嵌入字段的列名前缀
autoCreateTime创建时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoCreateTime:nano
autoUpdateTime创建/更新时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoUpdateTime:milli
index根据参数创建索引,多个字段使用相同的名称则创建复合索引,查看 索引 获取详情
uniqueIndexindex 相同,但创建的是唯一索引
check创建检查约束,例如 check:age > 13,查看 约束 获取详情
<-设置字段写入的权限, <-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限
->设置字段读的权限,->:false 无读权限
-忽略该字段,- 表示无读写,-:migration 表示无迁移权限,-:all 表示无读写迁移权限
comment迁移时为字段添加注释

字段级权限控制

注意:使用 GORM Migrator 创建表时,不会创建被忽略的字段。

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:"-"`  // 通过 struct 读写会忽略该字段
  Name string `gorm:"-:all"`        // 通过 struct 读写、迁移会忽略该字段
  Name string `gorm:"-:migration"`  // 通过 struct 迁移会忽略该字段
}

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

  1. 想要保存 UNIX(毫/纳)秒时间戳,而不是 time,只需简单地将 time.Time 修改为 int 即可。
  2. GORM 约定使用 CreatedAt、UpdatedAt 追踪创建/更新时间。
type User struct {
  CreatedAt time.Time // 在创建时,如果该字段值为零值,则使用当前时间填充
  UpdatedAt int       // 在创建时该字段值为零值或者在更新时,使用当前时间戳秒数填充
  Updated   int64 `gorm:"autoUpdateTime:nano"` // 使用时间戳纳秒数填充更新时间
  Updated   int64 `gorm:"autoUpdateTime:milli"` // 使用时间戳毫秒数填充更新时间
  Created   int64 `gorm:"autoCreateTime"`      // 使用时间戳秒数填充创建时间
}

嵌入结构体

  1. 对于匿名字段,GORM 会将其字段包含在父结构体中

    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
    }
    
  2. 对于正常的结构体字段,可以通过标签 embedded 将其嵌入,例如:

    type Author struct {
        Name  string
        Email string
    }
    
    type Blog struct {
      ID      int
      Author  Author `gorm:"embedded"`
      Upvotes int32
    }
    // 等效于
    type Blog struct {
      ID    int64
      Name  string
      Email string
      Upvotes  int32
    }
    
  3. 也可以使用标签 embeddedPrefix 来为 db 中的字段名添加前缀

    type Blog struct {
      ID      int
      Author  Author `gorm:"embedded;embeddedPrefix:author_"`
      Upvotes int32
    }
    // 等效于
    type Blog struct {
      ID          int64
      AuthorName string
      AuthorEmail string
      Upvotes     int32
    }
    

模型迁移

AutoMigrate(大部分情况下够用)

  1. AutoMigrate 用于自动迁移模型。
  2. AutoMigrate 会创建表、缺失的外键、约束、列和索引。 如果大小、精度、是否为空可以更改,则 AutoMigrate 会改变列的类型,它不会删除未使用的列。
db.AutoMigrate(&User{})
或
db.AutoMigrate(new(User))

db.AutoMigrate(&User{}, &Product{}, &Order{})
或
db.AutoMigrate(new(User),new(Product),new(Order))

// 创建表时添加后缀
db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{})

Migrator 接口

  1. GORM 提供了 Migrator 接口,该接口为每个数据库提供了统一的 API 接口,可用来为数据库构建独立迁移
  2. GORM 将基于使用 MySQL 的版本执行不同 SQL。
type Migrator interface {
  // AutoMigrate
  AutoMigrate(dst ...interface{}) error

  // Database
  CurrentDatabase() string
  FullDataTypeOf(*schema.Field) clause.Expr

  // Tables
  CreateTable(dst ...interface{}) error
  DropTable(dst ...interface{}) error
  HasTable(dst interface{}) bool
  RenameTable(oldName, newName interface{}) error
  GetTables() (tableList []string, err error)

  // Columns
  AddColumn(dst interface{}, field string) error
  DropColumn(dst interface{}, field string) error
  AlterColumn(dst interface{}, field string) error
  MigrateColumn(dst interface{}, field *schema.Field, columnType ColumnType) error
  HasColumn(dst interface{}, field string) bool
  RenameColumn(dst interface{}, oldName, field string) error
  ColumnTypes(dst interface{}) ([]ColumnType, error)

  // Constraints
  CreateConstraint(dst interface{}, name string) error
  DropConstraint(dst interface{}, name string) error
  HasConstraint(dst interface{}, name string) bool

  // Indexes
  CreateIndex(dst interface{}, name string) error
  DropIndex(dst interface{}, name string) error
  HasIndex(dst interface{}, name string) bool
  RenameIndex(dst interface{}, oldName, newName string) error
}
  1. 获取Migrator对象

    migrator := handler.Migrator()
    
  2. 返回当前使用的数据库名

    migrator.CurrentDatabase()
    

    在这里插入图片描述

  3. 表操作

    // 为 `User` 创建表
    db.Migrator().CreateTable(&User{})
    
    // 将 "ENGINE=InnoDB" 添加到创建 `User` 的 SQL 里去
    db.Set("gorm:table_options", "ENGINE=InnoDB").Migrator().CreateTable(&User{})
    
    // 检查 `User` 对应的表是否存在
    db.Migrator().HasTable(&User{})
    db.Migrator().HasTable("users")
    
    // 如果存在表则删除(删除时会忽略、删除外键约束)
    db.Migrator().DropTable(&User{})
    db.Migrator().DropTable("users")
    
    // 重命名表
    db.Migrator().RenameTable(&User{}, &UserInfo{})
    db.Migrator().RenameTable("users", "user_infos")
    

    在这里插入图片描述

  4. 列操作

    type User struct {
      Name string
    }
    
    // 添加 name 字段
    db.Migrator().AddColumn(&User{}, "Name")
    // 删除 name 字段
    db.Migrator().DropColumn(&User{}, "Name")
    // 修改 name 字段
    db.Migrator().AlterColumn(&User{}, "Name")
    // 检查 name 字段是否存在
    db.Migrator().HasColumn(&User{}, "Name")
    
    type User struct {
      Name    string
      NewName string
    }
    
    // 字段重命名
    db.Migrator().RenameColumn(&User{}, "Name", "NewName")
    db.Migrator().RenameColumn(&User{}, "name", "new_name")
    
    // 字段类型
    db.Migrator().ColumnTypes(&User{}) ([]gorm.ColumnType, error)
    
    type ColumnType interface {
        Name() string
        DatabaseTypeName() string                 // varchar
        ColumnType() (columnType string, ok bool) // varchar(64)
        PrimaryKey() (isPrimaryKey bool, ok bool)
        AutoIncrement() (isAutoIncrement bool, ok bool)
        Length() (length int64, ok bool)
        DecimalSize() (precision int64, scale int64, ok bool)
        Nullable() (nullable bool, ok bool)
        Unique() (unique bool, ok bool)
        ScanType() reflect.Type
        Comment() (value string, ok bool)
        DefaultValue() (value string, ok bool)
    }
    
  5. 约束

    type UserIndex struct {
      Name  string `gorm:"check:name_checker,name <> 'jinzhu'"`
    }
    
    // 创建约束
    db.Migrator().CreateConstraint(&User{}, "name_checker")
    
    // 删除约束
    db.Migrator().DropConstraint(&User{}, "name_checker")
    
    // 检查约束是否存在
    db.Migrator().HasConstraint(&User{}, "name_checker")
    
    type User struct {
      gorm.Model
      CreditCards []CreditCard
    }
    
    type CreditCard struct {
      gorm.Model
      Number string
      UserID uint
    }
    
    // 为 user & credit_cards 创建 db 外键
    db.Migrator().CreateConstraint(&User{}, "CreditCards")
    db.Migrator().CreateConstraint(&User{}, "fk_users_credit_cards")
    // ALTER TABLE `credit_cards` ADD CONSTRAINT `fk_users_credit_cards` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)
    
    // 检查 user & credit_cards 的外键是否存在
    db.Migrator().HasConstraint(&User{}, "CreditCards")
    db.Migrator().HasConstraint(&User{}, "fk_users_credit_cards")
    
    // 删除 user & credit_cards 的 db 外键
    db.Migrator().DropConstraint(&User{}, "CreditCards")
    db.Migrator().DropConstraint(&User{}, "fk_users_credit_cards")
    
    
  6. 索引

    type User struct {
      gorm.Model
      Name string `gorm:"size:255;index:idx_name,unique"`
    }
    
    // 为 Name 字段创建索引
    db.Migrator().CreateIndex(&User{}, "Name")
    db.Migrator().CreateIndex(&User{}, "idx_name")
    
    // 为 Name 字段删除索引
    db.Migrator().DropIndex(&User{}, "Name")
    db.Migrator().DropIndex(&User{}, "idx_name")
    
    // 检查索引是否存在
    db.Migrator().HasIndex(&User{}, "Name")
    db.Migrator().HasIndex(&User{}, "idx_name")
    
    type User struct {
      gorm.Model
      Name  string `gorm:"size:255;index:idx_name,unique"`
      Name2 string `gorm:"size:255;index:idx_name_2,unique"`
    }
    // 修改索引名
    db.Migrator().RenameIndex(&User{}, "Name", "Name2")
    db.Migrator().RenameIndex(&User{}, "idx_name", "idx_name_2")
    
    

处理错误

  1. GORM 的错误处理与常见的 Go 代码不同,因为 GORM 提供的是链式 API。

  2. 如果遇到任何错误,GORM 会设置 *gorm.DB 的 Error 字段:

    if err := db.Where("name = ?", "jinzhu").First(&user).Error; err != nil {
      // 处理错误...
    }
    

ErrRecordNotFound

当 First、Last、Take 方法找不到记录时,GORM 会返回 ErrRecordNotFound 错误。如果发生了多个错误,可以通过 errors.Is 判断错误是否为 ErrRecordNotFound

// 检查错误是否为 RecordNotFound
err := db.First(&user, 100).Error
errors.Is(err, gorm.ErrRecordNotFound)

Context上下文

GORM 通过 WithContext 方法提供了 Context 支持,通过 WithContext 设置的 Context 是线程安全的

单会话模式

通常被用于执行单次操作

db.WithContext(ctx).Find(&users)

持续会话模式

被用于执行一系列操作

tx := db.WithContext(ctx)
tx.First(&user, 1)
tx.Model(&user).Update("role", "admin")

context超时

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

db.WithContext(ctx).Find(&users)

Hooks/Callbacks 中的 Context

func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
  ctx := tx.Statement.Context
  // ...
  return
}

中间件使用

func SetDBMiddleware(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    timeoutContext, _ := context.WithTimeout(context.Background(), time.Second)
    // 将带有超时的db放入请求上下文
    ctx := context.WithValue(r.Context(), "DB", db.WithContext(timeoutContext))
    next.ServeHTTP(w, r.WithContext(ctx))
  })
}

r.Get("/", func(w http.ResponseWriter, r *http.Request) {
  // 取出db
  db, ok := ctx.Value("DB").(*gorm.DB)

  var users []User
  db.Find(&users)

  // 你的其他 DB 操作...
})

简单的CRUD

链式操作

GORM 允许进行链式操作:

db.Where("name = ?", "jinzhu").Where("age = ?", 18).First(&user)

GORM 中有三种类型的方法: 链式方法、终结方法、新建会话方法

在 链式方法, 终结方法之后, GORM 返回一个初始化的 *gorm.DB 实例,实例不能安全地重复使用,并且新生成的 SQL 可能会被先前的条件污染,例如:

queryDB := DB.Where("name = ?", "jinzhu")

queryDB.Where("age > ?", 10).First(&user)
// SELECT * FROM users WHERE name = "jinzhu" AND age > 10

queryDB.Where("age > ?", 20).First(&user2)
// SELECT * FROM users WHERE name = "jinzhu" AND age > 10 AND age > 20

为了重新使用初始化的 *gorm.DB 实例, 可以使用 新建会话方法创建一个可共享的 *gorm.DB, 例如

queryDB := DB.Where("name = ?", "jinzhu").Session(&gorm.Session{})

queryDB.Where("age > ?", 10).First(&user)
// SELECT * FROM users WHERE name = "jinzhu" AND age > 10

queryDB.Where("age > ?", 20).First(&user2)
// SELECT * FROM users WHERE name = "jinzhu" AND age > 20

链式方法

链式方法是将Clauses(分句) 修改或添加到当前 Statement(语句) 的方法,例如:

Where, Select, Omit, Joins, Scopes, Preload, Raw (Raw can’t be used with other chainable methods to build SQL)…

终结方法

终结(方法) 是会立即执行的方法,然后生成并执行 SQL,比如这些方法:

Create, First, Find, Take, Save, Update, Delete, Scan, Row, Rows…

新建会话方法

  1. GORM 定义了 Session、WithContext、Debug 方法做为 新建会话方法。

  2. 在 链式方法, 终结 方法之后, GORM 返回一个初始化的 *gorm.DB 实例,不能安全地再使用。

  3. 应该使用 新建会话方法 来标记 *gorm.DB 为可共享。

db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
// db is a new initialized *gorm.DB, which is safe to reuse

tx := db.Where("name = ?", "jinzhu").Session(&gorm.Session{})
tx := db.Where("name = ?", "jinzhu").WithContext(context.Background())
tx := db.Where("name = ?", "jinzhu").Debug()
// `Session`, `WithContext`, `Debug` returns `*gorm.DB` marked as safe to reuse, newly initialized `*gorm.Statement` based on it keeps current conditions

// good case
tx.Where("age = ?", 18).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age = 18

// good case
tx.Where("age = ?", 28).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age = 28;
会话

GORM 提供了 Session 方法,其配置和全局配置一模一样(全局配置是由Session组合出来的,gorm.config=seesion+其他功能)

// Session 配置
type Session struct {
  // DryRun 生成 SQL 但不执行。 它可以用于准备或测试生成的 SQL
  DryRun                   bool
	
  // PrepareStmt 预编译在执行任何 SQL 时都会创建一个 prepared statement 并将其缓存,以提高后续的效率
  PrepareStmt              bool
  // 通过 NewDB 选项创建一个不带之前条件的新 DB
  NewDB                    bool
  // 创建一个新的初始化DB,它不再是Method Chain/Goroutine Safe
  Initialized              bool
  // 跳过钩子
  SkipHooks                bool
  // 禁用默认事务
  SkipDefaultTransaction   bool
  // 禁用嵌套事务
  DisableNestedTransaction bool
  // GORM 默认不允许进行全局 update/delete,该操作会返回 ErrMissingWhereClause 错误
  AllowGlobalUpdate        bool
  // 在创建、更新记录时,GORM 会通过 Upsert 自动保存关联及其引用记录。 如果想要更新关联的数据,应该使用 FullSaveAssociations 模式
  FullSaveAssociations     bool
  // 声明查询字段
  QueryFields              bool
  // 上下文
  Context                  context.Context
  // 日志
  Logger                   logger.Interface
  // NowFunc 允许改变 GORM 获取当前时间的实现
  NowFunc                  func() time.Time
  // 默认批量大小
  CreateBatchSize          int
}
DryRun
生成 SQL 但不执行。 它可以用于准备或测试生成的 SQL,例如:
// 新建会话模式
stmt := db.Session(&Session{DryRun: true}).First(&user, 1).Statement
stmt.SQL.String() //=> SELECT * FROM `users` WHERE `id` = $1 ORDER BY `id`
stmt.Vars         //=> []interface{}{1}

// 全局 DryRun 模式
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{DryRun: true})

// 不同的数据库生成不同的 SQL
stmt := db.Find(&user, 1).Statement
stmt.SQL.String() //=> SELECT * FROM `users` WHERE `id` = $1 // PostgreSQL
stmt.SQL.String() //=> SELECT * FROM `users` WHERE `id` = ?  // MySQL
stmt.Vars         //=> []interface{}{1}
PreparedStmt(预编译)
// 全局模式,所有 DB 操作都会创建并缓存预编译语句
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  PrepareStmt: true,
})

// 会话模式
tx := db.Session(&Session{PrepareStmt: true})
tx.First(&user, 1)
tx.Find(&users)
tx.Model(&user).Update("Age", 18)

// returns prepared statements manager
stmtManger, ok := tx.ConnPool.(*PreparedStmtDB)

// 关闭 *当前会话* 的预编译模式
stmtManger.Close()

// 为 *当前会话* 预编译 SQL
stmtManger.PreparedSQL // => []string{}

// 为当前数据库连接池的(所有会话)开启预编译模式
stmtManger.Stmts // map[string]*sql.Stmt

for sql, stmt := range stmtManger.Stmts {
  sql  // 预编译 SQL
  stmt // 预编译模式
  stmt.Close() // 关闭预编译模式
}
NewDB

通过 NewDB 选项创建一个不带之前条件的新 DB,例如:

tx := db.Where("name = ?", "jinzhu").Session(&gorm.Session{NewDB: true})

tx.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1

tx.First(&user, "id = ?", 10)
// SELECT * FROM users WHERE id = 10 ORDER BY id

// 不带 `NewDB` 选项
tx2 := db.Where("name = ?", "jinzhu").Session(&gorm.Session{})
tx2.First(&user)
// SELECT * FROM users WHERE name = "jinzhu" ORDER BY id

初始化
tx := db.Session(&gorm.Session{Initialized: true})

跳过钩子

如果您想跳过 钩子 方法,您可以使用 SkipHooks 会话模式,例如:

DB.Session(&gorm.Session{SkipHooks: true}).Create(&user)

DB.Session(&gorm.Session{SkipHooks: true}).Create(&users)

DB.Session(&gorm.Session{SkipHooks: true}).CreateInBatches(users, 100)

DB.Session(&gorm.Session{SkipHooks: true}).Find(&user)

DB.Session(&gorm.Session{SkipHooks: true}).Delete(&user)

DB.Session(&gorm.Session{SkipHooks: true}).Model(User{}).Where("age > ?", 18).Updates(&user)

禁用嵌套事务

在一个 DB 事务中使用 Transaction 方法,GORM 会使用 SavePoint(savedPointName),RollbackTo(savedPointName) 为你提供嵌套事务支持。 你可以通过 DisableNestedTransaction 选项关闭它,例如:

db.Session(&gorm.Session{
  DisableNestedTransaction: true,
}).CreateInBatches(&users, 100)
AllowGlobalUpdate

GORM 默认不允许进行全局 update/delete,该操作会返回 ErrMissingWhereClause 错误。 您可以通过将一个选项设置为 true 来启用它,例如:

db.Session(&gorm.Session{
  AllowGlobalUpdate: true,
}).Model(&User{}).Update("name", "jinzhu")
// UPDATE users SET `name` = "jinzhu"
FullSaveAssociations

在创建、更新记录时,GORM 会通过 Upsert 自动保存关联及其引用记录。 如果您想要更新关联的数据,您应该使用 FullSaveAssociations 模式,例如:

db.Session(&gorm.Session{FullSaveAssociations: true}).Updates(&user)
// ...
// INSERT INTO "addresses" (address1) VALUES ("Billing Address - Address 1"), ("Shipping Address - Address 1") ON DUPLICATE KEY SET address1=VALUES(address1);
// INSERT INTO "users" (name,billing_address_id,shipping_address_id) VALUES ("jinzhu", 1, 2);
// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu@example.com"), (111, "jinzhu-2@example.com") ON DUPLICATE KEY SET email=VALUES(email);
// ...
Context

通过 Context 选项,您可以传入 Context 来追踪 SQL 操作,例如:

timeoutCtx, _ := context.WithTimeout(context.Background(), time.Second)
tx := db.Session(&Session{Context: timeoutCtx})

tx.First(&user) // 带有 context timeoutCtx 的查询操作
tx.Model(&user).Update("role", "admin") // 带有 context timeoutCtx 的更新操作

GORM 也提供了简写形式的方法 WithContext,其实现如下:

func (db *DB) WithContext(ctx context.Context) *DB {
  return db.Session(&Session{Context: ctx})
}
自定义 Logger

Gorm 允许使用 Logger 选项自定义内建 Logger,例如:

newLogger := logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags),
              logger.Config{
                SlowThreshold: time.Second,
                LogLevel:      logger.Silent,
                Colorful:      false,
              })
db.Session(&Session{Logger: newLogger})

db.Session(&Session{Logger: logger.Default.LogMode(logger.Silent)})
NowFunc

NowFunc 允许改变 GORM 获取当前时间的实现,例如:

db.Session(&Session{
  NowFunc: func() time.Time {
    return time.Now().Local()
  },
})
调试

Debug 只是将会话的 Logger 修改为调试模式的简写形式,其实现如下:

func (db *DB) Debug() (tx *DB) {
  return db.Session(&Session{
    Logger:         db.Logger.LogMode(logger.Info),
  })
}
查询字段

声明查询字段

db.Session(&gorm.Session{QueryFields: true}).Find(&user)
// SELECT `users`.`name`, `users`.`age`, ... FROM `users` // 有该选项
// SELECT * FROM `users` // 没有该选项
CreateBatchSize

默认批量大小

users = [5000]User{{Name: "jinzhu", Pets: []Pet{pet1, pet2, pet3}}...}

db.Session(&gorm.Session{CreateBatchSize: 1000}).Create(&users)
// INSERT INTO users xxx (需 5 次)
// INSERT INTO pets xxx (需 15 次)

事务

禁用默认事务

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

// 全局禁用
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  SkipDefaultTransaction: true,
})

// 持续会话模式
tx := db.Session(&Session{SkipDefaultTransaction: true})
tx.First(&user, 1)
tx.Find(&users)
tx.Model(&user).Update("Age", 18)

一般事务

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
})

嵌套事务

db.Transaction(func(tx *gorm.DB) error {
  tx.Create(&user1)

  tx.Transaction(func(tx2 *gorm.DB) error {
    tx2.Create(&user2)
    return errors.New("rollback user2") // Rollback user2
  })

  tx.Transaction(func(tx2 *gorm.DB) error {
    tx2.Create(&user3)
    return nil
  })

  return nil
})

// Commit user1, user3

手动事务(使用较少)

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

// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
tx.Create(...)

// ...

// 遇到错误时回滚事务
tx.Rollback()

// 否则,提交事务
tx.Commit()

示例

func CreateAnimals(db *gorm.DB) error {
  // 再唠叨一下,事务一旦开始,你就应该使用 tx 处理数据
  tx := db.Begin()
  defer func() {
    if r := recover(); r != nil {
      tx.Rollback()
    }
  }()

  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
  }

  return tx.Commit().Error
}

SavePoint、RollbackTo

GORM 提供了 SavePoint、Rollbackto 方法,来提供保存点以及回滚至保存点功能

tx := db.Begin()
tx.Create(&user1)

tx.SavePoint("sp1")
tx.Create(&user2)
tx.RollbackTo("sp1") // Rollback user2

tx.Commit() // Commit user1

钩子

  1. Hook 是在创建、查询、更新、删除等操作之前、之后调用的函数。
  2. 钩子方法的函数签名应该是 func(*gorm.DB) error
  3. 在 GORM 中保存、删除操作会默认运行在事务上, 因此在事务完成之前该事务中所作的更改是不可见的,如果您的钩子返回了任何错误,则修改将被回滚。

Create钩子

// 开始事务
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
}

func (u *User) AfterSave(tx *gorm.DB) (err error) {
  if !u.IsValid() {
    return errors.New("rollback invalid user")
  }
  return nil
}

Update钩子

// 开始事务
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
}

Delete钩子

// 开始事务
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
}

Retrieve钩子

// 从 db 中加载数据
// Preloading (eager loading)
AfterFind

func (u *User) AfterFind(tx *gorm.DB) (err error) {
  if u.MemberShip == "" {
    u.MemberShip = "user"
  }
  return
}

修改当前操作(一般不用)

// create改retrieve
func (u *User) BeforeCreate(tx *gorm.DB) error {
  // 通过 tx.Statement 修改当前操作,例如:
  tx.Statement.Select("Name", "Age")
  tx.Statement.AddClause(clause.OnConflict{DoNothing: true})

  // tx 是带有 `NewDB` 选项的新会话模式 
  // 基于 tx 的操作会在同一个事务中,但不会带上任何当前的条件
  err := tx.First(&role, "name = ?", user.Role).Error
  // SELECT * FROM roles WHERE name = "admin"
  // ...
  return err
}

Create

创建记录

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

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

user.ID             // 返回插入数据的主键
result.Error        // 返回 error
result.RowsAffected // 返回插入记录的条数
func CreateUser(username, password, phone, email, qq, wechat string) *User {
	userObj := &User{Username: username, Password: password, Phone: phone, Email: email, QQ: qq, WeChat: wechat}
	result := handler.Create(userObj)
	if result.Error != nil {
		return nil
	}
	return userObj
}

用指定的字段创建记录

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")

批量插入

var users = []User{{Name: "jinzhu1"}, {Name: "jinzhu2"}, {Name: "jinzhu3"}}
db.Create(&users)
for _, user := range users {
  user.ID // 1,2,3
}

// 使用 CreateInBatches 分批创建时,可以指定每批的数量
ar users = []User{{name: "jinzhu_1"}, ...., {Name: "jinzhu_10000"}}
// 将[0-99]的user插入,数量为 100
db.CreateInBatches(users, 100)

使用CreateBatchSize 选项初始化 GORM 时,所有的创建& 关联 INSERT 都将遵循该选项,即Size不超CreateBatchSize.

&gorm.Config{
  CreateBatchSize: 1000,
}
db := db.Session(&gorm.Session{CreateBatchSize: 1000})

// 准备数据
users = [5000]User{{Name: "jinzhu", Pets: []Pet{pet1, pet2, pet3}}...}

db.Create(&users)
// INSERT INTO users xxx (5 batches)
// INSERT INTO pets xxx (15 batches)

创建钩子

type User struct {
	gorm.Model
	Username string `json:"username" form:"username" bson:"username"`
	Password string `json:"password" form:"password" bson:"password"`
	Phone    string `json:"phone" form:"phone" bson:"phone"`
	Email    string `json:"email" bson:"email" form:"email"`
	QQ       string `json:"QQ" bson:"QQ" form:"QQ"`
	WeChat   string `json:"weChat" bson:"weChat" form:"weChat"`
}

// 当username和password为空时,报错
func (u *User) AfterSave(tx *gorm.DB) (err error) {
	if u.Username == "" || u.Password == "" {
		return errors.New("rollback invalid user")
	}
	return nil
}

在这里插入图片描述

如果您想跳过 钩子 方法,可以使用 SkipHooks 会话模式,例如:

DB.Session(&gorm.Session{SkipHooks: true}).Create(&user)

DB.Session(&gorm.Session{SkipHooks: true}).Create(&users)

DB.Session(&gorm.Session{SkipHooks: true}).CreateInBatches(users, 100)

根据 Map 创建

  1. GORM 支持根据 map[string]interface{} 和 []map[string]interface{}{} 创建记录,但是根据 map 创建记录时,association 不会被调用,且主键也不会自动填充
  2. ORM一定要时时刻刻体现model,当可以将modell当作参数使用时,就不需要Model(model)了,但参数是其他值时,需要Model(model).操作(val)来体现Model。
db.Model(&User{}).Create(map[string]interface{}{
  "Name": "jinzhu", "Age": 18,
})

// batch insert from `[]map[string]interface{}{}`
db.Model(&User{}).Create([]map[string]interface{}{
  {"Name": "jinzhu_1", "Age": 18},
  {"Name": "jinzhu_2", "Age": 20},
})

使用 SQL 表达式、Context Valuer 创建记录

GORM 允许使用 SQL 表达式插入数据,有两种方法实现这个目标。根据 map[string]interface{} 或 自定义数据类型 创建,例如:

// 通过 map 创建记录
db.Model(User{}).Create(map[string]interface{}{
  "Name": "jinzhu",
  "Location": clause.Expr{SQL: "ST_PointFromText(?)", Vars: []interface{}{"POINT(100 100)"}},
})
// INSERT INTO `users` (`name`,`location`) VALUES ("jinzhu",ST_PointFromText("POINT(100 100)"));

// 通过自定义类型创建记录
type Location struct {
    X, Y int
}

// Scan 方法实现了 sql.Scanner 接口
func (loc *Location) Scan(v interface{}) error {
  // Scan a value into struct from database driver
}

func (loc Location) GormDataType() string {
  return "geometry"
}

func (loc Location) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {
  return clause.Expr{
    SQL:  "ST_PointFromText(?)",
    Vars: []interface{}{fmt.Sprintf("POINT(%d %d)", loc.X, loc.Y)},
  }
}

type User struct {
  Name     string
  Location Location
}

db.Create(&User{
  Name:     "jinzhu",
  Location: Location{X: 100, Y: 100},
})
// INSERT INTO `users` (`name`,`location`) VALUES ("jinzhu",ST_PointFromText("POINT(100 100)"))

关联创建

创建关联数据时,如果关联值是非零值,这些关联会被 upsert,且它们的 Hook 方法也会被调用

type CreditCard struct {
  gorm.Model
  Number   string
  UserID   uint
}

type User struct {
  gorm.Model
  Name       string
  // 与CreditCard一对一关联
  CreditCard CreditCard
}

db.Create(&User{
  Name: "jinzhu",
  CreditCard: CreditCard{Number: "411111111111"}
})
// INSERT INTO `users` ...
// INSERT INTO `credit_cards` ...

可以通过 Select、 Omit 跳过关联保存,例如:

db.Omit("CreditCard").Create(&user)

// 跳过所有关联
db.Omit(clause.Associations).Create(&user)

默认值

  1. 插入记录到数据库时,默认值 会被用于 填充值为 零值 的字段

  2. 可以通过标签 default 为字段定义默认值

    type User struct {
      ID   int64
      Name string `gorm:"default:galeone"`
      Age  int64  `gorm:"default:18"`
    }
    
  3. 对于声明了默认值的字段,像 0、‘’、false 等零值是不会保存到数据库,需要使用指针类型或 Scanner/Valuer 来避免这个问题

    type User struct {
      gorm.Model
      Name string
      Age  *int           `gorm:"default:18"`
      Active sql.NullBool `gorm:"default:true"`
    }
    
  4. 若要数据库有默认、虚拟/生成的值,你必须为字段设置 default 标签。若要在迁移时跳过默认值定义,你可以使用 default:(-),例如:

    type User struct {
      ID        string `gorm:"default:uuid_generate_v3()"` // db func
      FirstName string
      LastName  string
      Age       uint8
      FullName  string `gorm:"->;type:GENERATED ALWAYS AS (concat(firstname,' ',lastname));default:(-);"`
    }
    

Upsert 及冲突

GORM 为不同数据库提供了兼容的 Upsert 支持

import "gorm.io/gorm/clause"

// Do nothing on conflict
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)

// Update columns to default value on `id` conflict
db.Clauses(clause.OnConflict{
  Columns:   []clause.Column{{Name: "id"}},
  DoUpdates: clause.Assignments(map[string]interface{}{"role": "user"}),
}).Create(&users)
// MERGE INTO "users" USING *** WHEN NOT MATCHED THEN INSERT *** WHEN MATCHED THEN UPDATE SET ***; SQL Server
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE ***; MySQL

// Use SQL expression
db.Clauses(clause.OnConflict{
  Columns:   []clause.Column{{Name: "id"}},
  DoUpdates: clause.Assignments(map[string]interface{}{"count": gorm.Expr("GREATEST(count, VALUES(count))")}),
}).Create(&users)
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `count`=GREATEST(count, VALUES(count));

// Update columns to new value on `id` conflict
db.Clauses(clause.OnConflict{
  Columns:   []clause.Column{{Name: "id"}},
  DoUpdates: clause.AssignmentColumns([]string{"name", "age"}),
}).Create(&users)
// MERGE INTO "users" USING *** WHEN NOT MATCHED THEN INSERT *** WHEN MATCHED THEN UPDATE SET "name"="excluded"."name"; SQL Server
// INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age"; PostgreSQL
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name`=VALUES(name),`age`=VALUES(age); MySQL

// Update all columns to new value on conflict except primary keys and those columns having default values from sql func
db.Clauses(clause.OnConflict{
  UpdateAll: true,
}).Create(&users)
// INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age", ...;
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name`=VALUES(name),`age`=VALUES(age), ...; MySQL

查询

检索单个对象

  1. GORM 提供了 First、Take、Last 方法,以便从数据库中检索单个对象。

  2. 如果你想避免ErrRecordNotFound错误,你可以使用Find,比如db.Limit(1).Find(&user),Find方法可以接受struct和slice的数据。

  3. 对单个对象使用Find而不带limit,db.Find(&user)将会查询整个表并且只返回第一个对象,这是性能不高并且不确定的。

// 获取第一条记录(主键升序)
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 or nil

// 检查 ErrRecordNotFound 错误
errors.Is(result.Error, gorm.ErrRecordNotFound)


var user User
var users []User
// works because destination struct is passed in
db.First(&user)
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1

// 有效,因为模型是使用`db.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

使用:

func GetSpUser(model int) *User {
	var user User
	var result *gorm.DB
	switch model {
	case 1:
		result = handler.First(&user)
	case 2:
		result = handler.Take(&user)
	case 3:
		result = handler.Last(&user)
	default:
		result = handler.Take(&user)
	}
	if result.Error != nil {
		return nil
	}
	return &user
}

根据主键检索

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);

如果主键是字符串(像uuid),则查询将按如下方式编写:
db.First(&user, "id = ?", "1b74413f-f3b8-409f-ac47-e8c062e3472a")
// SELECT * FROM users WHERE id = "1b74413f-f3b8-409f-ac47-e8c062e3472a";

当目标对象有一个主键值时,将使用主键构建查询条件
var user = User{ID: 10}
db.First(&user)
// SELECT * FROM users WHERE id = 10;

var result User
db.Model(User{ID: 10}).First(&result)
// SELECT * FROM users WHERE id = 10;

检索全部对象

// Get all records
result := db.Find(&users)
// SELECT * FROM users;

result.RowsAffected // returns found records count, equals `len(users)`
result.Error        // returns error

Where条件查询

String 条件
// Get first matched record
db.Where("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;

// Get all matched records
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';

如果对象的主键已设置,则条件查询不会覆盖主键的值,而是将其用作“和”条件:

var user = User{ID: 10}
db.Where("id = ?", 20).First(&user)
// SELECT * FROM users WHERE id = 10 and id = 20 ORDER BY id ASC LIMIT 1
Struct & Map 条件
/ 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;

// Slice of primary keys
db.Where([]int64{20, 21, 22}).Find(&users)
// SELECT * FROM users WHERE id IN (20, 21, 22);

注意:当使用struct进行查询时,GORM将仅使用非零字段进行查询,这意味着如果字段的值为0、“”、false或其他零值,则不会将其用于构建查询条件,例如:

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

要在查询条件中包含零值,可以使用map,该map将包含所有关键字值作为查询条件,例如:

db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;
指定结构体查询字段
db.Where(&User{Name: "jinzhu"}, "name", "Age").Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;

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

查询条件可以以与Where类似的方式内联到First和Find等方法中。

// Get by primary key if it were a non-integer type
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 条件

构建NOT条件,类似于Where

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;

// Not In slice of primary keys
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' AND age = 18);
选择特定字段

选择允许指定要从数据库检索的字段,否则,GORM将默认选择所有字段。

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;

排序
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;

db.Clauses(clause.OrderBy{
  Expression: clause.Expr{SQL: "FIELD(id,?)", Vars: []interface{}{[]int{1, 2, 3}}, WithoutParentheses: true},
}).Find(&User{})
// SELECT * FROM users ORDER BY FIELD(id,1,2,3)
Limit & Offset
db.Limit(3).Find(&users)
// SELECT * FROM users LIMIT 3;

// Cancel limit condition with -1
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;

// Cancel offset condition with -1
db.Offset(10).Find(&users1).Offset(-1).Find(&users2)
// SELECT * FROM users OFFSET 10; (users1)
// SELECT * FROM users; (users2)
Group By(分组) & 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)
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)

// multiple joins with parameter
db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "jinzhu@example.org").Joins("JOIN credit_cards ON credit_cards.user_id = users.id").Where("credit_cards.number = ?", "411111111111").Find(&user)

Joins 预加载
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`;


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;
Joins 一个衍生表
type User struct {
    Id  int
    Age int
}

type Order struct {
    UserId     int
    FinishedAt *time.Time
}

query := db.Table("order").Select("MAX(order.finished_at) as latest").Joins("left join user user on order.user_id = user.id").Where("user.age > ?", 18).Group("order.user_id")
db.Model(&Order{}).Joins("join (?) q on order.finished_at = q.latest", query).Scan(&results)
// SELECT `order`.`user_id`,`order`.`finished_at` FROM `order` join (SELECT MAX(order.finished_at) as latest FROM `order` left join user user on order.user_id = user.id WHERE user.age > 18 GROUP BY `order`.`user_id`) q on order.finished_at = q.latest

Scan

将结果扫描到结构体中

type Result struct {
  Name string
  Age  int
}

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

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

高级查询

智能选择字段

GORM 允许通过 Select 方法选择特定的字段,如果您在应用程序中经常使用此功能,你也可以定义一个较小的结构体,以实现调用 API 时自动选择特定的字段,例如:

type User struct {
  ID     uint
  Name   string
  Age    int
  Gender string
  // 假设后面还有几百个字段...
}

type APIUser struct {
  ID   uint
  Name string
}

// 查询时会自动选择 `id`, `name` 字段
db.Model(&User{}).Limit(10).Find(&APIUser{})
// SELECT `id`, `name` FROM `users` LIMIT 10

子查询

子查询可以嵌套在查询中,GORM 允许在使用 *gorm.DB 对象作为参数时生成子查询

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%")

From 子查询

GORM 允许您在 Table 方法中通过 FROM 子句使用子查询,例如:

db.Table("(?) as u", db.Model(&User{}).Select("name", "age")).Where("age = ?", 18).Find(&User{})
// SELECT * FROM (SELECT `name`,`age` FROM `users`) as u WHERE `age` = 18

subQuery1 := db.Model(&User{}).Select("name")
subQuery2 := db.Model(&Pet{}).Select("name")
db.Table("(?) as u, (?) as p", subQuery1, subQuery2).Find(&User{})
// SELECT * FROM (SELECT `name` FROM `users`) as u, (SELECT `name` FROM `pets`) as p

Group 条件

使用 Group 条件可以更轻松的编写复杂 SQL

db.Where(
    db.Where("pizza = ?", "pepperoni").Where(db.Where("size = ?", "small").Or("size = ?", "medium")),
).Or(
    db.Where("pizza = ?", "hawaiian").Where("size = ?", "xlarge"),
).Find(&Pizza{}).Statement

// SELECT * FROM `pizzas` WHERE (pizza = "pepperoni" AND (size = "small" OR size = "medium")) OR (pizza = "hawaiian" AND size = "xlarge")

带多个列的 In

带多个列的 In 查询

db.Where("(name, age, role) IN ?", [][]interface{}{{"jinzhu", 18, "admin"}, {"jinzhu2", 19, "user"}}).Find(&users)
// SELECT * FROM users WHERE (name, age, role) IN (("jinzhu", 18, "admin"), ("jinzhu 2", 19, "user"));

命名参数

GORM 支持 sql.NamedArg 和 map[string]interface{}{} 形式的命名参数,例如:

db.Where("name1 = @name OR name2 = @name", sql.Named("name", "jinzhu")).Find(&user)
// SELECT * FROM `users` WHERE name1 = "jinzhu" OR name2 = "jinzhu"

db.Where("name1 = @name OR name2 = @name", map[string]interface{}{"name": "jinzhu"}).First(&user)
// SELECT * FROM `users` WHERE name1 = "jinzhu" OR name2 = "jinzhu" ORDER BY `users`.`id` LIMIT 1

Find 至 map

GORM 允许扫描结果至 map[string]interface{} 或 []map[string]interface{},此时别忘了指定 Model 或 Table,例如:

result := map[string]interface{}{}
db.Model(&User{}).First(&result, "id = ?", 1)

var results []map[string]interface{}
db.Table("users").Find(&results)

FirstOrInit

获取第一条匹配的记录,或者根据给定的条件初始化一个实例(仅支持 sturct 和 map 条件)

// 未找到 user,则根据给定的条件初始化一条记录
db.FirstOrInit(&user, User{Name: "non_existing"})
// user -> User{Name: "non_existing"}

// 找到了 `name` = `jinzhu` 的 user
db.Where(User{Name: "jinzhu"}).FirstOrInit(&user)
// user -> User{ID: 111, Name: "Jinzhu", Age: 18}

// 找到了 `name` = `jinzhu` 的 user
db.FirstOrInit(&user, map[string]interface{}{"name": "jinzhu"})
// user -> User{ID: 111, Name: "Jinzhu", Age: 18}

如果没有找到记录,可以使用包含更多的属性的结构体初始化 user,Attrs 不会被用于生成查询 SQL

// 未找到 user,则根据给定的条件以及 Attrs 初始化 user
db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrInit(&user)
// SELECT * FROM USERS WHERE name = 'non_existing' ORDER BY id LIMIT 1;
// user -> User{Name: "non_existing", Age: 20}

// 未找到 user,则根据给定的条件以及 Attrs 初始化 user
db.Where(User{Name: "non_existing"}).Attrs("age", 20).FirstOrInit(&user)
// SELECT * FROM USERS WHERE name = 'non_existing' ORDER BY id LIMIT 1;
// user -> User{Name: "non_existing", Age: 20}

// 找到了 `name` = `jinzhu` 的 user,则忽略 Attrs
db.Where(User{Name: "Jinzhu"}).Attrs(User{Age: 20}).FirstOrInit(&user)
// SELECT * FROM USERS WHERE name = jinzhu' ORDER BY id LIMIT 1;
// user -> User{ID: 111, Name: "Jinzhu", Age: 18}

不管是否找到记录,Assign 都会将属性赋值给 struct,但这些属性不会被用于生成查询 SQL,也不会被保存到数据库

// 未找到 user,根据条件和 Assign 属性初始化 struct
db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrInit(&user)
// user -> User{Name: "non_existing", Age: 20}

// 找到 `name` = `jinzhu` 的记录,依然会更新 Assign 相关的属性
db.Where(User{Name: "Jinzhu"}).Assign(User{Age: 20}).FirstOrInit(&user)
// SELECT * FROM USERS WHERE name = jinzhu' ORDER BY id LIMIT 1;
// user -> User{ID: 111, Name: "Jinzhu", Age: 20}

FirstOrCreate

获取匹配的第一条记录或者根据给定条件创建一条新纪录(仅 struct, map 条件有效),RowsAffected 返回创建、更新的记录数

// 未找到 User,根据给定条件创建一条新纪录
result := db.FirstOrCreate(&user, User{Name: "non_existing"})
// INSERT INTO "users" (name) VALUES ("non_existing");
// user -> User{ID: 112, Name: "non_existing"}
// result.RowsAffected // => 1

// 找到 `name` = `jinzhu` 的 User
result := db.Where(User{Name: "jinzhu"}).FirstOrCreate(&user)
// user -> User{ID: 111, Name: "jinzhu", "Age": 18}
// result.RowsAffected // => 0

如果没有找到记录,可以使用包含更多的属性的结构体创建记录,Attrs 不会被用于生成查询 SQL 。

// 未找到 user,根据条件和 Assign 属性创建记录
db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrCreate(&user)
// SELECT * FROM users WHERE name = 'non_existing' ORDER BY id LIMIT 1;
// INSERT INTO "users" (name, age) VALUES ("non_existing", 20);
// user -> User{ID: 112, Name: "non_existing", Age: 20}

// 找到了 `name` = `jinzhu` 的 user,则忽略 Attrs
db.Where(User{Name: "jinzhu"}).Attrs(User{Age: 20}).FirstOrCreate(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;
// user -> User{ID: 111, Name: "jinzhu", Age: 18}

不管是否找到记录,Assign 都会将属性赋值给 struct,并将结果写回数据库

// 未找到 user,根据条件和 Assign 属性创建记录
db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrCreate(&user)
// SELECT * FROM users WHERE name = 'non_existing' ORDER BY id LIMIT 1;
// INSERT INTO "users" (name, age) VALUES ("non_existing", 20);
// user -> User{ID: 112, Name: "non_existing", Age: 20}

// 找到了 `name` = `jinzhu` 的 user,依然会根据 Assign 更新记录
db.Where(User{Name: "jinzhu"}).Assign(User{Age: 20}).FirstOrCreate(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;
// UPDATE users SET age=20 WHERE id = 111;
// user -> User{ID: 111, Name: "jinzhu", Age: 20}

迭代

GORM 支持通过行进行迭代

rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Rows()
defer rows.Close()

for rows.Next() {
  var user User
  // ScanRows 方法用于将一行记录扫描至结构体
  db.ScanRows(rows, &user)

  // 业务逻辑...
}

FindInBatches

用于批量查询并处理记录

// 每次批量处理 100 条
result := db.Where("processed = ?", false).FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error {
  for _, result := range results {
    // 批量处理找到的记录
  }

  tx.Save(&results)

  tx.RowsAffected // 本次批量操作影响的记录数

  batch // Batch 1, 2, 3

  // 如果返回错误会终止后续批量操作
  return nil
})

result.Error // returned error
result.RowsAffected // 整个批量操作影响的记录数

查询钩子

对于查询操作,GORM 支持 AfterFind 钩子,查询记录后会调用它,详情请参考 钩子

func (u *User) AfterFind(tx *gorm.DB) (err error) {
  if u.Role == "" {
    u.Role = "user"
  }
  return
}

Pluck

Pluck 用于从数据库查询单个列,并将结果扫描到切片。如果您想要查询多列,您应该使用 Select 和 Scan

var ages []int64
db.Model(&users).Pluck("age", &ages)

var names []string
db.Model(&User{}).Pluck("name", &names)

db.Table("deleted_users").Pluck("name", &names)

// Distinct Pluck
db.Model(&User{}).Distinct().Pluck("Name", &names)
// SELECT DISTINCT `name` FROM `users`

// 超过一列的查询,应该使用 `Scan` 或者 `Find`,例如:
db.Select("name", "age").Scan(&users)
db.Select("name", "age").Find(&users)

Scope

Scopes 允许你指定常用的查询,可以在调用方法时引用这些查询

func AmountGreaterThan1000(db *gorm.DB) *gorm.DB {
  return db.Where("amount > ?", 1000)
}

func PaidWithCreditCard(db *gorm.DB) *gorm.DB {
  return db.Where("pay_mode_sign = ?", "C")
}

func PaidWithCod(db *gorm.DB) *gorm.DB {
  return db.Where("pay_mode_sign = ?", "C")
}

func OrderStatus(status []string) func (db *gorm.DB) *gorm.DB {
  return func (db *gorm.DB) *gorm.DB {
    return db.Where("status IN (?)", status)
  }
}

db.Scopes(AmountGreaterThan1000, PaidWithCreditCard).Find(&orders)
// 查找所有金额大于 1000 的信用卡订单

db.Scopes(AmountGreaterThan1000, PaidWithCod).Find(&orders)
// 查找所有金额大于 1000 的货到付款订单

db.Scopes(AmountGreaterThan1000, OrderStatus([]string{"paid", "shipped"})).Find(&orders)
// 查找所有金额大于 1000 且已付款或已发货的订单

Count

Count 用于获取匹配的记录数

var count int64
db.Model(&User{}).Where("name = ?", "jinzhu").Or("name = ?", "jinzhu 2").Count(&count)
// SELECT count(1) FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2'

db.Model(&User{}).Where("name = ?", "jinzhu").Count(&count)
// SELECT count(1) FROM users WHERE name = 'jinzhu'; (count)

db.Table("deleted_users").Count(&count)
// SELECT count(1) FROM deleted_users;

// Count with Distinct
db.Model(&User{}).Distinct("name").Count(&count)
// SELECT COUNT(DISTINCT(`name`)) FROM `users`

db.Table("deleted_users").Select("count(distinct(name))").Count(&count)
// SELECT count(distinct(name)) FROM deleted_users

// Count with Group
users := []User{
  {Name: "name1"},
  {Name: "name2"},
  {Name: "name3"},
  {Name: "name3"},
}

db.Model(&User{}).Group("name").Count(&count)
count // => 3

更新

Save保存所有字段

  1. Save 会保存所有的字段,即使字段是零值
  2. 不要给Save使用Model方法
  3. 如果Save的结构体不含有ID,则会创建,否则执行覆盖更新
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;

更新单个列

当使用 Update 更新单列时,也可以直接禁止掉全局更新(即必须传入条件,否则就全库更新)

// 条件更新
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).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;

更新多列

  1. Updates 方法支持 struct 和 map[string]interface{} 参数。
  2. 当使用 struct 更新时,默认情况下,GORM 只会更新非零值的字段
  3. 当使用 struct 进行更新时,GORM 只会更新非零值的字段,可以使用 map 更新字段,或者使用 Select 指定要更新的字段。
// 根据 `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;

更新 Hook

GORM allows the hooks BeforeSave, BeforeUpdate, AfterSave, AfterUpdate. Those methods will be called when updating a record。(GORM允许以上钩子,这些方法会在更新了一个记录时被调用)

func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
    if u.Role == "admin" {
        return errors.New("admin user not allowed to update")
    }
    return
}

更新选定字段

// Select with Map
// 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;

// Select with Struct (select zero value fields)
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 all fields (select all fields include zero value fields)
db.Model(&user).Select("*").Updates(User{Name: "jinzhu", Role: "admin", Age: 0})

// Select all fields but omit Role (select all fields include zero value fields)
db.Model(&user).Select("*").Omit("Role").Updates(User{Name: "jinzhu", Role: "admin", Age: 0})

批量更新

// 根据 struct 更新
db.Model(User{}).Where("role = ?", "admin").Updates(User{Name: "hello", Age: 18})
// UPDATE users SET name='hello', age=18 WHERE role = 'admin';

// 根据 map 更新
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);

更新的记录数

// 通过 `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        // 更

使用 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.Model(&user).Update("company_name", db.Model(&Company{}).Select("name").Where("companies.id = users.company_id"))
// UPDATE "users" SET "company_name" = (SELECT name FROM companies WHERE companies.id = users.company_id);

db.Table("users as u").Where("name = ?", "jinzhu").Update("company_name", db.Table("companies as c").Select("name").Where("c.id = u.company_id"))

db.Table("users as u").Where("name = ?", "jinzhu").Updates(map[string]interface{}{"company_name": db.Table("companies as c").Select("name").Where("c.id = u.company_id")})

不使用 Hook 和时间追踪

可以使用 UpdateColumn、UpdateColumns,其用法类似于 Update、Updates

// 更新单个列
db.Model(&user).UpdateColumn("name", "hello")
// UPDATE users SET name='hello' WHERE id = 111;

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

// 更新选中的列
db.Model(&user).Select("name", "age").UpdateColumns(User{Name: "hello", Age: 0})
// UPDATE users SET name='hello', age=0 WHERE id = 111;

返回修改行的数据

返回被修改的数据,仅适用于支持 Returning 的数据库,例如:

// 返回所有列
var users []User
DB.Model(&users).Clauses(clause.Returning{}).Where("role = ?", "admin").Update("salary", gorm.Expr("salary * ?", 2))
// UPDATE `users` SET `salary`=salary * 2,`updated_at`="2021-10-28 17:37:23.19" WHERE role = "admin" RETURNING *
// users => []User{{ID: 1, Name: "jinzhu", Role: "admin", Salary: 100}, {ID: 2, Name: "jinzhu.2", Role: "admin", Salary: 1000}}

// 返回指定的列
DB.Model(&users).Clauses(clause.Returning{Columns: []clause.Column{{Name: "name"}, {Name: "salary"}}}).Where("role = ?", "admin").Update("salary", gorm.Expr("salary * ?", 2))
// UPDATE `users` SET `salary`=salary * 2,`updated_at`="2021-10-28 17:37:23.19" WHERE role = "admin" RETURNING `name`, `salary`
// users => []User{{ID: 0, Name: "jinzhu", Role: "", Salary: 100}, {ID: 0, Name: "jinzhu.2", Role: "", Salary: 1000}}

检查字段是否有变更

GORM 提供了 Changed 方法,它可以被用在 Before Update Hook 里,它会返回字段是否有变更的布尔值.

Changed 方法只能与 Update、Updates 方法一起使用,并且它只是检查 Model 对象字段的值与 Update、Updates 的值是否相等。 如果值有变更,且字段没有被忽略,则返回 true.

func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
  // 如果 Role 字段有变更
    if tx.Statement.Changed("Role") {
    return errors.New("role not allowed to change")
    }

  if tx.Statement.Changed("Name", "Admin") { // 如果 Name 或 Role 字段有变更
    tx.Statement.SetColumn("Age", 18)
  }

  // 如果任意字段有变更
    if tx.Statement.Changed() {
        tx.Statement.SetColumn("RefreshedAt", time.Now())
    }
    return nil
}

db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(map[string]interface{"name": "jinzhu2"})
// Changed("Name") => true
db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(map[string]interface{"name": "jinzhu"})
// Changed("Name") => false, 因为 `Name` 没有变更
db.Model(&User{ID: 1, Name: "jinzhu"}).Select("Admin").Updates(map[string]interface{
  "name": "jinzhu2", "admin": false,
})
// Changed("Name") => false, 因为 `Name` 没有被 Select 选中并更新

db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(User{Name: "jinzhu2"})
// Changed("Name") => true
db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(User{Name: "jinzhu"})
// Changed("Name") => false, 因为 `Name` 没有变更
db.Model(&User{ID: 1, Name: "jinzhu"}).Select("Admin").Updates(User{Name: "jinzhu2"})
// Changed("Name") => false, 因为 `Name` 没有被 Select 选中并更新

在 Update 时修改值

若要在 Before 钩子中改变要更新的值,如果它是一个完整的更新,可以使用 Save;否则,应该使用 SetColumn ,例如:

func (user *User) BeforeSave(tx *gorm.DB) (err error) {
  if pw, err := bcrypt.GenerateFromPassword(user.Password, 0); err == nil {
    tx.Statement.SetColumn("EncryptedPassword", pw)
  }

  if tx.Statement.Changed("Code") {
    user.Age += 20
    tx.Statement.SetColumn("Age", user.Age)
  }
}

db.Model(&user).Update("Name", "jinzhu")

删除

删除一条记录

删除一条记录时,删除对象需要指定主键,否则会触发 批量 Delete

// Email 的 ID 是 `10`
db.Delete(&email)
// DELETE from emails where id = 10;

// 带额外条件的删除
db.Where("name = ?", "jinzhu").Delete(&email)
// DELETE from emails where id = 10 AND name = "jinzhu";

根据主键删除

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);

Delete Hook

对于删除操作,GORM 支持 BeforeDelete、AfterDelete Hook,在删除记录时会调用这些方法。

func (u *User) BeforeDelete(tx *gorm.DB) (err error) {
    if u.Role == "admin" {
        return errors.New("admin user not allowed to delete")
    }
    return
}

批量删除

如果指定的值不包括主属性,那么 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 错误

对此,必须加一些条件,或者使用原生 SQL,或者启用 AllowGlobalUpdate(全局删除) 模式。

要有效地删除大量记录,请将带有主键的切片传递给delete方法。

var users = []User{{ID: 1}, {ID: 2}, {ID: 3}}
db.Delete(&users)
// DELETE FROM users WHERE id IN (1,2,3);

db.Delete(&users, "name LIKE ?", "%jinzhu%")
// DELETE FROM users WHERE name LIKE "%jinzhu%" AND id IN (1,2,3); 

阻止全局删除

如果您在没有任何条件的情况下执行批量删除,GORM将不会运行它,并将返回ErrMissingWhereClause错误

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

db.Delete(&[]User{{Name: "jinzhu1"}, {Name: "jinzhu2"}}).Error // gorm.ErrMissingWhereClause

db.Where("1 = 1").Delete(&User{})
// DELETE FROM `users` WHERE 1=1

db.Exec("DELETE FROM users")
// DELETE FROM users

db.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&User{})
// DELETE FROM users

返回删除行的数据

返回被删除的数据,仅适用于支持 Returning 的数据库,例如:

// 返回所有列
var users []User
DB.Clauses(clause.Returning{}).Where("role = ?", "admin").Delete(&users)
// DELETE FROM `users` WHERE role = "admin" RETURNING *
// users => []User{{ID: 1, Name: "jinzhu", Role: "admin", Salary: 100}, {ID: 2, Name: "jinzhu.2", Role: "admin", Salary: 1000}}

// 返回指定的列
DB.Clauses(clause.Returning{Columns: []clause.Column{{Name: "name"}, {Name: "salary"}}}).Where("role = ?", "admin").Delete(&users)
// DELETE FROM `users` WHERE role = "admin" RETURNING `name`, `salary`
// users => []User{{ID: 0, Name: "jinzhu", Role: "", Salary: 100}, {ID: 0, Name: "jinzhu.2", Role: "", Salary: 1000}}

软删除

  1. 如果模型包含了一个 gorm.deletedat 字段(gorm.Model 已经包含了该字段),它将自动获得软删除的能力。
  2. 拥有软删除能力的模型调用 Delete 时,记录不会从数据库中被真正删除。但 GORM 会将 DeletedAt 置为当前时间, 并且不能再通过普通的查询方法找到该记录。
// 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;

如果你不想包含gorm.Model,可以启用软删除功能如下:

type User struct {
  ID      int
  Deleted gorm.DeletedAt
  Name    string
}

查找被软删除的记录和永久删除(Unscoped)

  1. 您可以使用Unscoped查找软删除的记录
  2. 您可以使用Unscoped永久删除匹配的记录
db.Unscoped().Where("age = 20").Find(&users)
// SELECT * FROM users WHERE age = 20;
db.Unscoped().Delete(&order)
// DELETE FROM orders WHERE id=10;

Delete Flag

默认情况下,gorm.Model 使用 *time.Time 作为 DeletedAt 字段的值。此外,通过 gorm.io/plugin/soft_delete 插件还支持其它数据格式。

纳秒:

import "gorm.io/plugin/soft_delete"

type User struct {
  ID    uint
  Name  string
  DeletedAt soft_delete.DeletedAt `gorm:"softDelete:milli"`
  // DeletedAt soft_delete.DeletedAt `gorm:"softDelete:nano"`
}

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

// 删除
UPDATE users SET deleted_at = /* 当前毫秒、纳秒时间戳 */ WHERE ID = 1;

1/0:

import "gorm.io/plugin/soft_delete"

type User struct {
  ID    uint
  Name  string
  IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}

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

// 删除
UPDATE users SET is_del = 1 WHERE ID = 1;

混合模式:

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

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

// 删除
UPDATE users SET is_del = 1, deleted_at = /* current unix second */ WHERE ID = 1;

原生SQL

原生查询 SQL 和 Scan

type Result struct {
  ID   int
  Name string
  Age  int
}

var result Result
db.Raw("SELECT id, name, age FROM users WHERE name = ?", 3).Scan(&result)

db.Raw("SELECT id, name, age FROM users WHERE name = ?", 3).Scan(&result)

var age int
db.Raw("SELECT SUM(age) FROM users WHERE role = ?", "admin").Scan(&age)

var users []User
db.Raw("UPDATE users SET name = ? WHERE age = ? RETURNING id, name", "jinzhu", 20).Scan(&users)

Exec 原生 SQL

db.Exec("DROP TABLE users")
db.Exec("UPDATE orders SET shipped_at = ? WHERE id IN ?", time.Now(), []int64{1, 2, 3})

// Exec with SQL Expression
db.Exec("UPDATE users SET money = ? WHERE name = ?", gorm.Expr("money * ? + ?", 10000, 1), "jinzhu")

命名参数

GORM 支持 sql.NamedArg、map[string]interface{}{} 或 struct 形式的命名参数,例如:

db.Where("name1 = @name OR name2 = @name", sql.Named("name", "jinzhu")).Find(&user)
// SELECT * FROM `users` WHERE name1 = "jinzhu" OR name2 = "jinzhu"

db.Where("name1 = @name OR name2 = @name", map[string]interface{}{"name": "jinzhu2"}).First(&result3)
// SELECT * FROM `users` WHERE name1 = "jinzhu2" OR name2 = "jinzhu2" ORDER BY `users`.`id` LIMIT 1

// 原生 SQL 及命名参数
db.Raw("SELECT * FROM users WHERE name1 = @name OR name2 = @name2 OR name3 = @name",
   sql.Named("name", "jinzhu1"), sql.Named("name2", "jinzhu2")).Find(&user)
// SELECT * FROM users WHERE name1 = "jinzhu1" OR name2 = "jinzhu2" OR name3 = "jinzhu1"

db.Exec("UPDATE users SET name1 = @name, name2 = @name2, name3 = @name",
   sql.Named("name", "jinzhunew"), sql.Named("name2", "jinzhunew2"))
// UPDATE users SET name1 = "jinzhunew", name2 = "jinzhunew2", name3 = "jinzhunew"

db.Raw("SELECT * FROM users WHERE (name1 = @name AND name3 = @name) AND name2 = @name2",
   map[string]interface{}{"name": "jinzhu", "name2": "jinzhu2"}).Find(&user)
// SELECT * FROM users WHERE (name1 = "jinzhu" AND name3 = "jinzhu") AND name2 = "jinzhu2"

type NamedArgument struct {
    Name string
    Name2 string
}

db.Raw("SELECT * FROM users WHERE (name1 = @Name AND name3 = @Name) AND name2 = @Name2",
     NamedArgument{Name: "jinzhu", Name2: "jinzhu2"}).Find(&user)
// SELECT * FROM users WHERE (name1 = "jinzhu" AND name3 = "jinzhu") AND name2 = "jinzhu2"

DryRun 模式

在不执行的情况下生成 SQL 及其参数,可以用于准备或测试生成的 SQL

stmt := db.Session(&Session{DryRun: true}).First(&user, 1).Statement
stmt.SQL.String() //=> SELECT * FROM `users` WHERE `id` = $1 ORDER BY `id`
stmt.Vars         //=> []interface{}{1}

ToSQL(不一定安全)

  1. 返回生成的 SQL 但不执行。

  2. GORM使用 database/sql 的参数占位符来构建 SQL 语句,它会自动转义参数以避免 SQL 注入,但不保证生成 SQL 的安全,请只用于调试。

sql := DB.ToSQL(func(tx *gorm.DB) *gorm.DB {
  return tx.Model(&User{}).Where("id = ?", 100).Limit(10).Order("age desc").Find(&[]User{})
})
sql //=> SELECT * FROM "users" WHERE id = 100 AND "users"."deleted_at" IS NULL ORDER BY age desc LIMIT 10

Row & Rows

// 使用 GORM API 构建 SQL
row := db.Table("users").Where("name = ?", "jinzhu").Select("name", "age").Row()
row.Scan(&name, &age)

// 使用原生 SQL
row := db.Raw("select name, age, email from users where name = ?", "jinzhu").Row()
row.Scan(&name, &age, &email)

// 使用 GORM API 构建 SQL
rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows()
defer rows.Close()
for rows.Next() {
  rows.Scan(&name, &age, &email)

  // 业务逻辑...
}

// 原生 SQL
rows, err := db.Raw("select name, age, email from users where name = ?", "jinzhu").Rows()
defer rows.Close()
for rows.Next() {
  rows.Scan(&name, &age, &email)

  // 业务逻辑...
}

将 sql.Rows 扫描至 model

rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows() // (*sql.Rows, error)
defer rows.Close()

var user User
for rows.Next() {
  // ScanRows 将一行扫描至 user
  db.ScanRows(rows, &user)

  // 业务逻辑...
}

连接

在一条 tcp DB 连接中运行多条 SQL (不是事务)

db.Connection(func(tx *gorm.DB) error {
  tx.Exec("SET my.role = ?", "admin")

  tx.First(&User{})
})

子句(Clause)

  1. GORM 内部使用 SQL builder 生成 SQL。对于每个操作,GORM 都会创建一个 *gorm.Statement 对象,所有的 GORM API 都是在为 statement 添加、修改 子句,最后,GORM 会根据这些子句生成 SQL

  2. 例如,当通过 First 进行查询时,它会在 Statement 中添加以下子句

    clause.Select{Columns: "*"}
    clause.From{Tables: clause.CurrentTable}
    clause.Limit{Limit: 1}
    clause.OrderByColumn{
      Column: clause.Column{Table: clause.CurrentTable, Name: clause.PrimaryKey},
    }
    

然后 GORM 在 Query callback 中构建最终的查询 SQL:

Statement.Build("SELECT", "FROM", "WHERE", "GROUP BY", "ORDER BY", "LIMIT", "FOR")

生成 SQL:

SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1
子句构造器

不同的数据库, 子句可能会生成不同的 SQL

db.Offset(10).Limit(5).Find(&users)
// SQL Server 会生成
// SELECT * FROM "users" OFFSET 10 ROW FETCH NEXT 5 ROWS ONLY
// MySQL 会生成
// SELECT * FROM `users` LIMIT 5 OFFSET 10
子句选项

GORM 定义了很多 子句,其中一些 子句提供了你可能会用到的选项

尽管很少会用到它们,但如果你发现 GORM API 与你的预期不符合。这可能可以很好地检查它们,例如:

db.Clauses(clause.Insert{Modifier: "IGNORE"}).Create(&user)
// INSERT IGNORE INTO users (name,age...) VALUES ("jinzhu",18...);
StatementModifier

GORM 提供了 StatementModifier 接口,允许您修改语句,使其符合您的要求,这儿有一个 Hint 示例

import "gorm.io/hints"

db.Clauses(hints.New("hint")).Find(&User{})
// SELECT * /*+ hint */ FROM `users`

关联关系

一对一

Belongs To

  1. belongs to 会与另一个模型建立了一对一的连接。
  2. 这种模型的每一个实例都“属于”另一个模型的一个实例。
  3. 在 User 对象中,有一个和 Company 一样的 CompanyID,默认情况下, CompanyID 被隐含地用来在 User 和 Company 之间创建一个外键关系, 因此必须包含在 User 结构体中才能填充 Company 内部结构体
  4. 外键的名字一般使用CompanyID
// `User` 属于 `Company`,`CompanyID` 是外键
type User struct {
  gorm.Model
  Name      string
  // company_id
  CompanyID int
  // 注意不是匿名嵌套
  Company   Company
}

type Company struct {
  ID   int
  Name string
}
重写外键

要定义一个 belongs to 关系,数据库的表中必须存在外键。默认情况下,外键的名字,使用拥有者的类型名称加上表的主键的字段名字

例如,定义一个User实体属于Company实体,那么外键的名字一般使用CompanyID。

GORM同时提供自定义外键名字的方式,如下例所示。

type User struct {
  gorm.Model
  Name         string
  CompanyRefer int
  // 使用 CompanyRefer 作为外键
  // 若如此做,User表中的字段为"...,name,company_refer"
  // 区别于:"...,name,company_id"
  Company      Company `gorm:"foreignKey:CompanyRefer"`
}

type Company struct {
  ID   int
  Name string
}
重写引用

对于 belongs to 关系,GORM 通常使用数据库表,主表(拥有者)的主键值作为外键参考。 正如上面的例子,我们使用主表Company中的主键字段ID作为外键的参考值。

如果在Company实体中设置了User实体,那么GORM会自动把Company中的ID属性保存到User的CompanyID属性中。

同样的,您也可以使用标签 references 来更改它,例如:

// `User` 属于 `Company`,`CompanyID` 是外键
type User struct {
  gorm.Model
  Name      string
  // 关联时会将Company的ID放到company_id
  CompanyID string
  // 使用 Code 作为引用
  Company   Company `gorm:"references:Code"`
}

type Company struct {
  ID   int
  Code string
  Name string
}

如果外键名恰好在拥有者类型中存在,GORM 通常会错误的认为它是 has one 关系。我们需要在 belongs to 关系中指定 references

// `User` 属于 `Company`,`CompanyID` 是外键
type User struct {
  gorm.Model
  Name      string
  // 外键名
  CompanyID string
  Company   Company `gorm:"references:CompanyID"` // 使用 Company.CompanyID 作为引用
}

// 拥有者
type Company struct {
  CompanyID   int
  Code        string
  Name        string
}
外键约束

可以通过 constraint 标签配置 OnUpdate、OnDelete 实现外键约束,在使用 GORM 进行迁移时它会被创建。

type User struct {
  gorm.Model
  Name      string
  CompanyID int
  Company   Company `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}

type Company struct {
  ID   int
  Name string
}

Has One

has one 与另一个模型建立一对一的关联,但它和一对一关系有些许不同。 这种关联表明一个模型的每个实例都包含或拥有另一个模型的一个实例。

例如,应用包含 user 和 credit card 模型,且每个 user 只能有一张 credit card。

// User 有一张 CreditCard,UserID 是外键
type User struct {
  gorm.Model
  CreditCard CreditCard
}

type CreditCard struct {
  gorm.Model
  Number string
  // user_id
  UserID uint
}
检索
// 检索用户列表并预加载信用卡
func GetAll(db *gorm.DB) ([]User, error) {
    var users []User
    err := db.Model(&User{}).Preload("CreditCard").Find(&users).Error
    return users, err
}
重写外键

对于 has one 关系,同样必须存在外键字段。拥有者将把属于它的模型的主键保存到这个字段。

这个字段的名称通常由 has one 模型的类型(User)加上其(ID) 主键 生成,对于上面的例子,它是 UserID

为 user 添加 credit card 时,它会将 user 的 ID 保存到自己的 UserID 字段。

如果你想要使用另一个字段来保存该关系,你同样可以使用标签 foreignKey 来更改它,例如:

type User struct {
  gorm.Model
  CreditCard CreditCard `gorm:"foreignKey:UserName"` // 使用 UserName 作为外键
}

type CreditCard struct {
  gorm.Model
  Number   string
  UserName string
}
重写引用

默认情况下,拥有者实体会将 has one 对应模型的主键保存为外键,您也可以修改它,用另一个字段来保存,例如下面这个使用 Name 来保存的例子。

您可以使用标签 references 来更改它,例如:

type User struct {
  gorm.Model
  Name       string     `gorm:"index"`
  // 外键是CreditCard.UserName,同时将本表的name当作引用
  CreditCard CreditCard `gorm:"foreignKey:UserName;references:name"`
}

type CreditCard struct {
  gorm.Model
  Number   string
  // 会将User.Name填入到username中
  UserName string
}
多态关联

GORM 为 has one 和 has many 提供了多态关联支持,它会将拥有者实体的表名、主键值都保存到多态类型的字段中。

type Cat struct {
  ID    int
  Name  string
  Toy   Toy `gorm:"polymorphic:Owner;"`
}

type Dog struct {
  ID   int
  Name string
  Toy  Toy `gorm:"polymorphic:Owner;"`
}

type Toy struct {
  ID        int
  Name      string
  OwnerID   int
  OwnerType string
}

db.Create(&Dog{Name: "dog1", Toy: Toy{Name: "toy1"}})
// INSERT INTO `dogs` (`name`) VALUES ("dog1")
// INSERT INTO `toys` (`name`,`owner_id`,`owner_type`) VALUES ("toy1","1","dogs")

您可以使用标签 polymorphicValue 来更改多态类型的值,例如:

type Dog struct {
  ID   int
  Name string
  Toy  Toy `gorm:"polymorphic:Owner;polymorphicValue:master"`
}

type Toy struct {
  ID        int
  Name      string
  OwnerID   int
  OwnerType string
}

db.Create(&Dog{Name: "dog1", Toy: Toy{Name: "toy1"}})
// INSERT INTO `dogs` (`name`) VALUES ("dog1")
// INSERT INTO `toys` (`name`,`owner_id`,`owner_type`) VALUES ("toy1","1","master")

预加载

GORM 可以通过 Preload、Joins 预加载 has one 关联的记录.

自引用Has One
type User struct {
  gorm.Model
  Name      string
  ManagerID *uint
  Manager   *User
}
外键约束

可以通过为标签 constraint 配置 OnUpdate、OnDelete 实现外键约束

type User struct {
  gorm.Model
  CreditCard CreditCard `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}

type CreditCard struct {
  gorm.Model
  Number string
  UserID uint
}

小结

  1. belongs to属于:A属于B,A就被打了个标签,意味着这是B的东西。
  2. has one拥有:A拥有B(相当于B属于A),B就被打了个标签,意味着这是A的东西。
  3. 你是主人,你给你的玩具写上自己的名字来表示他是你的东西。

一对多

has many 与另一个模型建立了一对多的连接。 不同于 has one,拥有者可以有零或多个关联模型。

例如,您的应用包含 user 和 credit card 模型,且每个 user 可以有多张 credit card。

// User 有多张 CreditCard,UserID 是外键
type User struct {
  gorm.Model
  // CreditCards不出现在表中仅仅作为一种符号标记给gorm自己使用,如:Preload()
  CreditCards []CreditCard
}

type CreditCard struct {
  gorm.Model
  Number string
  UserID uint
}

检索

// 检索用户列表并预加载信用卡
func GetAll(db *gorm.DB) ([]User, error) {
    var users []User
    err := db.Model(&User{}).Preload("CreditCards").Find(&users).Error
    return users, err
}

重写外键

要定义 has many 关系,同样必须存在外键。 默认的外键名是拥有者的类型名加上其主键字段名

例如,要定义一个属于User 的模型,则其外键应该是 UserID

此外,想要使用另一个字段作为外键,您可以使用 foreignKey 标签自定义它:

type User struct {
  gorm.Model
  CreditCards []CreditCard `gorm:"foreignKey:UserRefer"`
}

type CreditCard struct {
  gorm.Model
  Number    string
  UserRefer uint
}

重写引用

GORM 通常使用拥有者的主键作为外键的值。 对于上面的例子,它是 User 的 ID 字段。

为 user 添加 credit card 时,GORM 会将 userID 字段保存到 credit card 的 UserID 字段。

同样的,您也可以使用标签 references 来更改它,例如:

type User struct {
  gorm.Model
  MemberNumber string
  CreditCards  []CreditCard `gorm:"foreignKey:UserNumber;references:MemberNumber"`
}

type CreditCard struct {
  gorm.Model
  Number     string
  UserNumber string
}

多态关联

GORM 为 has one 和 has many 提供了多态关联支持,它会将拥有者实体的表名、主键都保存到多态类型的字段中。

type Dog struct {
  ID   int
  Name string
  Toys []Toy `gorm:"polymorphic:Owner;"`
}

type Toy struct {
  ID        int
  Name      string
  OwnerID   int
  OwnerType string
}

db.Create(&Dog{Name: "dog1", Toys: []Toy{{Name: "toy1"}, {Name: "toy2"}}})
// INSERT INTO `dogs` (`name`) VALUES ("dog1")
// INSERT INTO `toys` (`name`,`owner_id`,`owner_type`) VALUES ("toy1","1","dogs"), ("toy2","1","dogs")

您可以使用标签 polymorphicValue 来更改多态类型的值,例如:

type Dog struct {
  ID   int
  Name string
  Toys []Toy `gorm:"polymorphic:Owner;polymorphicValue:master"`
}

type Toy struct {
  ID        int
  Name      string
  OwnerID   int
  OwnerType string
}

db.Create(&Dog{Name: "dog1", Toys: []Toy{{Name: "toy1"}, {Name: "toy2"}}})
// INSERT INTO `dogs` (`name`) VALUES ("dog1")
// INSERT INTO `toys` (`name`,`owner_id`,`owner_type`) VALUES ("toy1","1","master"), ("toy2","1","master")

预加载

GORM 可以通过 Preload 预加载 has many 关联的记录

自引用 Has Many

type User struct {
  gorm.Model
  Name      string
  ManagerID *uint
  // Team不会出现在表中
  Team      []User `gorm:"foreignkey:ManagerID"`
}

外键约束

可以通过为标签 constraint 配置 OnUpdate、OnDelete 实现外键约束

type User struct {
  gorm.Model
  CreditCards []CreditCard `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}

type CreditCard struct {
  gorm.Model
  Number string
  UserID uint
}

多对多

Many to Many 会在两个 model 中添加一张连接表。

例如,您的应用包含了 user 和 language,且一个 user 可以说多种 language,多个 user 也可以说一种 language。

// User 拥有并属于多种 language,`user_languages` 是连接表
type User struct {
  gorm.Model
  Languages []Language `gorm:"many2many:user_languages;"`
}

type Language struct {
  gorm.Model
  Name string
}

当使用 GORM 的 AutoMigrate 为 User 创建表时,GORM 会自动创建连接表

反向引用

// User 拥有并属于多种 language,`user_languages` 是连接表
type User struct {
  gorm.Model
  Languages []*Language `gorm:"many2many:user_languages;"`
}

type Language struct {
  gorm.Model
  Name string
  // 反向声明user就可以通过预加载Users去反向查找了
  Users []*User `gorm:"many2many:user_languages;"`
}

检索

// 检索 User 列表并预加载 Language
func GetAllUsers(db *gorm.DB) ([]User, error) {
    var users []User
    err := db.Model(&User{}).Preload("Languages").Find(&users).Error
    return users, err
}

// 检索 Language 列表并预加载 User
func GetAllLanguages(db *gorm.DB) ([]Language, error) {
    var languages []Language
    err := db.Model(&Language{}).Preload("Users").Find(&languages).Error
    return languages, err
}

重写外键

对于 many2many 关系,连接表会同时拥有两个模型的外键,例如:

type User struct {
  gorm.Model
  Languages []Language `gorm:"many2many:user_languages;"`
}

type Language struct {
  gorm.Model
  Name string
}

// 连接表:user_languages
//   foreign key: user_id, reference: users.id
//   foreign key: language_id, reference: languages.id

若要重写它们,可以使用标签 foreignKey、references、joinforeignKey、joinReferences。当然,您不需要使用全部的标签,你可以仅使用其中的一个重写部分的外键、引用。

type User struct {
    gorm.Model
    Profiles []Profile `gorm:"many2many:user_profiles;foreignKey:Refer;joinForeignKey:UserReferID;References:UserRefer;joinReferences:ProfileRefer"`
    Refer    uint      `gorm:"index:,unique"`
}

type Profile struct {
    gorm.Model
    Name      string
    UserRefer uint `gorm:"index:,unique"`
}

// 会创建连接表:user_profiles
//   foreign key: user_refer_id, reference: users.refer
//   foreign key: profile_refer, reference: profiles.user_refer

自引用 Many2Many

type User struct {
  gorm.Model
    Friends []*User `gorm:"many2many:user_friends"`
}

// 会创建连接表:user_friends
//   foreign key: user_id, reference: users.id
//   foreign key: friend_id, reference: users.id

自定义第三张表(连接表)

自定义连接表要求外键是复合主键或复合唯一索引

type Person struct {
  ID        int
  Name      string
  Addresses []Address `gorm:"many2many:person_addresses;"`
}

type Address struct {
  ID   uint
  Name string
}

type PersonAddress struct {
  PersonID  int `gorm:"primaryKey"`
  AddressID int `gorm:"primaryKey"`
  CreatedAt time.Time
  DeletedAt gorm.DeletedAt
}

func (PersonAddress) BeforeCreate(db *gorm.DB) error {
  // ...
}

// 修改 Person 的 Addresses 字段的连接表为 PersonAddress
// PersonAddress 必须定义好所需的外键,否则会报错
err := db.SetupJoinTable(&Person{}, "Addresses", &PersonAddress{})

外键约束

可以通过为标签 constraint 配置 OnUpdate、OnDelete 实现外键约束,

type User struct {
  gorm.Model
  Languages []Language `gorm:"many2many:user_speaks;"`
}

type Language struct {
  Code string `gorm:"primarykey"`
  Name string
}

// CREATE TABLE `user_speaks` (`user_id` integer,`language_code` text,PRIMARY KEY (`user_id`,`language_code`),CONSTRAINT `fk_user_speaks_user` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,CONSTRAINT `fk_user_speaks_language` FOREIGN KEY (`language_code`) REFERENCES `languages`(`code`) ON DELETE SET NULL ON UPDATE CASCADE);

复合外键

  1. 如果模型使用了 复合主键,GORM 会默认启用复合外键。
  2. 也可以覆盖默认的外键、指定多个外键,只需用逗号分隔那些键名,例如:
type Tag struct {
  ID     uint   `gorm:"primaryKey"`
  Locale string `gorm:"primaryKey"`
  Value  string
}

type Blog struct {
  ID         uint   `gorm:"primaryKey"`
  Locale     string `gorm:"primaryKey"`
  Subject    string
  Body       string
  Tags       []Tag `gorm:"many2many:blog_tags;"`
  // 将id,locale作为第三张表的外键
  LocaleTags []Tag `gorm:"many2many:locale_blog_tags;ForeignKey:id,locale;References:id"`
  SharedTags []Tag `gorm:"many2many:shared_blog_tags;ForeignKey:id;References:id"`
}

// 连接表:blog_tags
//   foreign key: blog_id, reference: blogs.id
//   foreign key: blog_locale, reference: blogs.locale
//   foreign key: tag_id, reference: tags.id
//   foreign key: tag_locale, reference: tags.locale

// 连接表:locale_blog_tags
//   foreign key: blog_id, reference: blogs.id
//   foreign key: blog_locale, reference: blogs.locale
//   foreign key: tag_id, reference: tags.id

// 连接表:shared_blog_tags
//   foreign key: blog_id, reference: blogs.id
//   foreign key: tag_id, reference: tags.id

CRUD

CUD创建更新删除(关联模式)

自动创建、更新

在创建、更新记录时,GORM 会通过 Upsert 自动保存关联及其引用记录。

user := User{
  Name:            "jinzhu",
  BillingAddress:  Address{Address1: "Billing Address - Address 1"},
  ShippingAddress: Address{Address1: "Shipping Address - Address 1"},
  Emails:          []Email{
    {Email: "jinzhu@example.com"},
    {Email: "jinzhu-2@example.com"},
  },
  Languages:       []Language{
    {Name: "ZH"},
    {Name: "EN"},
  },
}

db.Create(&user)
// BEGIN TRANSACTION;
// INSERT INTO "addresses" (address1) VALUES ("Billing Address - Address 1"), ("Shipping Address - Address 1") ON DUPLICATE KEY DO NOTHING;
// INSERT INTO "users" (name,billing_address_id,shipping_address_id) VALUES ("jinzhu", 1, 2);
// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu@example.com"), (111, "jinzhu-2@example.com") ON DUPLICATE KEY DO NOTHING;
// INSERT INTO "languages" ("name") VALUES ('ZH'), ('EN') ON DUPLICATE KEY DO NOTHING;
// INSERT INTO "user_languages" ("user_id","language_id") VALUES (111, 1), (111, 2) ON DUPLICATE KEY DO NOTHING;
// COMMIT;
db.Save(&user)

如果您想要更新关联的数据,您应该使用 FullSaveAssociations 模式

db.Session(&gorm.Session{FullSaveAssociations: true}).Updates(&user)
// ...
// INSERT INTO "addresses" (address1) VALUES ("Billing Address - Address 1"), ("Shipping Address - Address 1") ON DUPLICATE KEY SET address1=VALUES(address1);
// INSERT INTO "users" (name,billing_address_id,shipping_address_id) VALUES ("jinzhu", 1, 2);
// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu@example.com"), (111, "jinzhu-2@example.com") ON DUPLICATE KEY SET email=VALUES(email);
// ...
跳过自动创建、更新

若要在创建、更新时跳过自动保存,可以使用 Select 或 Omit

// User 拥有并属于多种 language,`user_languages` 是连接表
type User struct {
  gorm.Model
  Languages []Language `gorm:"many2many:user_languages;"`
}

type Language struct {
  gorm.Model
  Name string
}
user := User{
  Name:            "jinzhu",
  BillingAddress:  Address{Address1: "Billing Address - Address 1"},
  ShippingAddress: Address{Address1: "Shipping Address - Address 1"},
  Emails:          []Email{
    {Email: "jinzhu@example.com"},
    {Email: "jinzhu-2@example.com"},
  },
  Languages:       []Language{
    {Name: "ZH"},
    {Name: "EN"},
  },
}

db.Select("Name").Create(&user)
// INSERT INTO "users" (name) VALUES ("jinzhu", 1, 2);

db.Omit("BillingAddress").Create(&user)
// Skip create BillingAddress when creating a user

db.Omit(clause.Associations).Create(&user)
// Skip all associations when creating a user

对于 many2many 关联,GORM 在创建连接表引用之前,会先 upsert 关联。如果想跳过关联的 upsert,可以这样做:

db.Omit("Languages.*").Create(&user)

// 跳过创建关联及其引用
db.Omit("Languages").Create(&user)
Select/Omit 关联字段
user := User{
  Name:            "jinzhu",
  BillingAddress:  Address{Address1: "Billing Address - Address 1", Address2: "addr2"},
  ShippingAddress: Address{Address1: "Shipping Address - Address 1", Address2: "addr2"},
}

// 创建 user 及其 BillingAddress、ShippingAddress
// 在创建 BillingAddress 时,仅使用其 address1、address2 字段,忽略其它字段
db.Select("BillingAddress.Address1", "BillingAddress.Address2").Create(&user)

db.Omit("BillingAddress.Address2", "BillingAddress.CreatedAt").Create(&user)
关联模式

关联模式包含一些在处理关系时有用的方法

type User2 struct {
	gorm.Model
	Name      string
	ManagerID *uint
	// Team不会出现在表中
	// 一对多自关联
	// 如果不为空,则会将ID放入Team中User的ManagerID
	Team []User2 `gorm:"foreignkey:ManagerID"`
}
// 开始关联模式
var user User
db.Model(&user).Association("Languages")
// `user` 是源模型,它的主键不能为空
// 关系的字段名是 `Languages`
// 如果匹配了上面两个要求,会开始关联模式,否则会返回错误
db.Model(&user).Association("Languages").Error
查找关联
// 查找所有匹配的关联记录
db.Model(&user).Association("Languages").Find(&languages)
// 查找带条件的关联
codes := []string{"zh-CN", "en-US", "ja-JP"}
db.Model(&user).Where("code IN ?", codes).Association("Languages").Find(&languages)
db.Model(&user).Where("code IN ?", codes).Order("code desc").Association("Languages").Find(&languages)
添加关联

// 为 many to many、has many 添加新的关联;为 has one, belongs to 替换当前的关联

db.Model(&user).Association("Languages").Append([]Language{languageZH, languageEN})
db.Model(&user).Association("Languages").Append(&Language{Name: "DE"})
db.Model(&user).Association("CreditCard").Append(&CreditCard{Number: "411111111111"})
替换关联

用一个新的关联替换当前的关联

db.Model(&user).Association("Languages").Replace([]Language{languageZH, languageEN})
db.Model(&user).Association("Languages").Replace(Language{Name: "DE"}, languageEN)
删除关联

如果存在,则删除源模型与参数之间的关系,只会删除引用,不会从数据库中删除这些对象。

db.Model(&user).Association("Languages").Delete([]Language{languageZH, languageEN})
db.Model(&user).Association("Languages").Delete(languageZH, languageEN)
清空关联

删除源模型与关联之间的所有引用,但不会删除这些关联

db.Model(&user).Association("Languages").Clear()
关联计数

返回当前关联的计数

db.Model(&user).Association("Languages").Count()
// 条件计数
codes := []string{"zh-CN", "en-US", "ja-JP"}
db.Model(&user).Where("code IN ?", codes).Association("Languages").Count()
批量处理数据

关联模式也支持批量处理,例如

// 查询所有用户的所有角色
db.Model(&users).Association("Role").Find(&roles)

// 从所有 team 中删除 user A
db.Model(&users).Association("Team").Delete(&userA)

// 获取去重的用户所属 team 数量
db.Model(&users).Association("Team").Count()

// 对于批量数据的 `Append`、`Replace`,参数的长度必须与数据的长度相同,否则会返回 error
var users = []User{user1, user2, user3}
// 例如:现在有三个 user,Append userA 到 user1 的 team,Append userB 到 user2 的 team,Append userA、userB 和 userC 到 user3 的 team
db.Model(&users).Association("Team").Append(&userA, &userB, &[]User{userA, userB, userC})
// 重置 user1 team 为 userA,重置 user2 的 team 为 userB,重置 user3 的 team 为 userA、 userB 和 userC
db.Model(&users).Association("Team").Replace(&userA, &userB, &[]User{userA, userB, userC})
带 Select 的删除

可以在删除记录时通过 Select 来删除具有 has one、has many、many2many 关系的记录

// 删除 user 时,也删除 user 的 account
db.Select("Account").Delete(&user)
// 删除 user 时,也删除 user 的 Orders、CreditCards 记录
db.Select("Orders", "CreditCards").Delete(&user)
// 删除 user 时,也删除用户所有 has one/many、many2many 记录
db.Select(clause.Associations).Delete(&user)
// 删除 users 时,也删除每一个 user 的 account
db.Select("Account").Delete(&users)

只有当记录的主键不为空时,关联才会被删除,GORM 会使用这些主键作为条件来删除关联记录

// DOESN'T WORK
db.Select("Account").Where("name = ?", "jinzhu").Delete(&User{})
// 会删除所有 name=`jinzhu` 的 user,但这些 user 的 account 不会被删除
db.Select("Account").Where("name = ?", "jinzhu").Delete(&User{ID: 1})
// 会删除 name = `jinzhu` 且 id = `1` 的 user,并且 user `1` 的 account 也会被删除
db.Select("Account").Delete(&User{ID: 1})
// 会删除 id = `1` 的 user,并且 user `1` 的 account 也会被删除
关联标签
标签描述
foreignKey指定当前模型的列作为连接表的外键
references指定引用表的列名,其将被映射为连接表外键
polymorphic指定多态类型,比如模型名
polymorphicValue指定多态值、默认表名
many2many指定连接表表名
joinForeignKey指定连接表的外键列名,其将被映射到当前表
joinReferences指定连接表的外键列名,其将被映射到引用表
constraint关系约束,例如:OnUpdateOnDelete
举例
  1. 模型
type User2 struct {
	gorm.Model
	Name      string
	ManagerID *uint
	// Team不会出现在表中
	// 一对多自关联
	// 如果不为空,则会将ID放入Team中User的ManagerID
	Team []User2 `gorm:"foreignkey:ManagerID"`
}
  1. 新建5个用户
func main() {
	handler.AutoMigrate(new(User2))
	us := make([]User2, 0)
	for i := 0; i < 5; i++ {
		us = append(us, User2{
			Name: fmt.Sprintf("u_%d", i),
		})
	}

	handler.CreateInBatches(us, 10)
}
  1. 使用关联模式添加Team
func main() {
	users := make([]User2, 0)
	handler.Where("id > ?", 3).Find(&users)
	// 将users设置为id=1的user的团队成员
	handler.Model(new(User2)).Where("id = ?", 1).Association("Team").Append(users)
}

预加载(查询)

GORM允许使用 Preload通过多个SQL中来直接加载关系

type User struct {
  gorm.Model
  Username string
  Orders   []Order
}

type Order struct {
  gorm.Model
  UserID uint
  Price  float64
}

// 查找 user 时预加载相关 Order
db.Preload("Orders").Find(&users)
// SELECT * FROM users;
// SELECT * FROM orders WHERE user_id IN (1,2,3,4);

db.Preload("Orders").Preload("Profile").Preload("Role").Find(&users)
// SELECT * FROM users;
// SELECT * FROM orders WHERE user_id IN (1,2,3,4); // has many
// SELECT * FROM profiles WHERE user_id IN (1,2,3,4); // has one
// SELECT * FROM roles WHERE id IN (4,5,6); // belongs to

Joins 预加载

Preload 在一个单独查询中加载关联数据。而 Join Preload 会使用 left join 加载关联数据,例如

db.Joins("Company").Joins("Manager").Joins("Account").First(&user, 1)
db.Joins("Company").Joins("Manager").Joins("Account").First(&user, "users.name = ?", "jinzhu")
db.Joins("Company").Joins("Manager").Joins("Account").Find(&users, "users.id IN ?", []int{1,2,3,4,5})

带条件的 Join

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;

Join Preload 适用于一对一的关系,例如: has one, belongs to

预加载全部

与创建、更新时使用 Select 类似,clause.Associations 也可以和 Preload 一起使用,它可以用来 预加载 全部关联

type User struct {
  gorm.Model
  Name       string
  CompanyID  uint
  Company    Company
  Role       Role
  Orders     []Order
}

// clause.Associations不会预加载嵌套的关联关系
db.Preload(clause.Associations).Find(&users)

// 但是可以将其与嵌套预加载一起使用
db.Preload("Orders.OrderItems.Product").Preload(clause.Associations).Find(&users)

条件预加载

GORM 允许带条件的 Preload 关联,类似于内联条件

// 带条件的预加载 Order
db.Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users)
// SELECT * FROM users;
// SELECT * FROM orders WHERE user_id IN (1,2,3,4) AND state NOT IN ('cancelled');

db.Where("state = ?", "active").Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users)
// SELECT * FROM users WHERE state = 'active';
// SELECT * FROM orders WHERE user_id IN (1,2) AND state NOT IN ('cancelled');
自定义预加载 SQL

您可以通过 func(db *gorm.DB) *gorm.DB 实现自定义预加载 SQL,例如:

db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
  return db.Order("orders.amount DESC")
}).Find(&users)
// SELECT * FROM users;
// SELECT * FROM orders WHERE user_id IN (1,2,3,4) order by orders.amount DESC;
嵌套预加载

GORM 支持嵌套预加载

db.Preload("Orders.OrderItems.Product").Preload("CreditCard").Find(&users)
// 自定义预加载 `Orders` 的条件
// 这样,GORM 就不会加载不匹配的 order 记录
db.Preload("Orders", "state = ?", "paid").Preload("Orders.OrderItems").Find(&users)

索引

GORM 允许通过 index、uniqueIndex 标签创建索引,这些索引将在使用 GORM 进行AutoMigrate 或 Createtable 时创建

索引标签

GORM 可以接受很多索引设置,例如class、type、where、comment、expression、sort、collate、option

type User struct {
    Name  string `gorm:"index"`
    Name2 string `gorm:"index:idx_name,unique"`
    Name3 string `gorm:"index:,sort:desc,collate:utf8,type:btree,length:10,where:name3 != 'jinzhu'"`
    Name4 string `gorm:"uniqueIndex"`
    Age   int64  `gorm:"index:,class:FULLTEXT,comment:hello \\, world,where:age > 10"`
    Age2  int64  `gorm:"index:,expression:ABS(age)"`
}

// MySQL 选项
type User struct {
    Name string `gorm:"index:,class:FULLTEXT,option:WITH PARSER ngram INVISIBLE"`
}

// PostgreSQL 选项
type User struct {
    Name string `gorm:"index:,option:CONCURRENTLY"`
}

唯一索引

uniqueIndex 标签的作用与 index 类似,它等效于 index:,unique

type User struct {
    Name1 string `gorm:"uniqueIndex"`
    Name2 string `gorm:"uniqueIndex:idx_name,sort:desc"`
}

复合索引

两个字段使用同一个索引名将创建复合索引

// create composite index `idx_member` with columns `name`, `number`
type User struct {
    Name   string `gorm:"index:idx_member"`
    Number string `gorm:"index:idx_member"`
}

字段优先级

  1. 复合索引列的顺序会影响其性能,因此必须仔细考虑

  2. 可以使用 priority 指定顺序,默认优先级值是 10,如果优先级值相同,则顺序取决于模型结构体字段的顺序

type User struct {
    Name   string `gorm:"index:idx_member"`
    Number string `gorm:"index:idx_member"`
}
// column order: name, number

type User struct {
    Name   string `gorm:"index:idx_member,priority:2"`
    Number string `gorm:"index:idx_member,priority:1"`
}
// column order: number, name

type User struct {
    Name   string `gorm:"index:idx_member,priority:12"`
    Number string `gorm:"index:idx_member"`
}
// column order: number, name

多索引

一个字段接受多个 index、uniqueIndex 标签,这会在一个字段上创建多个索引

type UserIndex struct {
    OID          int64  `gorm:"index:idx_id;index:idx_oid,unique"`
    MemberNumber string `gorm:"index:idx_id"`
}

复合主键

type Product struct {
  ID           string `gorm:"primaryKey"`
  LanguageCode string `gorm:"primaryKey"`
  Code         string
  Name         string
}

默认情况下,整型 PrioritizedPrimaryField 启用了 AutoIncrement(自增),要禁用它,需要为整型字段关闭 autoIncrement。

type Product struct {
  CategoryID uint64 `gorm:"primaryKey;autoIncrement:false"`
  TypeID     uint64 `gorm:"primaryKey;autoIncrement:false"`
}

共享复合索引

如果您正在创建带有嵌入结构的共享复合索引,您不能指定索引名称。 作为嵌入结构不止一次会导致重复的索引名称在 db。

在这种情况下,您可以使用索引标签 composite, 它意味着复合索引的id。 所有具有相同结构复合id的字段都与原始规则一样,被合并到相同的索引中。 但改进使得最受影响/嵌入结构能够通过命名策略生成索引名称。 例如:

type Foo struct {
  IndexA int `gorm:"index:,unique,composite:myname"`
  IndexB int `gorm:"index:,unique,composite:myname"`
}

如果表Foo被创建,复合索引的名称将是 idx_foo_myname。

type Bar0 struct {
  Foo
}

type Bar1 struct {
  Foo
}

复合索引的名称分别是 idx_bar0_myname 和 idx_bar1_myname。

复合 只能在指定索引名称时使用。

安全

GORM 使用 database/sql 的参数占位符来构造 SQL 语句,这可以自动转义参数,避免 SQL 注入数据

查询条件

用户的输入只能作为参数,例如:

userInput := "jinzhu;drop table users;"

// 安全的,会被转义
db.Where("name = ?", userInput).First(&user)

// SQL 注入
db.Where(fmt.Sprintf("name = %v", userInput)).First(&user)

内联条件

// 会被转义
db.First(&user, "name = ?", userInput)

// SQL 注入
db.First(&user, fmt.Sprintf("name = %v", userInput))

当通过用户输入的整形主键检索记录时,你应该对变量进行类型检查。

userInputID := "1=1;drop table users;"
// 安全的,返回 err
id,err := strconv.Atoi(userInputID)
if err != nil {
    return error
}
db.First(&user, id)

// SQL 注入
db.First(&user, userInputID)
// SELECT * FROM users WHERE 1=1;drop table users;

SQL 注入方法

为了支持某些功能,一些输入不会被转义,调用方法时要小心用户输入的参数。

db.Select("name; drop table users;").First(&user)
db.Distinct("name; drop table users;").First(&user)

db.Model(&user).Pluck("name; drop table users;", &names)

db.Group("name; drop table users;").First(&user)

db.Group("name").Having("1 = 1;drop table users;").First(&user)

db.Raw("select name from users; drop table users;").First(&user)

db.Exec("select name from users; drop table users;")

db.Order("name; drop table users;").First(&user)

避免 SQL 注入的一般原则是,不信任用户提交的数据。您可以进行白名单验证来测试用户的输入是否为已知安全的、已批准、已定义的输入,并且在使用用户的输入时,仅将它们作为参数。

小tips

Model ()传值

model()方法中传入一个空模型和有ID的模型时是不同的,gorm会根据传入的Model操作接下来的sql,所以传入一个空模型可能会导致数据库中出现id = 0的情况。

// 相当于一个id=0的模型对象
model(new(User))
// 相当于id=1的模型对象
model(&User{ID:1})

models定义的位置

建议将model都放到一个文件夹中,否则极易出现循环导入。
在这里插入图片描述

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
gorm scopes是Golang中使用的一种功能,用定义和应用查询作用域。它可以帮助我们在查询数据库时,根据不同的条件和需求,动态地构建查询语句。 使用gorm scopes,我们可以将一组查询条件封装成一个作用域(scope),然后在需要的时候应用到查询中。这样可以使代码更加模块化和可复用,同时也能提高查询的灵活性和可读性。 在gorm中,我们可以通过定义结构体的方法来创建作用域。这些方法需要接收一个gorm.DB类型的参数,并返回一个gorm.DB类型的结果。在方法内部,我们可以使用gorm提供的各种查询方法来构建查询条件,例如Where、Order、Limit等。 下面是一个使用gorm scopes的示例: ```go type User struct { ID uint Name string Age int } func (db *gorm.DB) AgeGreaterThan(age int) *gorm.DB { return db.Where("age > ?", age) } func main() { db, err := gorm.Open("mysql", "user:password@tcp(localhost:3306)/database") if err != nil { panic(err) } defer db.Close() var users []User db.Scopes(db.AgeGreaterThan(18)).Find(&users) } ``` 在上面的示例中,我们定义了一个名为AgeGreaterThan的作用域,它接收一个年龄参数,并返回一个添加了查询条件的gorm.DB对象。然后,在main函数中,我们通过调用Scopes方法并传入AgeGreaterThan作用域,来应用该作用域到查询中。 这样,最终执行的查询语句将会是`SELECT * FROM users WHERE age > 18`,并将结果存储到users变量中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Generalzy

文章对您有帮助,倍感荣幸

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值