gorm深入浅出(上)

796 阅读24分钟

gorm深入浅出

官网:GORM 指南 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

ORM简介

对象关系映射(Object Relational Mapping,简称ORM)模式是一种为了解决面向对象与关系数据库(如mysql数据库)存在的互不匹配的现象的技术。简单的说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系数据库中。

安装

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

快速入门

gormModel 结构体

type Model struct {
	ID        uint `gorm:"primarykey"` //主键
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt DeletedAt `gorm:"index"` // 索引
}
package main

import (
	"fmt"

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

type Product struct {
	// 继承gorm的Model结构体
	gorm.Model
	Code  string
	Price uint
}

// 创建表
func create(db *gorm.DB) {
	db.AutoMigrate(&Product{})
}

// 插入数据
func insert(db *gorm.DB) {
	p := Product{
		Code:  "1001",
		Price: 100,
	}
	db.Create(&p)
}

// 查询
func find(db *gorm.DB, id int) {
	var p Product
	db.First(&p, id)
	// 查询条件
	db.First(&p, "code=?", "1001")
	fmt.Printf("p: %v\n", p)

}

// 更新
func update(db *gorm.DB, id int) {
	var p Product
	db.First(&p, id)
	// db.Model(&p).Update("Price", 200)

	// db.Model(&p).Updates(Product{Price: 1001, Code: "1002"}) // 仅更新非零字段
	db.Model(&p).Updates(map[string]interface{}{"Price": 1003, "Code": "1003"})
}

// 删除
func del(db *gorm.DB, id int) {
	var p Product
	db.First(&p, id)
	// 逻辑删除
	db.Delete(&p, id)
}

func main() {
	dsn := "root:admin123@tcp(localhost:3306)/go_db?charset=utf8mb4&parseTime=true"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}

	// create(db)
	// insert(db)
	// find(db, 1)
	// update(db, 1)
	del(db, 1)
}

声明模型

模型定义

模型是标准的 struct,由 Go 的基本数据类型、实现了 Scanner 和 Valuer 接口的自定义类型及其指针或别名组成

例如上面的:

type Product struct {
	// 继承gorm的Model结构体
	gorm.Model
	Code  string
	Price uint
}

约定

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

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

高级选项

字段级权限控制

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

注意: 使用 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 读写会忽略该字段
}

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

GORM 约定使用 CreatedAtUpdatedAt 追踪创建/更新时间。如果您定义了这种字段,GORM 在创建、更新时会自动填充 当前时间

要使用不同名称的字段,您可以配置 autoCreateTimeautoUpdateTime 标签

如果您想要保存 UNIX(毫/纳)秒时间戳,而不是 time,您只需简单地将 time.Time 修改为 int 即可

type User struct {
 CreatedAt time.Time // Set to current time if it is zero on creating
 UpdatedAt int       // Set to current unix seconds on updating or if it is zero on creating
 Updated   int64 `gorm:"autoUpdateTime:nano"` // Use unix nano seconds as updating time
 Updated   int64 `gorm:"autoUpdateTime:milli"`// Use unix milli seconds as updating time
 Created   int64 `gorm:"autoCreateTime"`      // Use unix seconds as creating time
}

嵌入结构体

对于匿名字段,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
}

对于正常的结构体字段,你也可以通过标签 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
}

并且,您可以使用标签 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
}

字段标签

声明 model 时,tag 是可选的,GORM 支持以下 tag:tag 名大小写不敏感,但建议使用 camelCase 风格

标签名说明
column指定 db 列名
type列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes 并且可以和其他标签一起使用,例如:not nullsizeautoIncrement… 像 varbinary(8) 这样指定数据库数据类型也是支持的。在使用指定数据库数据类型时,它需要是完整的数据库数据类型,如:MEDIUMINT UNSIGNED not NULL AUTO_INCREMENT
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根据参数创建索引,多个字段使用相同的名称则创建复合索引,查看 索引 获取详情
uniqueIndex与 index 相同,但创建的是唯一索引
check创建检查约束,例如 check:age > 13,查看 约束 获取详情
<-设置字段写入的权限, <-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限
->设置字段读的权限,->:false 无读权限
-忽略该字段,- 无读写权限
comment迁移时为字段添加注释

关联标签

GORM 允许通过标签为关联配置外键、约束、many2many 表,详情请参考 关联部分

gorm连接到数据库

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

MySQL

package main

