子查询
查询大于平均年龄的用户
# 原生sql
select * from students where age > (select avg(age) from students);
使用gorm编写
var users []Student
DB.Model(Student{}).Where("age > (?)", DB.Model(Student{}).Select("avg(age)")).Find(&users)
fmt.Println(users)
命名参数
var users []Student
DB.Where("name = @name and age = @age", sql.Named("name", "枫枫"), sql.Named("age", 23)).Find(&users)
DB.Where("name = @name and age = @age", map[string]any{"name": "枫枫", "age": 23}).Find(&users)
fmt.Println(users)
find到map
var res []map[string]any
DB.Table("students").Find(&res)
fmt.Println(res)
查询引用Scope
可以再model层写一些通用的查询方式,这样外界就可以直接调用方法即可
func Age23(db *gorm.DB) *gorm.DB {
return db.Where("age > ?", 23)
}
func main(){
var users []Student
DB.Scopes(Age23).Find(&users)
fmt.Println(users)
}
我们先从一对多开始多表关系的学习
因为一对多的关系生活中到处都是
例如
老板与员工
女神和舔狗
老师和学生
班级与学生
用户与文章
...
一对多关系 表结构建立
在gorm中,官方文档是把一对多关系分为了两类,
Belongs To 属于谁
Has Many 我拥有的
他们本来是一起的,本教程把它们合在一起讲
我们以用户和文章为例
一个用户可以发布多篇文章,一篇文章属于一个用户
type User struct {
ID uint `gorm:"size:4"`
Name string `gorm:"size:8"`
Articles []Article // 用户拥有的文章列表
}
type Article struct {
ID uint `gorm:"size:4"`
Title string `gorm:"size:16"`
UserID uint // 属于 这里的类型要和引用的外键类型一致,包括大小
User User // 属于
}
关于外键命名,外键名称就是关联表名+ID,类型是uint
重写外键关联
type User struct {
ID uint `gorm:"size:4"`
Name string `gorm:"size:8"`
Articles []Article `gorm:"foreignKey:UID"` // 用户拥有的文章列表
}
type Article struct {
ID uint `gorm:"size:4"`
Title string `gorm:"size:16"`
UID uint // 属于
User User `gorm:"foreignKey:UID"` // 属于
}
这里有个地方要注意
我改了Article 的外键,将UID作为了外键,那么User这个外键关系就要指向UID
与此同时,User所拥有的Articles也得更改外键,改为UID
重写外键引用
type User struct {
ID uint `gorm:"size:4"`
Name string `gorm:"size:8"`
Articles []Article `gorm:"foreignKey:UserName;references:Name"` // 用户拥有的文章列表
}
type Article struct {
ID uint `gorm:"size:4"`
Title string `gorm:"size:16"`
UserName string
User User `gorm:"references:Name"` // 属于
}
这一块的逻辑比较复杂
比如有1个用户
id | name |
1 | 枫枫 |
之前的外键关系是这样表示文章的
id | title | user_id |
1 | python | 1 |
2 | javascript | 1 |
3 | golang | 1 |
如果改成直接关联Name,那就变成了这样
id | title | user_name |
1 | python | 枫枫 |
2 | javascript | 枫枫 |
3 | golang | 枫枫 |
虽然这样很方便,但是非常不适合在实际项目中这样用
我们还是用第一版的表结构做一对多关系的增删改查
一对多的添加
创建用户,并且创建文章
a1 := Article{Title: "python"}
a2 := Article{Title: "golang"}
user := User{Name: "枫枫", Articles: []Article{a1, a2}}
DB.Create(&user)
gorm自动创建了两篇文章,以及创建了一个用户,还将他们的关系给关联上了
创建文章,关联已有用户
a1 := Article{Title: "golang零基础入门", UserID: 1}
DB.Create(&a1)
var user User
DB.Take(&user, 1)
DB.Create(&Article{Title: "python零基础入门", User: user})
外键添加
给现有用户绑定文章
var user User
DB.Take(&user, 2)
var article Article
DB.Take(&article, 5)
user.Articles = []Article{article}
DB.Save(&user)
也可以用Append方法
var user User
DB.Take(&user, 2)
var article Article
DB.Take(&article, 5)
//user.Articles = []Article{article}
//DB.Save(&user)
DB.Model(&user).Association("Articles").Append(&article)
给现有文章关联用户
var article Article
DB.Take(&article, 5)
article.UserID = 2
DB.Save(&article)
也可用Append方法
var user User
DB.Take(&user, 2)
var article Article
DB.Take(&article, 5)
DB.Model(&article).Association("User").Append(&user)
查询
查询用户,显示用户的文章列表
var user User
DB.Take(&user, 1)
fmt.Println(user)
直接这样,是显示不出文章列表
预加载
我们必须要使用预加载来加载文章列表
var user User
DB.Preload("Articles").Take(&user, 1)
fmt.Println(user)
预加载的名字就是外键关联的属性名
查询文章,显示文章用户的信息
同样的,使用预加载
var article Article
DB.Preload("User").Take(&article, 1)
fmt.Println(article)
嵌套预加载
查询文章,显示用户,并且显示用户关联的所有文章,这就得用到嵌套预加载了
var article Article
DB.Preload("User.Articles").Take(&article, 1)
fmt.Println(article)
带条件的预加载
查询用户下的所有文章列表,过滤某些文章
var user User
DB.Preload("Articles", "id = ?", 1).Take(&user, 1)
fmt.Println(user)
这样,就只有id为1的文章被预加载出来了
自定义预加载
var user User
DB.Preload("Articles", func(db *gorm.DB) *gorm.DB {
return db.Where("id in ?", []int{1, 2})
}).Take(&user, 1)
fmt.Println(user)
CopyErrorOK!
删除
级联删除
删除用户,与用户关联的文章也会删除
var user User
DB.Take(&user, 1)
DB.Select("Articles").Delete(&user)
CopyErrorOK!
清除外键关系
删除用户,与将与用户关联的文章,外键设置为null
var user User
DB.Preload("Articles").Take(&user, 2)
DB.Model(&user).Association("Articles").Delete(&user.Articles)
一对一关系比较少,一般用于表的扩展
例如一张用户表,有很多字段
那么就可以把它拆分为两张表,常用的字段放主表,不常用的字段放详情表
表结构搭建
type User struct {
ID uint
Name string
Age int
Gender bool
UserInfo UserInfo // 通过UserInfo可以拿到用户详情信息
}
type UserInfo struct {
UserID uint // 外键
ID uint
Addr string
Like string
}
添加记录
添加用户,自动添加用户详情
DB.Create(&User{
Name: "枫枫",
Age: 21,
Gender: true,
UserInfo: UserInfo{
Addr: "湖南省",
Like: "写代码",
},
})
添加用户详情,关联已有用户
这个场景特别适合网站的注册,以及后续信息完善
刚开始注册的时候,只需要填写很基本的信息,这就是添加主表的一条记录
注册进去之后,去个人中心,添加头像,修改地址...
这就是添加附表
DB.Create(&UserInfo{
UserID: 2,
Addr: "南京市",
Like: "吃饭",
})
当然,也可以直接把用户对象传递进来
我们需要改一下表结构
type User struct {
ID uint
Name string
Age int
Gender bool
UserInfo UserInfo // 通过UserInfo可以拿到用户详情信息
}
type UserInfo struct {
User *User // 要改成指针,不然就嵌套引用了
UserID uint // 外键
ID uint
Addr string
Like string
}
不限于重新迁移,直接添加即可
var user User
DB.Take(&user, 2)
DB.Create(&UserInfo{
User: &user,
Addr: "南京市",
Like: "吃饭",
})
查询
一般是通过主表查副表
var user User
DB.Preload("UserInfo").Take(&user)
fmt.Println(user)
多对多关系,需要用第三张表存储两张表的关系
表结构搭建
type Tag struct {
ID uint
Name string
Articles []Article `gorm:"many2many:article_tags;"` // 用于反向引用
}
type Article struct {
ID uint
Title string
Tags []Tag `gorm:"many2many:article_tags;"`
}
多对多添加
添加文章,并创建标签
DB.Create(&Article{
Title: "python基础课程",
Tags: []Tag{
{Name: "python"},
{Name: "基础课程"},
},
})
添加文章,选择标签
var tags []Tag
DB.Find(&tags, "name = ?", "基础课程")
DB.Create(&Article{
Title: "golang基础",
Tags: tags,
})
多对多查询
查询文章,显示文章的标签列表
var article Article
DB.Preload("Tags").Take(&article, 1)
fmt.Println(article)
查询标签,显示文章列表
var tag Tag
DB.Preload("Articles").Take(&tag, 2)
fmt.Println(tag)
多对多更新
移除文章的标签
var article Article
DB.Preload("Tags").Take(&article, 1)
DB.Model(&article).Association("Tags").Delete(article.Tags)
fmt.Println(article)
更新文章的标签
var article Article
var tags []Tag
DB.Find(&tags, []int{2, 6, 7})
DB.Preload("Tags").Take(&article, 2)
DB.Model(&article).Association("Tags").Replace(tags)
fmt.Println(article)
自定义连接表
默认的连接表,只有双方的主键id,展示不了更多信息了
这是官方的例子,我修改了一下
type Article struct {
ID uint
Title string
Tags []Tag `gorm:"many2many:article_tags"`
}
type Tag struct {
ID uint
Name string
}
type ArticleTag struct {
ArticleID uint `gorm:"primaryKey"`
TagID uint `gorm:"primaryKey"`
CreatedAt time.Time
}
生成表结构
// 设置Article的Tags表为ArticleTag
DB.SetupJoinTable(&Article{}, "Tags", &ArticleTag{})
// 如果tag要反向应用Article,那么也得加上
// DB.SetupJoinTable(&Tag{}, "Articles", &ArticleTag{})
err := DB.AutoMigrate(&Article{}, &Tag{}, &ArticleTag{})
fmt.Println(err)
操作案例
举一些简单的例子
- 添加文章并添加标签,并自动关联
- 添加文章,关联已有标签
- 给已有文章关联标签
- 替换已有文章的标签
- 添加文章并添加标签,并自动关联
DB.SetupJoinTable(&Article{}, "Tags", &ArticleTag{}) // 要设置这个,才能走到我们自定义的连接表
DB.Create(&Article{
Title: "flask零基础入门",
Tags: []Tag{
{Name: "python"},
{Name: "后端"},
{Name: "web"},
},
})
// CreatedAt time.Time 由于我们设置的是CreatedAt,gorm会自动填充当前时间,
// 如果是其他的字段,需要使用到ArticleTag 的添加钩子 BeforeCreate
- 添加文章,关联已有标签
DB.SetupJoinTable(&Article{}, "Tags", &ArticleTag{})
var tags []Tag
DB.Find(&tags, "name in ?", []string{"python", "web"})
DB.Create(&Article{
Title: "flask请求对象",
Tags: tags,
})
- 给已有文章关联标签
DB.SetupJoinTable(&Article{}, "Tags", &ArticleTag{})
article := Article{
Title: "django基础",
}
DB.Create(&article)
var at Article
var tags []Tag
DB.Find(&tags, "name in ?", []string{"python", "web"})
DB.Take(&at, article.ID).Association("Tags").Append(tags)
- 替换已有文章的标签
var article Article
var tags []Tag
DB.Find(&tags, "name in ?", []string{"后端"})
DB.Take(&article, "title = ?", "django基础")
DB.Model(&article).Association("Tags").Replace(tags)
CopyErrorOK!
- 查询文章列表,显示标签
var articles []Article
DB.Preload("Tags").Find(&articles)
fmt.Println(articles)
SetupJoinTable
添加和更新的时候得用这个
这样才能走自定义的连接表,以及走它的钩子函数
查询则不需要这个
自定义连接表主键
这个功能还是很有用的,例如你的文章表 可能叫ArticleModel,你的标签表可能叫TagModel
那么按照gorm默认的主键名,那就分别是ArticleModelID,TagModelID,太长了,根本就不实用
这个地方,官网给的例子看着也比较迷,不过我已经跑通了
主要是要修改这两项
joinForeignKey 连接的主键id
JoinReferences 关联的主键id
type ArticleModel struct {
ID uint
Title string
Tags []TagModel `gorm:"many2many:article_tags;joinForeignKey:ArticleID;JoinReferences:TagID"`
}
type TagModel struct {
ID uint
Name string
Articles []ArticleModel `gorm:"many2many:article_tags;joinForeignKey:TagID;JoinReferences:ArticleID"`
}
type ArticleTagModel struct {
ArticleID uint `gorm:"primaryKey"` // article_id
TagID uint `gorm:"primaryKey"` // tag_id
CreatedAt time.Time
}
生成表结构
DB.SetupJoinTable(&ArticleModel{}, "Tags", &ArticleTagModel{})
DB.SetupJoinTable(&TagModel{}, "Articles", &ArticleTagModel{})
err := DB.AutoMigrate(&ArticleModel{}, &TagModel{}, &ArticleTagModel{})
fmt.Println(err)
添加,更新,查询操作和上面的都是一样
操作连接表
如果通过一张表去操作连接表,这样会比较麻烦
比如查询某篇文章关联了哪些标签
或者是举个更通用的例子,用户和文章,某个用户在什么时候收藏了哪篇文章
无论是通过用户关联文章,还是文章关联用户都不太好查
最简单的就是直接查连接表
type UserModel struct {
ID uint
Name string
Collects []ArticleModel `gorm:"many2many:user_collect_models;joinForeignKey:UserID;JoinReferences:ArticleID"`
}
type ArticleModel struct {
ID uint
Title string
// 这里也可以反向引用,根据文章查哪些用户收藏了
}
// UserCollectModel 用户收藏文章表
type UserCollectModel struct {
UserID uint `gorm:"primaryKey"` // article_id
ArticleID uint `gorm:"primaryKey"` // tag_id
CreatedAt time.Time
}
func main() {
DB.SetupJoinTable(&UserModel{}, "Collects", &UserCollectModel{})
err := DB.AutoMigrate(&UserModel{}, &ArticleModel{}, &UserCollectModel{})
fmt.Println(err)
}
常用的操作就是根据用户查收藏的文章列表
var user UserModel
DB.Preload("Collects").Take(&user, "name = ?", "枫枫")
fmt.Println(user)
但是这样不太好做分页,并且也拿不到收藏文章的时间
var collects []UserCollectModel
DB.Find(&collects, "user_id = ?", 2)
fmt.Println(collects)
这样虽然可以查到用户id,文章id,收藏的时间,但是搜索只能根据用户id搜,返回也拿不到用户名,文章标题等
我们需要改一下表结构,不需要重新迁移,加一些字段
type UserModel struct {
ID uint
Name string
Collects []ArticleModel `gorm:"many2many:user_collect_models;joinForeignKey:UserID;JoinReferences:ArticleID"`
}
type ArticleModel struct {
ID uint
Title string
}
// UserCollectModel 用户收藏文章表
type UserCollectModel struct {
UserID uint `gorm:"primaryKey"` // article_id
UserModel UserModel `gorm:"foreignKey:UserID"`
ArticleID uint `gorm:"primaryKey"` // tag_id
ArticleModel ArticleModel `gorm:"foreignKey:ArticleID"`
CreatedAt time.Time
}
查询
var collects []UserCollectModel
var user UserModel
DB.Take(&user, "name = ?", "枫枫")
// 这里用map的原因是如果没查到,那就会查0值,如果是struct,则会忽略零值,全部查询
DB.Debug().Preload("UserModel").Preload("ArticleModel").Where(map[string]any{"user_id": user.ID}).Find(&collects)
for _, collect := range collects {
fmt.Println(collect)
}
自定义的数据类型必须实现 Scanner 和 Valuer 接口,以便让 GORM 知道如何将该类型接收、保存到数据库
存储结构体
type Info struct {
Status string `json:"status"`
Addr string `json:"addr"`
Age int `json:"age"`
}
// Scan 从数据库中读取出来
func (i *Info) Scan(value interface{}) error {
bytes, ok := value.([]byte)
if !ok {
return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
}
info := Info{}
err := json.Unmarshal(bytes, &info)
*i = info
return err
}
// Value 存入数据库
func (i Info) Value() (driver.Value, error) {
return json.Marshal(i)
}
type User struct {
ID uint
Name string
Info Info `gorm:"type:string"`
}
添加和查询
DB.Create(&User{
Name: "枫枫",
Info: Info{
Status: "牛逼",
Addr: "成都市",
Age: 21,
},
})
var user User
DB.Take(&user)
fmt.Println(user)
枚举类型
枚举1.0
很多时候,我们会对一些状态进行判断,而这些状态都是有限的
例如,主机管理中,状态有 Running 运行中, OffLine 离线, Except 异常
如果存储字符串,不仅是浪费空间,每次判断还要多复制很多字符,最主要是后期维护麻烦
type Host struct {
ID uint
Name string
Status string
}
func main() {
host := Host{}
if host.Status == "Running" {
fmt.Println("在线")
}
if host.Status == "Except" {
fmt.Println("异常")
}
if host.Status == "OffLine" {
fmt.Println("离线")
}
}
后来,我们知道了用常量存储这些不变的值
type Host struct {
ID uint
Name string
Status string
}
const (
Running = "Running"
Except = "Except"
OffLine = "OffLine"
)
func main() {
host := Host{}
if host.Status == Running {
fmt.Println("在线")
}
if host.Status == Except {
fmt.Println("异常")
}
if host.Status == OffLine {
fmt.Println("离线")
}
}
虽然代码变多了,但是维护方便了
但是数据库中存储的依然是字符串,浪费空间这个问题并没有解决
枚举2.0
于是想到使用数字表示状态
type Host struct {
ID uint
Name string
Status int
}
const (
Running = 1
Except = 2
OffLine = 3
)
func main() {
host := Host{}
if host.Status == Running {
fmt.Println("在线")
}
if host.Status == Except {
fmt.Println("异常")
}
if host.Status == OffLine {
fmt.Println("离线")
}
}
但是,如果返回数据给前端,前端接收到的状态就是数字,不过问题不大,前端反正都要搞字符映射的
因为要做颜色差异显示
但是这并不是后端偷懒的理由
于是我们想到,在json序列化的时候,根据映射转换回去
type Host struct {
ID uint `json:"id"`
Name string `json:"name"`
Status int `json:"status"`
}
func (h Host) MarshalJSON() ([]byte, error) {
var status string
switch h.Status {
case Running:
status = "Running"
case Except:
status = "Except"
case OffLine :
status = "OffLine"
}
return json.Marshal(&struct {
ID uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
}{
ID: h.ID,
Name: h.Name,
Status: status,
})
}
const (
Running = 1
Except = 2
OffLine = 3
)
func main() {
host := Host{1, "枫枫", Running}
data, _ := json.Marshal(host)
fmt.Println(string(data)) // {"id":1,"name":"枫枫","status":"Running"}
}
这样写确实可以实现我们的需求,但是根本就不够通用,凡是用到枚举,都得给这个Struct实现MarshalJSON
方法
枚举3.0
于是类型别名出来了
type Status int
func (status Status) MarshalJSON() ([]byte, error) {
var str string
switch status {
case Running:
str = "Running"
case Except:
str = "Except"
case OffLine:
str = "Status"
}
return json.Marshal(str)
}
type Host struct {
ID uint `json:"id"`
Name string `json:"name"`
Status Status `json:"status"`
}
const (
Running Status = 1
Except Status = 2
OffLine Status = 3
)
func main() {
host := Host{1, "枫枫", Running}
data, _ := json.Marshal(host)
fmt.Println(string(data)) // {"id":1,"name":"枫枫","status":"Running"}
}
嗯,代码简洁了不少,在使用层面已经没有问题了
但是,这个结构体怎么表示数据库中的字段呢?
golang中没有枚举
我们只能自己通过逻辑实现枚举
type Weekday int
const (
Sunday Weekday = iota + 1 // EnumIndex = 1
Monday // EnumIndex = 2
Tuesday // EnumIndex = 3
Wednesday // EnumIndex = 4
Thursday // EnumIndex = 5
Friday // EnumIndex = 6
Saturday // EnumIndex = 7
)
var WeekStringList = []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}
var WeekTypeList = []Weekday{Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday}
// String 转字符串
func (w Weekday) String() string {
return WeekStringList[w-1]
}
// MarshalJSON 自定义类型转换为json
func (w Weekday) MarshalJSON() ([]byte, error) {
return json.Marshal(w.String())
}
// EnumIndex 自定义类型转原始类型
func (w Weekday) EnumIndex() int {
return int(w)
}
// ParseWeekDay 字符串转自定义类型
func ParseWeekDay(week string) Weekday {
for i, i2 := range WeekStringList {
if week == i2 {
return WeekTypeList[i]
}
}
return Monday
}
// ParseIntWeekDay 数字转自定义类型
func ParseIntWeekDay(week int) Weekday {
return Weekday(week)
}
type DayInfo struct {
Weekday Weekday `json:"weekday"`
Date time.Time `json:"date"`
}
func main() {
w := Sunday
fmt.Println(w)
dayInfo := DayInfo{Weekday: Sunday, Date: time.Now()}
data, err := json.Marshal(dayInfo)
fmt.Println(string(data), err)
week := ParseWeekDay("Sunday")
fmt.Println(week)
week = ParseIntWeekDay(2)
fmt.Println(week)
}
在需要输出的时候(print,json),自定义类型就变成了字符串
从外界接收的数据也能转换为自定义类型,这就是golang中的枚举,假枚举
事务就是用户定义的一系列数据库操作,这些操作可以视为一个完成的逻辑处理工作单元,要么全部执行,要么全部不执行,是不可分割的工作单元。
很形象的一个例子,张三给李四转账100元,在程序里面,张三的余额就要-100,李四的余额就要+100 整个事件是一个整体,哪一步错了,整个事件都是失败的
gorm事务默认是开启的。为了确保数据一致性,GORM 会在事务里执行写入操作(创建、更新、删除)。
如果没有这方面的要求,您可以在初始化时禁用它,这将获得大约 30%+ 性能提升。
一般不推荐禁用
// 全局禁用
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
SkipDefaultTransaction: true,
})
本节课表结构
type User struct {
ID uint `json:"id"`
Name string `json:"name"`
Money int `json:"money"`
}
// InnoDB引擎才支持事务,MyISAM不支持事务
// DB.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{})
普通事务
以张三给李四转账为例,不使用事务的后果
var zhangsan, lisi User
DB.Take(&zhangsan, "name = ?", "张三")
DB.Take(&lisi, "name = ?", "李四")
// 张三给李四转账100元
// 先给张三-100
zhangsan.Money -= 100
DB.Model(&zhangsan).Update("money", zhangsan.Money)
// 模拟失败的情况
// 再给李四+100
lisi.Money += 100
DB.Model(&lisi).Update("money", lisi.Money)
在失败的情况下,要么张三白白损失了100,要么李四凭空拿到100元
这显然是不合逻辑的,并且不合法的
那么,使用事务是怎样的
var zhangsan, lisi User
DB.Take(&zhangsan, "name = ?", "张三")
DB.Take(&lisi, "name = ?", "李四")
// 张三给李四转账100元
DB.Transaction(func(tx *gorm.DB) error {
// 先给张三-100
zhangsan.Money -= 100
err := tx.Model(&zhangsan).Update("money", zhangsan.Money).Error
if err != nil {
fmt.Println(err)
return err
}
// 再给李四+100
lisi.Money += 100
err = tx.Model(&lisi).Update("money", lisi.Money).Error
if err != nil {
fmt.Println(err)
return err
}
// 提交事务
return nil
})
使用事务之后,他们就是一体,一起成功,一起失败
手动事务
// 开始事务
tx := db.Begin()
// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
tx.Create(...)
// ...
// 遇到错误时回滚事务
tx.Rollback()
// 否则,提交事务
tx.Commit()
刚才的代码也可以这样实现
var zhangsan, lisi User
DB.Take(&zhangsan, "name = ?", "张三")
DB.Take(&lisi, "name = ?", "李四")
// 张三给李四转账100元
tx := DB.Begin()
// 先给张三-100
zhangsan.Money -= 100
err := tx.Model(&zhangsan).Update("money", zhangsan.Money).Error
if err != nil {
tx.Rollback()
}
// 再给李四+100
lisi.Money += 100
err = tx.Model(&lisi).Update("money", lisi.Money).Error
if err != nil {
tx.Rollback()
}
// 提交事务
tx.Commit()