目录
01. 什么是ORM
02. 环境搭建
go mod init 文件名
go get gorm.io/driver/mysql //mysql的驱动
go get "gorm.io/gorm"
03. 连接数据库
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// 定义一个全局变量db,用于后面数据库的读写操作,通常就放在全局里面
var DB *gorm.DB
func init() {
username := "root" //账号
password := "password" //密码
host := "IP" //数据库地址
port := "3306" //端口
Dnname := "dtbase" //数据库名
timeout := "10s" //连接超时,10s
//root:root@tcp(127.0.0.1:3306)/test?
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?timeout=%s", username, password, host, port, Dnname, timeout)
//连接mysql,获得DB类型实例,用于后面数据库的读写操作
db, err := gorm.Open(mysql.Open(dsn))
if err != nil {
panic("连接数据库失败,error=" + err.Error())
}
DB = db
//连接成功
fmt.Println("连接数据库成功")
}
func main() {
}
高级设置
是在open的地方设置
gorm 的命名策略
创建表
注意虽然我们的结构为Student,但是表名却是studens,并且字段全小写,至于为什么是这样,就是我们上面gorm命名规则的原因了
日志显示
gorm默认日志是只打印错误和慢sql,我们可以设置日志的显示等级
可以设置日志等级为info,但是这样显示会很占用空间
推荐:
04. 模型定义
模型是标准的 struct ,由Go的基本数据类型,实现了Scanner和Valuer接口的自定义类型及其指针或别名组成
定义一张表
PS:小写属性是不会生成字段的
自动生成表结构
AutoMigrate的逻辑是只新增,不擅长,不修改(大小会修改)
例如将 Name 修改为 Name1,进行迁移,会多出一个name1的字段
新增Email和改Name为Name1
修改表字段大小
有两种方式
下面那种样例
字段标签
05. 单表查询
5.1 表结构
5.2 添加单条记录
添加记录就是实例化结构体
有两条是因为点了两下哈
如果此时
那么他们分别是空字符串和null
这就是指针的好处,当然email也可以直接传nil因为他是指针
还可以打印s1看看
5.3 批量插入
Creat方法还可以用于插入多条记录
5.4 单条数据查询
会查询第一个数据
Take就是传统的:SELECT * FROM 'students' LIMIT 1
而Frist和Last是按照主键去查询的:SELECT * FROM 'students' ORDER BY 'studens'.'id' LIMIT 1
5.5 根据主键查询
注意这是前端传进来的话一定要用?拼接,不能fprintf,这样可以有效防止sql注入
5.6 根据结构体进行查询
但是注意只能根据主键查询
5.7 获取查询的结构
5.8 查询多条记录并返回Json数据
5.9 根据主键列表去查询
根据其他条件就可以
DB.Find(&studentList,"name in ?",[]string{"yuanlai","chenchen"})
06. 更新数据
有三个操作可以进行,Save,Uptate,Updates
6.1 Save 保存所有字段
用于单个记录的全字段更新,他会保存所有的字段,即使零值也会保存
相当于:UPDATE 'students_two' SET 'name'='y145','age'=23,'gender'=true,'emaI'=2777137742@qq.com WHERE 'id'=1
Select更新指定字段
6.2 Update批量更新
或者Model
6.3 Updates更新
可以传结构体
也可以传map
07. 删除数据
08. 添加钩子函数(HOOK)
比如再插入一条数据之前,我想要做一点事情
其实就是实现一个 BeforeCreate 的方法
在实现BeforeCreate之后
插入这个
09. Gorm高级查询
9.01 Where查询
我们重构一些数据
上面用了一个函数,传string返回他的地址,这样插入的时候好看些
然后正文,这个Where就等价于mysql的where
9.02 Select 选择字段
因为直接Find是select * 比较耗费性能
9.03 排序
9.04 分页查询
先看纯sql的分页查询
9.05 去重
先看sql去重
gorm去重(Scan就是把前面得到的结果给Scan里面的结构体)
9.06 分组查询
sql拼接名字
gorm写法
9.07 gorm执行原生sql
就上面例子来说,就是DB.Raw("saw_sql").Scan(&groupList)
9.08 子查询
子查询就是使用上次查询的结果来作为这次查询的参数
9.09 命名参数
我们之前是?,但是如果查询语句比较多,看的就不直观,orm就可以提供像@name(给参数命名)这样的方式
9.10 从Find到Map
我们每次查询的时候,都要写一个变量去接收查到的值,感觉很麻烦
(之前的 var students []Student,这个students用来Find(&students)传参的)
我们就可以用一个map来接收
emmmmmm好像没什么区别
9.11 查询引用Scope
10. 一对多关系
10.1 表结构建立
对于外键的命名,我们这里就必须要叫做UserID,其他的就不可以
或者不一样的话,我们就需要重写外键关联
重写外键关联
这就要注意,两边都要加上
10.2 添加数据
创建用户的时候创建文章
创建文章的时候,再去关联用户
又或者,但是这样就会又创建一个新的用户去关联他
又或者,不创建user,用已经有的
外键添加
其中Association和Append的方式更常用
10.3 查询数据
预加载
嵌套预加载
带条件的预加载
自定义预加载
10.4 删除数据
级联删除
清除外键关系
11. 一对一关系
表结构搭建
PS:UserInfo里面用指针是因为如果不用,就是和User相互引用了
添加记录
查询
删除
删除和一对多是一样的
12. 多对多关系
多对多关系,需要用第三张表存储两张表的关系
12.1 表结构搭建
12.2 添加
添加文章,并创建标签
查看表
创建文章,添加已有标签
12.3 查询
查询文章,显示文章的标签列表
查询标签,显示文章列表
12.4 更新
之前的方式
gorm提供的方法
让tag1替换为tag2
12.5 多对多自定义连接表(第三张表)
默认的连接表,只有双方的主键id,展示不了更多信息了,比如我们现在想要连接表里面添加数据的时候加上添加的时间,这个时候就需要自定义连接表,这就是他的意义
12.5.1 表结构及生成
注意`form:"many2many:article_tags"` article_tags这个名字是对应ArticleTag的,这个是对应的然后加上 _ 和 s,要是想改gorm里面的名字,那么AticleTag也要改
// 设置Article的Tags表为ArticleTag
DB.SetupJoinTable(&Article{}, "Tags", &ArticleTag{})
// 如果tag要反向应用Article,那么也得加上
// DB.SetupJoinTable(&Tag{}, "Articles", &ArticleTag{})
err := DB.AutoMigrate(&Article{}, &Tag{}, &ArticleTag{})
fmt.Println(err)
12.5.2 操作案例
举一些简单的例子
添加文章并添加标签,并自动关联
添加文章,关联已有标签
给已有文章关联标签
替换已有文章的标签
添加文章并添加标签,并自动关联
1. 添加文章并添加标签,并自动关联
DB.SetupJoinTable(&Article{}, "Tags", &ArticleTag{}) // 要设置这个,才能走到我们自定义的连接表
DB.Create(&Article{
Title: "flask零基础入门",
Tags: []Tag{
{Name: "python"},
{Name: "后端"},
{Name: "web"},
},
})
// CreatedAt time.Time 由于我们设置的是CreatedAt,gorm会自动填充当前时间,
// 如果是其他的字段,需要使用到ArticleTag 的添加钩子 BeforeCreate
2. 添加文章,关联已有标签
DB.SetupJoinTable(&Article{}, "Tags", &ArticleTag{})
var tags []Tag
DB.Find(&tags, "name in ?", []string{"python", "web"})
DB.Create(&Article{
Title: "flask请求对象",
Tags: tags,
})
3. 给已有文章关联标签
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)
4. 替换已有文章的标签
var article Article
var tags []Tag
DB.Find(&tags, "name in ?", []string{"后端"})
DB.Take(&article, "title = ?", "django基础")
DB.Model(&article).Association("Tags").Replace(tags)
5. 查询文章列表,显示标签
var articles []Article
DB.Preload("Tags").Find(&articles)
fmt.Println(articles)
SetupJoinTable
添加和更新的时候得用这个
这样才能走自定义的连接表,以及走它的钩子函数
查询则不需要这个
(如果添加和更新不加,就我们这个情况的话时间就不会添加上去,因为他就不会走SetupJoinTable的那张表,而是默认的那张表,而那张表里面则没有时间,所以不会添加时间,但是不会添加失败)
12.5.3 自定义连接表主键
这个功能还是很有用的,例如你的文章表 可能叫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)
添加,更新,查询操作和上面的都是一样
12.5.4 操作连接表(自定义连接表的时候)
如果通过一张表去操作连接表,这样会比较麻烦
比如查询某篇文章关联了哪些标签
或者是举个更通用的例子,用户和文章,某个用户在什么时候收藏了哪篇文章
无论是通过用户关联文章,还是文章关联用户都不太好查
最简单的就是直接查连接表
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)
}
13. 自定义数据类型
很多情况下我们存储到数据库中的数据是多变的
例如我需要存储json或者是数组
然后很多数据库并不能直接存储这些数据类型,我们就需要自定义数据类型
自定义的数据类型必须实现 Scanner 和 Valuer 接口,以便让 GORM 知道如何将该类型接收、保存到数据库
gorm中自定义数据类型无外乎就两个方法
在数据入库的时候要转换为什么数据,已经出库的时候数据变成什么样子
13.1 存储json
存储json可能是经常使用到的
我们需要定义一个结构体,在入库的时候,把它转换为[]byte类型,查询的时候把它转换为结构体
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))
}
err := json.Unmarshal(bytes, i)
return err
}
// Value 存入数据库
func (i Info) Value() (driver.Value, error) {
return json.Marshal(i)
}
type AuthModel struct {
ID uint
Name string
Info Info `gorm:"type:string"`
}
func main() {
DB.AutoMigrate(&AuthModel{})
}
插入数据
DB.Debug().Create(&AuthModel{
Name: "枫枫",
Info: Info{
Status: "success",
Addr: "湖南省长沙市",
Age: 21,
},
})
// INSERT INTO `auth_models` (`name`,`info`) VALUES ('枫枫','{"status":"success","addr":"湖南省长沙市","age":21}')
查询数据
var auth AuthModel
DB.Take(&auth, "name = ?", "枫枫")
fmt.Println(auth)
13.2 存储数组
很多时候存储数组也是很常见的
最简单的方式就是存json
type Array []string
// Scan 从数据库中读取出来
func (arr *Array) Scan(value interface{}) error {
bytes, ok := value.([]byte)
if !ok {
return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
}
err := json.Unmarshal(bytes, arr)
return err
}
// Value 存入数据库
func (arr Array) Value() (driver.Value, error) {
return json.Marshal(arr)
}
type HostModel struct {
ID uint `json:"id"`
IP string `json:"ip"`
Ports Array `gorm:"type:string" json:"ports"`
}
func main() {
//DB.AutoMigrate(&HostModel{})
//DB.Create(&HostModel{
// IP: "192.168.200.21",
// Ports: []string{"80", "8080"},
//})
var host HostModel
DB.Take(&host, 1)
fmt.Println(host)
}
当然,也可以用字符串拼接,例如 |
, =
, ,
, ;
type Array []string
// Scan 从数据库中读取出来
func (arr *Array) Scan(value interface{}) error {
data, ok := value.([]byte)
if !ok {
return errors.New(fmt.Sprintf("解析失败: %v %T", value, value))
}
*arr = strings.Split(string(data), "|")
return nil
}
// Value 存入数据库
func (arr Array) Value() (driver.Value, error) {
return strings.Join(arr, "|"), nil
}
当然,拼接的字符串不能是输入字符串中存在的
13.3 枚举类型
枚举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"}
}
嗯,代码简洁了不少,在使用层面已经没有问题了
在grom中使用
type Status int
func (s Status) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}
func (s Status) String() string {
var str string
switch s {
case Running:
str = "Running"
case Except:
str = "Except"
case OffLine:
str = "Status"
}
return str
}
const (
Running Status = 1
OffLine Status = 2
Except Status = 3
)
type Host struct {
ID uint `json:"id"`
Status Status `gorm:"size:8" json:"status"`
IP string `json:"ip"`
}
func main() {
//DB.AutoMigrate(&Host{})
//DB.Create(&Host{
// IP: "192.168.200.12",
// Status: Running,
//})
var host Host
DB.Take(&host)
fmt.Println(host)
fmt.Printf("%#v,%T\n", host.Status, host.Status)
data, _ := json.Marshal(host)
fmt.Println(string(data))
}
14. gorm 事务
事务就是用户定义的一系列数据库操作,这些操作可以视为一个完成的逻辑处理工作单元,要么全部执行,要么全部不执行,是不可分割的工作单元。
很形象的一个例子,张三给李四转账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{})
14.1 普通事务
以张三给李四转账为例,不使用事务的后果
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
})
使用事务之后,他们就是一体,一起成功,一起失败
14.2 手动事务
// 开始事务
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()
本篇文章根据 小破站 枫枫知道 所著,也是作者很喜欢的博主哈~
创作不易,希望读者三连支持 💖
赠人玫瑰,手有余香 💖