import (
	"fmt"
	"log"

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

func main() {
	// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
	dsn := "root:admin123@tcp(127.0.0.1:3306)/go_db?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("db: %v\n", db)
}

注意: 想要正确的处理 time.Time ,您需要带上 parseTime 参数, (更多参数) 要支持完整的 UTF-8 编码,您需要将 charset=utf8 更改为 charset=utf8mb4

MySQl 驱动程序提供了 一些高级配置 可以在初始化过程中使用,例如:

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

自定义驱动

GORM 允许通过 DriverName 选项自定义 MySQL 驱动,例如:

import (
 _ "example.com/my_mysql_driver"
 "gorm.io/gorm"
)

db, err := gorm.Open(mysql.New(mysql.Config{
 DriverName: "my_mysql_driver",
 DSN: "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local"// Data Source Name,参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name
}), &gorm.Config{})

现有的数据库连接

GORM 允许通过一个现有的数据库连接来初始化 *gorm.DB

import (
 "database/sql"
 "gorm.io/gorm"
)

sqlDB, err := sql.Open("mysql""mydb_dsn")
gormDB, err := gorm.Open(mysql.New(mysql.Config{
 Conn: sqlDB,
}), &gorm.Config{})

PostgreSQL

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

dsn := "host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})

我们使用 pgx 作为 postgres 的 database/sql 驱动,默认情况下,它会启用 prepared statement 缓存,你可以这样禁用它:

// https://github.com/go-gorm/postgres
db, err := gorm.Open(postgres.New(postgres.Config{
 DSN: "user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai",
 PreferSimpleProtocol: true// disables implicit prepared statement usage
}), &gorm.Config{})

自定义驱动

GORM 允许通过 DriverName 选项自定义 PostgreSQL 驱动,例如:

import (
 _ "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/dialers/postgres"
 "gorm.io/gorm"
)

db, err := gorm.Open(postgres.New(postgres.Config{
 DriverName: "cloudsqlpostgres",
 DSN: "host=project:region:instance user=postgres dbname=postgres password=password sslmode=disable",
})

现有的数据库连接

GORM 允许通过一个现有的数据库连接来初始化 *gorm.DB

import (
 "database/sql"
 "gorm.io/gorm"
)

sqlDB, err := sql.Open("postgres""mydb_dsn")
gormDB, err := gorm.Open(postgres.New(postgres.Config{
 Conn: sqlDB,
}), &gorm.Config{})

SQLite

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

// github.com/mattn/go-sqlite3
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})

注意: 您也可以使用 file::memory:?cache=shared 替代文件路径。这会告诉 SQLite 在系统内存中使用一个临时数据库。(查看 SQLite 文档 获取详情)

SQL Server

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

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

Clickhouse

github.com/go-gorm/cli…

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

func main() {
 dsn := "tcp://localhost:9000?database=gorm&username=gorm&password=gorm&read_timeout=10&write_timeout=20"
 db, err := gorm.Open(clickhouse.Open(dsn), &gorm.Config{})

 // Auto Migrate
 db.AutoMigrate(&User{})
 // Set table options
 db.Set("gorm:table_options""ENGINE=Distributed(cluster, default, hits)").AutoMigrate(&User{})

 // 插入
 db.Create(&user)

 // 查询
 db.Find(&user, "id = ?"10)

 // 批量插入
 var users = []User{user1, user2, user3}
 db.Create(&users)
 // ...
}

连接池

GORM 使用 database/sql 维护连接池

sqlDB, err := db.DB()

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

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

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

查看 通用接口 获取详情。

gorm创建记录

实例演示

package main

import (
	"fmt"
	"time"

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

var db *gorm.DB

func init() {
	dsn := "root:admin123@tcp(localhost:3306)/go_db?charset=utf8mb4&parseTime=true"
	d, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}
	db = d
}

// 开头要大写(公开) 不然创建不了对应的字段
type User struct {
	gorm.Model
	Name     string
	Age      int
	Birthday time.Time
}

func createTable() {
	db.AutoMigrate(&User{})
}

var user = User{
	Name:     "tom",
	Age:      20,
	Birthday: time.Now(),
}

// 插入一个
func insert1() {
	result := db.Create(&user)
	fmt.Printf("result.RowsAffected: %v\n", result.RowsAffected) // 影响几行 1
	fmt.Printf("user.ID: %v\n", user.ID)                         // user.ID: 1
}

//  选择添加
func insert2() {
	// 只添加Name Age CreatedAt字段 不添加Birthday
	db.Select("Name", "Age", "CreatedAt").Create(&user)
}

// 批量插入
func insert3() {
	var users = []User{{Name: "tom1"}, {Name: "tom2"}, {Name: "tom3"}}
	db.Create(&users)
}

