gorm的简单操作

1. 什么是orm

ORM全称是:Object Relational Mapping(对象关系映射),其主要作用是在编程中,把面向对象的概念跟数据库中表的概念对应起来。举例来说就是,我定义一个对象,那就对应着一张表,这个对象的实例,就对应着表中的一条记录。
对于数据来说,最重要最常用的是表:表中有列, orm就是将一张表映射成一个类,表中的列映射成类中的一个类。java 、python,但是针对go语言而言,struct,就是列如何映射,是因为列可以映射成struct中的类型,int->int,但是有另一个问题? 就是数据库中的列具备很好的描述性,但是struct有tag(标签)。执行sql, 需要我们有足够的sql语句基础、需要我们懂得不同的数据的sql

2. 常用orm

个人而言,不用太去纠结应该选择哪一个orm框架,但是实际上你用熟悉了一个,其他的orm迁移成本很低,我们选个一个star数量最高的一定不会有错,这些差异也不会很大sql语言远比orm重要的多
https://github.com/go-gorm/gorm
https://github.com/facebook/ent
https://github.com/jmoiron/sqlx
https://gitea.com/xorm/xorm/src/branch/master/README_CN.md
https://github.com/didi/gendry/blob/master/translation/zhcn/README.md

gorm文档

https://gorm.io/zh_CN/docs/create.html

3. orm的优缺点

优点:

  1. 提高了开发效率。
  2. 屏蔽sql细节。可以自动对实体Entity对象与数据库中的Table进行字段与属性的映射;不用直接SQL编码
  3. 屏蔽各种数据库之间的差异

缺点:

  1. orm会牺牲程序的执行效率和会固定思维模式
  2. 太过依赖orm会导致sql理解不够
  3. 对于固定的orm依赖过重,导致切换到其他的orm代价高

4. 如何正确看待orm和sql之间的关系

  1. sql为主,orm为辅
  2. orm主要目的是为了增加代码可维护性和开发效率

gorm连接数据库

package main

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

func main() {
	// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
	//loc 本地时区
	//想要正确的处理 time.Time ,您需要带上 parseTime 参数
	dsn := "root:root@tcp(192.168.0.102:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
	_, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic(err)
	}

}

注意:想要正确的处理 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{})

//设置全局的logger,这个logger在我们执行每个sql语句的时候会打印每一行sql

日志

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

Logger 接受的选项不多,您可以在初始化时自定义它,例如:

newLogger := logger.New(
  log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
  logger.Config{
    SlowThreshold:              time.Second,   // Slow SQL threshold
    LogLevel:                   logger.Silent, // Log level
    IgnoreRecordNotFoundError: true,           // Ignore ErrRecordNotFound error for logger
    ParameterizedQueries:      true,           // Don't include params in the SQL log
    Colorful:                  false,          // Disable color
  },
)

// Globally mode
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
  Logger: newLogger,
})

// Continuous session mode
tx := db.Session(&Session{Logger: newLogger})
tx.First(&user)
tx.Model(&user).Update("Age", 18)

日志级别

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

db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
  Logger: logger.Default.LogMode(logger.Silent),
})

Debug

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

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

自定义 Logger

参考 GORM 的 默认 logger 来定义您自己的 logger

Logger 需要实现以下接口,它接受 context,所以你可以用它来追踪日志

type Interface interface {
    LogMode(LogLevel) Interface
    Info(context.Context, string, ...interface{})
    Warn(context.Context, string, ...interface{})
    Error(context.Context, string, ...interface{})
    Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error)
}

创建表

_ = db.AutoMigrate(&Product{}) //此次应该有sql语句跟日志级别有关系

通过NullString解决不能更新零值的问题
整体源码

package main

import (
	"database/sql"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
	"os"
	"time"
)

type Product struct {
	gorm.Model
	//Code  string
	Code  sql.NullString
	Price uint
}

func main() {
	// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
	//loc 本地时区
	//想要正确的处理 time.Time ,您需要带上 parseTime 参数
	dsn := "root:root@tcp(192.168.0.102:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"

	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
		logger.Config{
			SlowThreshold: time.Second, // 慢SQL阈值
			LogLevel:      logger.Info, // 日志级别
			// 忽略记录器的ErrRecordNotFound错误 是否忽略 record not found 错误,默认为 false,如果设置为 true,则在查询结果为空时不会打印 record not found 错误信息
			IgnoreRecordNotFoundError: false,
			ParameterizedQueries:      false, // 不要在SQL日志中包含参数
			Colorful:                  true,  // 禁言彩色打印
		},
	)

	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: newLogger,
	})
	if err != nil {
		panic(err)
	}

	//设置全局的logger,这个logger在我们执行每个sql语句的时候会打印每一行sql

	//定义一个表结构,将表结构直接生成对应的表---migrations(迁移)
	//迁移
	_ = db.AutoMigrate(&Product{}) //此次应该有sql语句

	// 新增
	//db.Create(&Product{Code: "D42", Price: 100})
	db.Create(&Product{Code: sql.NullString{"D42", true}, Price: 100})
	// 查询
	var product Product
	db.First(&product, 1) // 根据整型主键查找
	db.First(&product, "code = ?", "D42") // 查找 code 字段值为 D42 的记录

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

	// Delete - 删除 product
	//并没有执行deleta语句,而是逻辑删除
	//db.Delete(&product, 1)
}

解决仅更新非零值字段的方法有两种

	/**
	1 将string设置为*string
	2 使用sql的NULLxxx来结局
	*/
	empty := ""
	db.Model(&User{ID: 1}).Updates(User{Email: &empty})
	//updates语句不会更新零值,但update语句会更新
	//db.Model(&User{ID: 1}).Update("Name", "")
	//db.Model(&User{ID: 1}).Updates(User{Name: ""})

约定

GORM 倾向于约定优于配置 默认情况下,GORM 使用 ID 作为主键,使用结构体名的 蛇形复数 作为表名,字段名的 蛇形 作为列名,并使用 CreatedAt、UpdatedAt 字段追踪创建、更新时间
如果您遵循 GORM 的约定,您就可以少写的配置、代码。 如果约定不符合您的实际要求,GORM 允许你配置它们

嵌入结构体

对于匿名字段,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 风格–>去官网看源码

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
	"os"
	"time"
)

type User struct {
	UserID        uint `gorm:"primarykey"`
	Name string `gorm:"column:user_name;type:varchar(50);index:idx_user_name;unique;default:'bobby'"`
}

func main() {
	// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
	//loc 本地时区
	//想要正确的处理 time.Time ,您需要带上 parseTime 参数
	dsn := "root:root@tcp(192.168.0.102:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"

	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
		logger.Config{
			SlowThreshold:             time.Second, // 慢SQL阈值
			LogLevel:                  logger.Info, // 日志级别
			IgnoreRecordNotFoundError: false,
			ParameterizedQueries:      false, // 不要在SQL日志中包含参数
			Colorful:                  true,  // 禁言彩色打印
		},
	)

	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: newLogger,
	})
	if err != nil {
		panic(err)
	}

	_ = db.AutoMigrate(&User{}) //此次应该有sql语句
		db.Create(&User{})

}

在这里插入图片描述

通过create方法插入记录

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

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

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

我们还可以使用 Create() 创建多项记录:

users := []*User{
    User{Name: "Jinzhu", Age: 18, Birthday: time.Now()},
    User{Name: "Jackson", Age: 19, Birthday: time.Now()},
}

result := db.Create(users) // 传递切片以插入多行数据

result.Error        // 返回 error
result.RowsAffected // 返回插入记录的条数
package main

import (
	"database/sql"
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
	"os"
	"time"
)

type User struct {
	ID           uint
	Name         string
	Email        *string //使用指针的方法解决空字符串
	Age          uint8
	Birthday     *time.Time
	MemberNumber sql.NullString
	ActivatedAt  sql.NullTime
	CreatedAt    time.Time
	UpdatedAt    time.Time
}

func main() {
	// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
	//loc 本地时区
	//想要正确的处理 time.Time ,您需要带上 parseTime 参数
	dsn := "root:root@tcp(192.168.0.102:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"

	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
		logger.Config{
			SlowThreshold:             time.Second, // 慢SQL阈值
			LogLevel:                  logger.Info, // 日志级别
			IgnoreRecordNotFoundError: false,
			ParameterizedQueries:      false, // 不要在SQL日志中包含参数
			Colorful:                  true,  // 禁言彩色打印
		},
	)

	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: newLogger,
	})
	if err != nil {
		panic(err)
	}

	_ = db.AutoMigrate(&User{}) //此次应该有sql语句

	user := User{
		Name: "bobby2",
	}
	//user.ID             // 返回插入数据的主键
	//result.Error        // 返回 error
	//result.RowsAffected // 返回插入记录的条数
	fmt.Println(user.ID)
	result := db.Create(&user)
	fmt.Println(user.ID)
	fmt.Println(result.Error)
	fmt.Println(result.RowsAffected)

	//updates语句不会更新零值,但update语句会更新
	//db.Model(&User{ID: 1}).Update("Name", "")
	//db.Model(&User{ID: 1}).Updates(User{Name: ""})

	//解决仅更新非零值字段的方法有两种
	/**
	1 将string设置为*string
	2 使用sql的NULLxxx来结局
	*/
	//empty := ""
	//db.Model(&User{ID: 1}).Updates(User{Email: &empty})

}