func main() {
	// createTable()
	// insert1()
	// insert2()
	insert3()
}

使用 CreateInBatches 分批创建时,你可以指定每批的数量,例如:

var users = []User{{name: "jinzhu_1"}, ...., {Name: "jinzhu_10000"}}

// 数量为 100
db.CreateInBatches(users, 100)

Upsert 和 Create With Associations 也支持批量插入

注意 使用CreateBatchSize 选项初始化 GORM 时,所有的创建& 关联 INSERT 都将遵循该选项

db, err := gorm.Open(sqlite.Open("gorm.db"), &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)

创建钩子(回调)

GORM 允许用户定义的钩子有 BeforeSave, BeforeCreate, AfterSave, AfterCreate 创建记录时将调用这些钩子方法,请参考 Hooks 中关于生命周期的详细信息

例子

...

// 批量插入
func insert3() {
	var users = []User{{Name: "tom1"}, {Name: "tom2"}, {Name: "tom3"}}
	db.Create(&users)
}

func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
	fmt.Println("BeforeCreate")
	return
}

func main() {
        ...
	insert3()
}

/*
BeforeCreate
BeforeCreate
BeforeCreate
*/

如果您想跳过 钩子 方法,您可以使用 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 创建

GORM 支持根据 map[string]interface{}[]map[string]interface{}{} 创建记录,例如:

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

注意: 根据 map 创建记录时,association、回调钩子 不会被调用,且主键也不会自动填充

使用 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
}

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

您也可以通过 SelectOmit 跳过关联保存,例如:

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

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

默认值

您可以通过标签 default 为字段定义默认值,如:

type User struct {
 ID   int64
 Name string `gorm:"default:galeone"`
 Age  int64  `gorm:"default:18"`
}

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

注意0''false 等零值,不会将这些字段定义的默认值保存到数据库。您需要使用指针类型或 Scanner/Valuer 来避免这个问题,例如:

type User struct {
 gorm.Model
 Name string
 Age  *int           `gorm:"default:18"`
 Active sql.NullBool `gorm:"default:true"`
}

注意 若要数据库有默认、虚拟/生成的值,你必须为字段设置 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 支持

Upsertid 更新,无创建

import "gorm.io/gorm/clause"

// 在冲突时,什么都不做
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)

// 在`id`冲突时,将列更新为默认值
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

// 使用SQL语句
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));

// 在`id`冲突时,将列更新为新值
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

// 在冲突时,更新除主键以外的所有列到新值。
db.Clauses(clause.OnConflict{
 UpdateAll: true,
}).Create(&users)
// INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age", ...;

您还可以查看 高级查询 中的 FirstOrInitFirstOrCreate

gorm查询记录

检索单个对象

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

...

var db *gorm.DB

func init() { ... }

// 开头要大写(公开) 不然创建不了对应的字段
type User struct {
	gorm.Model
	Name     string
	Age      int
	Birthday time.Time
}

func select1() {
	var user User

	// 获取一天记录(主键升序)
	// result := db.First(&user)
	// result.RowsAffected // 返回找到的记录数
	// result.Error        // returns error or nil
	// fmt.Printf("user.ID: %v\n", user.ID) // user.ID: 1

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

	// 获取一条记录 没有指定排序字段
	// db.Take(&user)
	// fmt.Printf("user.ID: %v\n", user.ID) // user.ID: 1

	// 获取最后一条记录(主键降序)
	db.Last(&user)
	fmt.Printf("user.ID: %v\n", user.ID) // user.ID: 10
}

func main() {
	select1()
}

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

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

var user User
var users []User  

// 有效,因为目标 struct 是指针
db.First(&user)
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1

// 有效,因为 map 需要通过 `db.Model()` 指定 model
result := map[string]interface{}{}
db.Model(&User{}).First(&result)
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1

// 无效 map没有指定model
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

用主键检索

如果主键是数字类型,您可以使用 内联条件 来检索对象。传入字符串参数时,需要特别注意 SQL 注入问题,查看 安全 获取详情.

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

检索全部对象

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

result.RowsAffected // 返回找到的记录数,相当于 `len(users)`
result.Error        // returns error

条件

String 条件