用指定的字段创建记录

创建记录并为指定字段赋值。

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

创建记录并忽略传递给 ‘Omit’ 的字段值

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

批量插入

要高效地插入大量记录,请将切片传递给Create方法。GORM将生成一条SQL语句来插入所有数据并回填主键值,钩子方法也将被调用。. 当记录可以分成多个批处理时,它将开始一个 交易。

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

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

你可以在使用CreateInBatches创建时指定批处理大小,例如:

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

// batch size 100
db.CreateInBatches(users, 100)

为什么不一次性提交所有的,还要分批次?
答:sql语句是有长度限制!

在使用Upsert(插入更新)和Create With Associations(使用关联创建)时,也支持批量插入
初始化GORM时使用CreateBatchSize选项,所有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允许用户定义钩子来实现前保存,前创建,后保存,后创建。这些钩子方法将在创建记录时被调用,有关生命周期的详细信息请参阅Hooks

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

    if u.Role == "admin" {
        return errors.New("invalid role")
    }
    return
}

如果你想跳过Hooks方法,你可以使用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},
})

当创建from map时,钩子不会被调用,关联不会被保存,主键值不会被回填

关联创建—>这个很像一对一

当创建一些具有关联的数据时,如果它的关联值不是零值,那么这些关联将被替换,并且它的Hooks方法将被调用。

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` ...

你可以使用Select, Omit跳过保存关联,例如:

db.Omit("CreditCard").Create(&user)
// skip all associations
db.Omit(clause.Associations).Create(&user)

查询

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

// 获取第一条记录(主键升序)
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;

// 获取一条记录,没有指定排序字段
db.Take(&user)
// SELECT * FROM users LIMIT 1;

// 获取最后一条记录(主键降序)
db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;

result := db.First(&user)
result.RowsAffected // 返回找到的记录数
result.Error        // returns error or nil

// 检查 ErrRecordNotFound 错误===>没有找到数据
errors.Is(result.Error, gorm.ErrRecordNotFound)

如果你想避免ErrRecordNotFound错误,你可以使用Find,比如db.Limit(1).Find(&user),Find方法可以接受struct和slice的数据。
对单个对象使用Find而不带limit,db.Find(&user)将会查询整个表并且只返回第一个对象,这是性能不高并且不确定的。

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

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

// works because model is specified using `db.Model()`
result := map[string]interface{}{}
db.Model(&User{}).First(&result)
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1

// doesn't work
result := map[string]interface{}{}
db.Table("users").First(&result)

// works with Take
result := map[string]interface{}{}
db.Table("users").Take(&result)

// no primary key defined, results will be ordered by first field (i.e., `Code`)
type Language struct {
  Code string
  Name string
}
db.First(&Language{})
// SELECT * FROM `languages` ORDER BY `languages`.`code` LIMIT 1
//通过first查询单个数据
var user User
db.First(&user)

//通过主键查询
//我们不能给user赋值
result := db.First(&user, 2)

if errors.Is(result.Error, gorm.ErrRecordNotFound) {
	fmt.Println("未找到")
}
fmt.Println(user.ID)

var users []User
result1 := db.Find(&users)
fmt.Println("总共记录", result1.RowsAffected)

for _, user := range users {
	fmt.Println(user.ID)
}

根据主键检索

如果主键是数字类型,您可以使用 内联条件 来检索对象。 当使用字符串时,需要额外的注意来避免SQL注入;查看 Security 部分来了解详情。

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);
//通过first查询单个数据
	var user User
	db.First(&user)

	//通过主键查询
	//我们不能给user赋值
	result := db.First(&user, 2)

	if errors.Is(result.Error, gorm.ErrRecordNotFound) {
		fmt.Println("未找到")
	}
	fmt.Println(user.ID)

如果主键是字符串(例如像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;

NOTE: 如果您使用 gorm 的特定字段类型(例如 gorm.DeletedAt),它将运行不同的查询来检索对象。

type User struct {
  ID           string `gorm:"primarykey;size:16"`
  Name         string `gorm:"size:24"`
  DeletedAt    gorm.DeletedAt `gorm:"index"`
}

var user = User{ID: 15}
db.First(&user)
//  SELECT * FROM `users` WHERE `users`.`id` = '15' AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1

检索全部对象

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

result.RowsAffected // returns found records count, equals `len(users)`
result.Error        // returns error
var users []User
	result1 := db.Find(&users)
	fmt.Println("总共记录", result1.RowsAffected)

	for _, user := range users {
		fmt.Println(user.ID)

条件

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===>jinzhu或者jinzhu2
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';

如果对象设置了主键,条件查询将不会覆盖主键的值,而是用 And 连接条件。 例如:

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

这个查询将会给出record not found错误 所以,在你想要使用例如 user 这样的变量从数据库中获取新值前,需要将例如 id 这样的主键设置为nil。

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

要在查询条件中包含零值,您可以使用映射,它将包括所有键值作为查询条件,例如:

db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;
//通过where查询
	var user User
	db.Where("name = ?", "bobby1").First(&user)
	var users []User
	db.Where(&User{Name: "bobby1"}).Find(&users) //这个才是查询所有的
	for _, user1 := range users {
		fmt.Println(user1.ID)
	}

	db.Where("name IN ?", []string{"bobby1", "bobby2"}).Find(&users)

更新

保存所有字段
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;

Save是一个组合功能。如果save value不包含主键,它将执行Create,否则将执行Update(包含所有字段)。

db.Save(&User{Name: "jinzhu", Age: 100})
// INSERT INTO `users` (`name`,`age`,`birthday`,`update_at`) VALUES ("jinzhu",100,"0000-00-00 00:00:00","0000-00-00 00:00:00")

db.Save(&User{ID: 1, Name: "jinzhu", Age: 100})
// UPDATE `users` SET `name`="jinzhu",`age`=100,`birthday`="0000-00-00 00:00:00",`update_at`="0000-00-00 00:00:00" WHERE `id` = 1

更新单个列

当使用Update更新单个列时,它需要有任何条件,否则会引发错误ErrMissingWhereClause,请查看Block Global Updates了解详细信息。当使用Model方法并且它的值有一个主值时,主键将被用来构建条件,例如:

// Update with conditions
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's ID is `111`:
db.Model(&user).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;

// Update with conditions and model value
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支持使用struct或map[string]接口{}进行更新,当使用struct进行更新时,默认情况下只更新非零字段

// Update attributes with `struct`, will only update non-zero fields
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;

// Update attributes with `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只会更新非零字段。您可能希望使用map来更新属性,或者使用Select来指定要更新的字段