// 获取第一条匹配的记录
db.Where("name = ?""jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;

// 获取全部匹配的记录
db.Where("name <> ?""jinzhu").Find(&users)
// SELECT * FROM users WHERE name <> 'jinzhu';

// IN
db.Where("name IN ?", []string{"jinzhu""jinzhu 2"}).Find(&users)
// SELECT * FROM users WHERE name IN ('jinzhu','jinzhu 2');

// LIKE
db.Where("name LIKE ?""%jin%").Find(&users)
// SELECT * FROM users WHERE name LIKE '%jin%';

// AND
db.Where("name = ? AND age >= ?""jinzhu""22").Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;

// Time
db.Where("updated_at > ?", lastWeek).Find(&users)
// SELECT * FROM users WHERE updated_at > '2000-01-01 00:00:00';

// BETWEEN
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
// SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00';

Struct & Map 条件

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

// Map
db.Where(map[string]interface{}{"name""jinzhu""age"20}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;

// 主键切片条件
db.Where([]int64{202122}).Find(&users)
// SELECT * FROM users WHERE id IN (20, 21, 22);

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

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

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

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

指定结构体查询字段

当使用 struct 进行查询时,你可以通过向 Where() 传入 struct 来指定查询条件的字段、值、表名,例如:

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;

内联条件

查询条件也可以被内联到 FirstFind 之类的方法中,其用法类似于 Where

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

// Plain SQL 原生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;

// 不在主键切片中的记录
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);

更复杂的 SQL 查询, 请查看 高级查询中的组条件。

选择特定字段

Select 允许您指定从数据库中检索哪些字段, 默认情况下,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;

还可以看一看 智能选择字段

Order

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

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

// 多个 order
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{123}}, WithoutParentheses: true},
}).Find(&User{})
// SELECT * FROM users ORDER BY FIELD(id,1,2,3)

Limit & Offset

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

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

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

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

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

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

查看 Pagination 学习如何写一个分页器

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

Distinct 也可以配合 Pluck, Count 使用

Joins(左连接、右连接等)

指定 Joins 条件

type result struct {
 Name  string
 Email string
}

db.Model(&User{}).Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&result{})
// SELECT users.name, emails.email FROM `users` left join emails on emails.user_id = users.id

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

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

// 带参数的多表连接
db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?""jinzhu@example.org").Joins("JOIN credit_cards ON credit_cards.user_id = users.id").Where("credit_cards.number = ?""411111111111").Find(&user)

Joins 预加载

您可以使用 Joins 实现单条 SQL 预加载关联记录,例如:

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

Join with conditions

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;

For more details, please refer to Preloading (Eager Loading).

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更新

保存所有字段

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

db.First(&user)

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

更新单个列

当使用 Update 更新单个列时,你需要指定条件,否则会返回 ErrMissingWhereClause 错误,查看 Block Global Updates 获取详情。当使用了 Model 方法,且该对象主键有值,该值会被用于构建条件,例如:

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

更新多列

Updates 方法支持 structmap[string]interface{} 参数。当使用 struct 更新时,默认情况下,GORM 只会更新非零值的字段

// 根据 `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;

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

更新选定字段

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

// 使用 Map 进行 Select
// User's ID is `111`:
db.Model(&user).Select("name").Updates(map[string]interface{}{"name""hello""age"18"active"false})
// UPDATE users SET name='hello' WHERE id=111;

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

// 使用 Struct 进行 Select(会 select 零值的字段)
db.Model(&user).Select("Name""Age").Updates(User{Name: "new_name", Age: 0})
// UPDATE users SET name='new_name', age=0 WHERE id=111;

// Select 所有字段(查询包括零值字段的所有字段)
db.Model(&user).Select("*").Update(User{Name: "jinzhu", Role: "admin", Age: 0})

// Select 除 Role 外的所有字段(包括零值字段的所有字段)
db.Model(&user).Select("*").Omit("Role").Update(User{Name: "jinzhu", Role: "admin", Age: 0})

更新 Hook

对于更新操作,GORM 支持 BeforeSaveBeforeUpdateAfterSaveAfterUpdate 钩子,这些方法将在更新记录时被调用,详情请参阅 钩子

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

批量更新

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

// 根据 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{1011}).Updates(map[string]interface{}{"name""hello""age"18})
// UPDATE users SET name='hello', age=18 WHERE id IN (10, 11);

阻止全局更新

如果在没有任何条件的情况下执行批量更新,默认情况下,GORM 不会执行该操作,并返回 ErrMissingWhereClause 错误

对此,你必须加一些条件,或者使用原生 SQL,或者启用 AllowGlobalUpdate 模式,例如:

// 必须要有 .Where
db.Model(&User{}).Update("name""jinzhu").Error // gorm.ErrMissingWhereClause

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

db.Exec("UPDATE users SET name = ?""jinzhu")
// UPDATE users SET name = "jinzhu"