更新选定字段

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

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

更新 Hook

GORM允许钩子beforeave, BeforeUpdate, AfterSave, AfterUpdate。这些方法将在更新记录时调用,详情请参考Hooks

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将执行批处理更新

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

// Update with 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);

阻止全局更新

如果你执行一个批处理更新没有任何条件,GORM将不会运行它,并将返回ErrMissingWhereClause错误默认
例如,您必须使用某些条件或使用原始SQL或启用AllowGlobalUpdate模式

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"

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

更新的记录数

获取受更新影响的行数

// Get updated records count with `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 // returns updated records count
result.Error        // returns updating error

删除

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

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

钩子函数

对于删除操作,GORM 支持 BeforeDelete、AfterDelete 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%";

可以将一个主键切片传递给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 错误,如果一定要这么做,你必须添加一些条件,或者使用原生SQL,或者开启AllowGlobalUpdate 模式,如下例:

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

返回删除行的数据

返回被删除的数据,仅当数据库支持回写功能时才能正常运行,如下例:

// 回写所有的列
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}}

软删除

如果你的模型包含了 gorm.DeletedAt字段(该字段也被包含在gorm.Model中),那么该模型将会自动获得软删除的能力!
当调用Delete时,GORM并不会从数据库中删除该记录,而是将该记录的DeleteAt设置为当前时间,而后的一般查询方法将无法查找到此条记录。

// 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来查询到被软删除的记录

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

永久删除

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

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

删除标志

默认情况下,gorm.Model使用*time.Time作为DeletedAt 的字段类型,不过软删除插件gorm.io/plugin/soft_delete同时也提供其他的数据格式支持
提示 当使用DeletedAt创建唯一复合索引时,你必须使用其他的数据类型,例如通过gorm.io/plugin/soft_delete插件将字段类型定义为unix时间戳等等

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

Unix 时间戳

使用unix时间戳作为删除标志

import "gorm.io/plugin/soft_delete"

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

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

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

你同样可以指定使用毫秒 milli或纳秒 nano作为值,如下例:

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 = /* current unix milli second or nano second */ 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;

混合模式

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

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

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

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

Belongs To

belongs to 会与另一个模型建立了一对一的连接。 这种模型的每一个实例都“属于”另一个模型的一个实例。
例如,您的应用包含 user 和 company,并且每个 user 能且只能被分配给一个 company。下面的类型就表示这种关系。 注意,在 User 对象中,有一个和 Company 一样的 CompanyID。 默认情况下, CompanyID 被隐含地用来在 User 和 Company 之间创建一个外键关系, 因此必须包含在 User 结构体中才能填充 Company 内部结构体。

// `User` 属于 `Company`,`CompanyID` 是外键
type User struct {
  gorm.Model
  Name      string
  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
  Company      Company `gorm:"foreignKey:CompanyRefer"`
  // 使用 CompanyRefer 作为外键
}

type Company struct {
  ID   int
  Name string
}

重写引用

对于 belongs to 关系,GORM 通常使用数据库表,主表(拥有者)的主键值作为外键参考。 正如上面的例子,我们使用主表Company中的主键字段ID作为外键的参考值。
如果设置了User实体属于Company实体,那么GORM会自动把Company中的ID属性保存到User的CompanyID属性中。
同样的,您也可以使用标签 references 来更改它,例如:

type User struct {
  gorm.Model
  Name      string
  CompanyID string
  Company   Company `gorm:"references:Code"` // 使用 Code 作为引用
}

type Company struct {
  ID   int
  Code string
  Name string
}

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

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
}
package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
	"os"
	"time"
)

type User struct {
	gorm.Model
	Name      string
	CompanyID int //数据库中存储的字段  sql使用它
	Company   Company
}

type Company struct {
	ID   int
	Name string
}

func main() {
	// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
	//loc 本地时区
	//想要正确的处理 time.Time ,您需要带上 parseTime 参数
	dsn := "root:root@tcp(192.168.0.102:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"

	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
		logger.Config{
			SlowThreshold:             time.Second, // 慢SQL阈值
			LogLevel:                  logger.Info, // 日志级别
			IgnoreRecordNotFoundError: false,
			ParameterizedQueries:      false, // 不要在SQL日志中包含参数
			Colorful:                  true,  // 禁言彩色打印
		},
	)

	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: newLogger,
	})
	if err != nil {
		panic(err)
	}

	//db.AutoMigrate(&User{}) //新建了user表和company表,并且设置外键
	//db.Create(&User{
	//	Name: "bobby",
	//	Company: Company{
	//		Name: "chengpeng",
	//	},
	//})

	//解决外键存在的问题
	db.Create(&User{
		Name: "bobby2",
		Company: Company{
			ID: 1,
		},
	})
}

Belongs to 的 CRUD

查看 关联模式 获取 belongs to 相关的用法

预加载

GORM 可以通过 Preload、Joins 预加载 belongs to 关联的记录,查看 预加载 获取详情

外键约束

你可以通过 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
}
var user User
//Preload
//db.Preload("Company").First(&user)
db.Joins("Company").First(&user)
fmt.Println(user)

Has Many

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

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

声明

// User 有多张 CreditCard,UserID 是外键
type User struct {
  gorm.Model
  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 会将 user 的 ID 字段保存到 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")

Has Many 的 CURD

查看 关联模式 获取 has many 相关的用法

预加载

GORM 可以通过 Preload 预加载 has many 关联的记录,查看 预加载 获取详情

自引用 Has Many

type User struct {
  gorm.Model
  Name      string
  ManagerID *uint
  Team      []User `gorm:"foreignkey:ManagerID"`
}

外键约束

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

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

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

你也可以在删除记录时通过 Select 来删除 has many 关联的记录,查看 Delete with Select 获取详情

package main

import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
	"os"
	"time"
)