// 配置允许全局更新 这样不需要 .Where 也可以更新
db.Session(&gorm.Session{AllowGlobalUpdate: true}).Model(&User{}).Update("name""jinzhu")
// UPDATE users SET `name` = "jinzhu"

更新的记录数

获取受更新影响的行数

// 通过 `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 表达式更新

GORM 允许使用 SQL 表达式更新列,例如:

// product 的 ID 是 `3`
db.Model(&product).Update("price", gorm.Expr("price * ? + ?"2100))
// 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 * ? + ?"2100)})
// 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;

并且 GORM 也允许使用 SQL 表达式、自定义数据类型的 Context Valuer 来更新,例如:

// 根据自定义数据类型创建
type Location struct {
   X, Y int
}

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

db.Model(&User{ID: 1}).Updates(User{
 Name:  "jinzhu",
 Location: Location{X: 100, Y: 100},
})
// UPDATE `user_with_points` SET `name`="jinzhu",`location`=ST_PointFromText("POINT(100 100)") WHERE `id` = 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 和时间追踪

如果您想在更新时跳过 Hook 方法且不追踪更新时间,可以使用 UpdateColumnUpdateColumns,其用法类似于 UpdateUpdates

// 更新单个列
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 Data From Modified Rows

返回的结果放到切片里面

// return all columns
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}}

// return specified columns
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}}

Check Field has changed?

GORM provides Changed method could be used in Before Update Hooks, it will return the field changed or not

The Changed method only works with methods Update, Updates, and it only checks if the updating value from Update / Updates equals the model value, will return true if it is changed and not omitted

func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
 // if Role changed
   if tx.Statement.Changed("Role") {
   return errors.New("role not allowed to change")
  }

 if tx.Statement.Changed("Name""Admin") { // if Name or Role changed
   tx.Statement.SetColumn("Age"18)
}

 // if any fields changed
   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` not changed
db.Model(&User{ID: 1, Name: "jinzhu"}).Select("Admin").Updates(map[string]interface{
 "name""jinzhu2""admin"false,
})
// Changed("Name") => false, `Name` not selected to update

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` not changed
db.Model(&User{ID: 1, Name: "jinzhu"}).Select("Admin").Updates(User{Name: "jinzhu2"})
// Changed("Name") => false, `Name` not selected to update

Change Updating Values

To change updating values in Before Hooks, you should use SetColumn unless it is a full updates with Save, for example:

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

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

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

gorm删除

删除一条记录

删除一条记录时,删除对象需要指定主键,否则会触发 批量 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";

根据主键删除

GORM 允许通过主键(可以是复合主键)和内联条件来删除对象,它可以使用数字(如以下例子。也可以使用字符串——译者注)。查看 查询-内联条件(Query Inline Conditions) 了解详情。

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 支持 BeforeDeleteAfterDelete Hook,在删除记录时会调用这些方法,查看 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 模式,例如:

// gorm.ErrMissingWhereClause 全局删除需要添加 .Where条件 触发如下配置 Session
db.Delete(&User{}).Error 

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

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

// 配置后不需要 .Where 也可以全局删除
db.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&User{})
// DELETE FROM users

从删除行返回数据

把删除的数据赋值给 users:

// return all columns
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}}

// return specified columns
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}}

软删除

If your model includes a gorm.DeletedAt field (which is included in gorm.Model), it will get soft delete ability automatically!

When calling Delete, the record WON’T be removed from the database, but GORM will set the DeletedAt‘s value to the current time, and the data is not findable with normal Query methods anymore.

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

If you don’t want to include gorm.Model, you can enable the soft delete feature like:

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

查找软删除记录

You can find soft deleted records with Unscoped

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

Delete permanently

You can delete matched records permanently with Unscoped

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

删除标记

Use unix second as delete flag

import "gorm.io/plugin/soft_delete"

type User struct {
 ID        uint
 Name      string
 DeletedAt soft_delete.DeletedAt
}

// Query
SELECT * FROM users WHERE deleted_at = 0;

// Delete
UPDATE users SET deleted_at = /* current unix second */ WHERE ID = 1;

INFO when using unique field with soft delete, you should create a composite index with the unix second based DeletedAt field, e.g:

import "gorm.io/plugin/soft_delete"

type User struct {
ID        uint
Name      string                `gorm:"uniqueIndex:udx_name"`
DeletedAt soft_delete.DeletedAt `gorm:"uniqueIndex:udx_name"`
}

使用 0/1 做删除标记

import "gorm.io/plugin/soft_delete"

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

// Query
SELECT * FROM users WHERE is_del = 0;

// Delete
UPDATE users SET is_del = 1 WHERE ID = 1;