// User 有多张 CreditCard,UserID 是外键
type User struct {
	gorm.Model
	CreditCards []CreditCard `gorm:"foreignKey:UserID"`
}

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

func main() {
	// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
	//loc 本地时区
	//想要正确的处理 time.Time ,您需要带上 parseTime 参数
	dsn := "root:root@tcp(192.168.0.102:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"

	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
		logger.Config{
			SlowThreshold:             time.Second, // 慢SQL阈值
			LogLevel:                  logger.Info, // 日志级别
			IgnoreRecordNotFoundError: false,
			ParameterizedQueries:      false, // 不要在SQL日志中包含参数
			Colorful:                  true,  // 禁言彩色打印
		},
	)

	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: newLogger,
	})
	if err != nil {
		panic(err)
	}

	//db.AutoMigrate(&User{}) //如果一个一个创建,就会发现没有外键
	//db.AutoMigrate(&CreditCard{})
	//db.AutoMigrate(&User{}, &CreditCard{}) //这样创建就会有外键
	

	//user := &User{}
	//db.Create(&user)
	//db.Create(&CreditCard{
	//	Number: "123",
	//	UserID: 1,
	//})
	//
	//db.Create(&CreditCard{
	//	Number: "1234",
	//	UserID: 1,
	//})

	var user User
	db.Preload("CreditCards").First(&user)
	for _, card := range user.CreditCards {
		fmt.Println(card.Number)
	}
}

在大型的系统中,不建议使用外键约束,外键约束也有很大的优点:数据的完整性
外键约束会让你的数据很完整,即使是业务代码,有些人考虑的不严谨也不会造成数据不一致
在大型的系统,高并发的系统重一般不使用外键约束,自己在业务层面保证数据的一致性

Many To Many

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

注意: 某些数据库只允许在唯一索引字段上创建外键,如果您在迁移时会创建外键,则需要指定 unique index 标签。

自引用 Many2Many

自引用 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

预加载

GORM 可以通过 Preload 预加载 has many 关联的记录,查看 预加载 获取详情

Many2Many 的 CURD

查看 关联模式 获取 many2many 相关的用法

自定义连接表

JoinTable可以是一个全功能的模型,像有软删除,钩子支持和更多的字段,你可以设置它与SetupJoinTable,例如:
注意: 自定义连接表要求外键是复合主键或复合唯一索引

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

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 实现外键约束,在使用 GORM 进行迁移时它会被创建,例如:

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

你也可以在删除记录时通过 Select 来删除 many2many 关系的记录,查看 Delete with Select 获取详情

复合外键

如果您的模型使用了 复合主键,GORM 会默认启用复合外键。

您也可以覆盖默认的外键、指定多个外键,只需用逗号分隔那些键名,例如:

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;"`
  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
package main

import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
	"os"
	"time"
)

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

type Language struct {
	gorm.Model
	Name string
}

func main() {
	// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
	//loc 本地时区
	//想要正确的处理 time.Time ,您需要带上 parseTime 参数
	dsn := "root:root@tcp(192.168.0.102:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"

	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
		logger.Config{
			SlowThreshold:             time.Second, // 慢SQL阈值
			LogLevel:                  logger.Info, // 日志级别
			IgnoreRecordNotFoundError: false,
			ParameterizedQueries:      false, // 不要在SQL日志中包含参数
			Colorful:                  true,  // 禁言彩色打印
		},
	)

	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: newLogger,
	})
	if err != nil {
		panic(err)
	}

	//db.AutoMigrate(&User{})

	//插入
	//languages := []Language{}
	//languages = append(languages, Language{Name: "go"})
	//languages = append(languages, Language{Name: "java"})
	//user := User{
	//
	//	Languages: languages,
	//}
	//
	//db.Create(&user)

	//获取数据--第一种
	//var user User
	//db.Preload("Languages").First(&user)
	//for _, language := range user.Languages {
	//	fmt.Println(language.Name)
	//}

	//如果我们已经取出一个用户来了,但是这个用户我们之前没有使用preload来加载对应的Languages
	//不是说用户有language我们就一定要取出来
	// 开始关联模式
	//查找关联-->第二种方法
	var user User
	db.First(&user)
	var laguages []Language
	_ = db.Model(&user).Association("Languages").Find(&laguages)
	for _, laguage := range laguages {
		fmt.Println(laguage.Name)
	}

	fmt.Println(user.ID)

}

我们自己定义表面是什么
同一的给所有的表面加上前缀
NamingStrategy和TableName不能同时配置

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"gorm.io/gorm/schema"
	"log"
	"os"
	"time"
)

//type Language1 struct {
//	gorm.Model
//	Name string
//}

type Language2 struct {
	Name    string
	AddTime time.Time //每个记录创建的时候自动加上当前加入到AddTime中  不加BeforeCreate这个方法就会报错
	//也可以加这样加
	//AddTime1 sql.NullTime
}

func (l *Language2) BeforeCreate(tx *gorm.DB) (err error) {
	l.AddTime = time.Now()
	return
}

//在数据默认是加一个默认值

// TableName 在gorm中可以通过给某一个struct添加TableName方法自定义表面
// 自定义表名
//func (Language1) TableName() string {
//	return "my_language"
//}

// 同一个表名加一个前缀
func main() {
	// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
	//loc 本地时区
	//想要正确的处理 time.Time ,您需要带上 parseTime 参数
	dsn := "root:root@tcp(192.168.0.102:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"

	//NamingStrategy和TableName不能同时配置
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
		logger.Config{
			SlowThreshold:             time.Second, // 慢SQL阈值
			LogLevel:                  logger.Info, // 日志级别
			IgnoreRecordNotFoundError: false,
			ParameterizedQueries:      false, // 不要在SQL日志中包含参数
			Colorful:                  true,  // 禁言彩色打印
		},
	)

	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		NamingStrategy: schema.NamingStrategy{
			TablePrefix: "chengpeng_",
		},
		Logger: newLogger,
	})
	if err != nil {
		panic(err)
	}

	db.AutoMigrate(&Language2{})
	db.Create(&Language2{Name: "Python"})
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/159140.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

故障发现、定位提效超 70%,去哪儿可观测体系做了哪些优化?

一分钟精华速览 去哪儿网的原有监控系统在指标数量上展现出了强大实力——上亿指标量和百万级的告警量&#xff0c;但在故障数据方面却稍显不足——订单类故障平均发现时间长达 4 分钟&#xff0c;仅有 20%的订单类故障能在 1 分钟内被发现&#xff0c;近半数的故障处理时长超…

Jenkins自动化部署一个Maven项目

Jenkins自动化部署 提示&#xff1a;本教程基于CentOS Linux 7系统下进行 Jenkins的安装 1. 下载安装jdk11 官网下载地址&#xff1a;https://www.oracle.com/cn/java/technologies/javase/jdk11-archive-downloads.html 本文档教程选择的是jdk-11.0.20_linux-x64_bin.tar.g…

接口测试实战工具如何选择?这6个工具首选(建议收藏)

常见接口类型 • HTTP/HTTPS 类型接口 基于HTTP协议开发的接口现在应用是最为广泛的&#xff0c;这类API使用起来简单明了&#xff0c;因为它是轻量级的、跨平台、跨语言的&#xff0c; 但凡是第三方提供的API都会有HTTP版本的接口。 RESTful API也是基于HTTP协议的&#xff0c…

Google Play上架马甲包攻略

想要更好的海外推广离开google play应用市场&#xff0c;有了谷歌就可以按各平台规则来进行推广&#xff0c;做广告投放基本都离不开谷歌市场的包&#xff0c;所以有了马甲包。然而&#xff0c;Google Play对于马甲包的上传有一系列限制和规定。以下是一些Google Play上架马甲包…

leetcode算法之位运算

目录 1.判断字符是否唯一2.丢失的数字3.两整数之和4.只出现一次的数字II5.消失的两个数字6.位1的个数7.比特位计数8.汉明距离 1.判断字符是否唯一 判断字符是否唯一 class Solution { public:bool isUnique(string astr) {//利用鸽巢原理做优化if(astr.size()>26) return…

性能压力测试的优势与重要性

性能压力测试是软件开发过程中至关重要的一环&#xff0c;它通过模拟系统在极限条件下的运行&#xff0c;以评估系统在正常和异常负载下的表现。这种测试为确保软件系统的可靠性、稳定性和可伸缩性提供了关键信息。下面将探讨性能压力测试的优势以及为什么在软件开发中它具有不…

数据资产到底如何入表?

2024年1月1日起&#xff0c;财政部《企业数据资源相关会计处理暂行规定》正式施行&#xff0c;距离现在只有一个多月的时间。 数据资源入表意味着企业可以将数据资源确认为企业资产负债表中“资产”一项。对于拥有丰富数据资源的企业来说&#xff0c;有望在财务报表中体现其真…

Redis(集合Set和有序集合SortedSet)

SET集合中的元素是不允许重复的&#xff0c;SET中的命令都是以S开头的。 使用SADD 在集合中添加元素&#xff0c;使用SMEMBERS查看元素。 当添加重复元素时&#xff0c;会返回0代表添加失败&#xff0c;查询还是就Redis一个元素。 使用SISMEMBER查询元素是否在集合中&#xff…

ADAudit Plus:提升企业安全的不可或缺的审计解决方案

随着信息技术的飞速发展&#xff0c;企业在数字化转型过程中面临着越来越多的安全威胁。为了保护敏感数据、防范内外部攻击&#xff0c;企业需要强大的安全审计解决方案。在这个背景下&#xff0c;ADAudit Plus作为一款强大而全面的安全审计工具&#xff0c;成为了众多企业的首…

谷歌浏览器F12/打开开发者工具网络就无法连接报错

刷新页面就出现提示&#xff1a; 原因&#xff1a;浏览器网络设置的问题&#xff0c;网络设置了离线状态&#xff0c;切换回3G即可。

网站页头被挂马状态及新增了index.html文件解决思路

今天网站刚新增了篇了文章《从nginx层阻断可执行的php 防止宝塔站点挂马》,整体测试下来还是不靠谱,设置后导致所有PHP文件都打不开了。 经过不断的查看日志和搜索办法总算告一段落,后续待观察。原因如下,多个网站目录新增了index.html文件,看时间是近两天上传的。 网站代…

Quest 3期间Sui上游戏处理了数百万笔交易

Sui固有的可扩展性和低且可预测的gas费使其成为Web3游戏的理想平台。在Quest 3中&#xff0c;参与的游戏项目处理了数百万笔交易&#xff0c;这毫无疑问地展示了Sui卓越的能力。 Quest 3的主题是游戏&#xff0c;让开发者有机会向潜在玩家介绍他们激动人心的创作。鼓励这些玩家…

Go 字符串处理:fmt.Sprintf与string.Builder的比较

在Go语言中&#xff0c;我们通常会遇到两种主要的方式来处理和操作字符串&#xff1a;使用fmt.Sprintf函数和string.Builder类型。尽管两者都可以实现字符串的格式化和连接&#xff0c;但它们在性能和用法上有一些关键区别。 1. fmt.Sprintf fmt.Sprintf是一个函数&#xff0c…

【解疑】ZIP压缩包的密码可以取消吗?

ZIP是工作中经常用到的文件压缩格式&#xff0c;它可以将多个文件压缩成一个文件&#xff0c;便于传输和存储。有时候&#xff0c;为了保护文件的私密性&#xff0c;很多人还会在压缩文件的同时设置密码保护。那问题来了&#xff0c;要是后续不需要保护了&#xff0c;ZIP压缩包…

DDD落地:从腾讯视频DDD重构之路,看DDD极大价值

尼恩说在前面 在40岁老架构师 尼恩的读者交流群(50)中&#xff0c;最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格&#xff0c;遇到很多很重要的面试题&#xff1a; 谈谈你的DDD落地经验&#xff1f; 谈谈你对DDD的理解&#x…

(C++)string类及其函数

愿所有美好如期而遇 标准库中的string类 string是对类模板实例化出的类型的重命名&#xff0c;也就是说string就是basic_string<char>这个类型。 string是表示字符串的字符串类 该类接口与常规类的接口基本相同&#xff0c;再添加了一些专门用来操作string的常规操作。…

接口测试系列之 —— 前端交互测试和后端逻辑测试

01 前端交互测试 前端页面与后端代码之间的交互测试&#xff0c;可以理解为接口功能测试的一个子集。 测试准备 在进行交互测试前&#xff0c;首先要对前端功能有明确的认知&#xff0c;能够明确区分&#xff1a; 什么功能属于前端页面逻辑功能 什么功能又属于前端与后…

专业数据标注公司:景联文科技领航数据标注行业,满足大模型时代新需求

随着大模型的蓬勃发展和相关政策的逐步推进&#xff0c;为数据要素市场化配置的加速推进提供了有力的技术保障和政策支持。数据要素生产力度的不断提升&#xff0c;为数据标注产业带来了迅速发展的契机。 根据国家工信安全发展研究中心测算&#xff0c;2022年中国数据加工环节的…