gorm(goWeb第二期)

p.s.这是萌新自己自学总结的笔记,如果想学习得更透彻的话还是请去看大佬的讲解

目录

ORM

什么是 ORM

ORM 是 Object Relational Mapping 的缩写,译为"对象关系映射",它解决了对象和关系型数据库之间的数据交互问题。

简单说就是 使用一个类表示一张表,类中的属性表示表的字段,类的实例化对象表示一条记录

使用对象的方法操作数据库

每个语言都有自己流行的 orm 框架

python: SQLAlchemy DjangoORM

Java: Hibernate Mybatis

Golang: GORM
和自动生成 SQL 语句相比,手动编写 SQL 语句的缺点是非常明显的,主要体现在以下两个方面:

• 对象的属性名和数据表的字段名往往不一致,我们在编写 SQL 语句时需要非常小心,要逐一核对属性名和字段名,确保它们不会出错,而且彼此之间要一一对应。

• 此外,当 SQL 语句出错时,数据库的提示信息往往也不精准,这给排错带来了不小的困难

• 不同的数据库,对应的 sql 语句也不一样

• sql 注入问题

使用

go 复制代码
package main

import (
	_ "github.com/go-sql-driver/mysql"
	"database/sql"
	"log"
	"fmt"
)


func main() {
	//go在docker中连接宿主机的mysql数据库,使用host.docker.internal
	dsn := "root:password@tcp(host.docker.internal:3306)/qill7?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		log.Fatalln("dsn格式错误",err)
	}
	err2 := db.Ping()
	if err2 != nil {
		log.Fatalln("数据库连接失败",err2)
	}
	fmt.Println(db)
	fmt.Println("数据库连接成功")

	// res,err := db.Exec("SELECT * FROM user")
	// if err != nil {
	// 	log.Fatalln("执行SQL失败",err)
	// }
	// fmt.Println(res)

	rows,err := db.Query("SELECT * FROM user")
	if err != nil {
		log.Fatalln("执行SQL失败",err)
	}
	for rows.Next() {
		var id int
		var name string
		var gender string
		var age int
		var phone string
		rows.Scan(&id, &name, &gender, &age, &phone)
		fmt.Println(id, name,gender,age,phone)
	}
}

gorm 的引入肯定是为了我们更好的操作数据库

在使用原生 sql 查询的时候

最大的问题就是没办法映射到结构体上

我们不难发现,数据库里面的记录就很想一个结构体,数据库的一列对应一个字段

所以数据库到结构体的映射就是 orm 的一个最大特点

所有的 orm 都是如此

使用一个类或者一个结构体表示一张表

然后使用一个实例化对象表示一条记录,字段表示列

go 复制代码
package main

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

type User struct {
	ID string 
	Name string
	Gender string
	Age int
	Phone string
}

//自定义表名
func (User) TableName() string {
	return "user"
}

func main() {
	//go在docker中连接宿主机的mysql数据库,使用host.docker.internal
	dsn := "root:password@tcp(host.docker.internal:3306)/qill7?charset=utf8mb4&parseTime=True&loc=Local"
	db,err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		log.Fatalln("数据库连接失败",err)
	}
	//fmt.Println(db)

	var userList []User
	db.Find(&userList)
	fmt.Println(userList)
}

单表CRUD

在 gorm 中,使用结构体表示一张表

例如 User 表

通常情况下,我们会使用 Model 结尾表示它是表的结构体

go 复制代码
type UserModel struct {
    ID        int64     `gorm:"primaryKey"` // 主键
    Name      string    `gorm:"not null;unique"` // 不能为空,且唯一
    CreatedAt time.Time // 在创建记录时自动设置为当前时间
}

根据 Go 的结构体定义,自动在数据库中创建(或更新)对应的数据表。

go 复制代码
package main

import (
	"golang/global"
	"fmt"
	"time"
)

type UserModel struct {
    ID        int64     `gorm:"primaryKey"` // 主键
    Name      string    `gorm:"not null;unique"` // 不能为空,且唯一
    CreatedAt time.Time // 在创建记录时自动设置为当前时间
}

//自定义表名
func (UserModel) TableName() string {
	return "UserModel"
}

func migrate() {
	err := global.DB.AutoMigrate(&UserModel{})
	if err != nil {
		fmt.Println("数据库迁移失败")
		return
	} else {
		fmt.Println("数据库迁移完成")
	}
}

//连接数据库,并根据 Go 的结构体定义,自动在数据库中创建(或更新)对应的数据表。
//生成表结构只会生成字段,不会删除或更新字段
func main() {
	global.Connet()
	//fmt.Println(global.DB)
	migrate()
	
}

插入

go 复制代码
func main() {
	global.Connet()
	//fmt.Println(global.DB)
	global.Migrate()


	var userList =  []global.UserModel{
		{Name: "Alice"},
		{Name: "Bob"},
		{Name: "Charlie"},
	}

	//Create函数传入的是指针或切片
	err := global.DB.Create(&userList).Error
	if err != nil {
		fmt.Println("创建用户失败", err)
	} else {
		fmt.Println("创建用户成功")
	}
}

查询

go 复制代码
func main() {
	global.Connet()
	
	// global.Migrate()

// // 查全部
// var userList []global.UserModel
// global.DB.Find(&userList)
// fmt.Println(userList)

// // 带条件查询
// var user []global.UserModel
// global.DB.Find(&user, "name = ?", "Bob")
// fmt.Println(user)

// // 获取一条记录
// var userList []global.UserModel
// global.DB.Take(&userList)
// fmt.Println(userList)

// // 如果查不到则会报错
// var user []global.UserModel
// err := global.DB.Take(&user, "id = ?", 100).Error
// if err == gorm.ErrRecordNotFound {
//     fmt.Println("不存在的记录")
// }

// // 如果不想出现错误
// var user []global.UserModel
// var err = global.DB.Limit(1).Find(&user, "id = ?", 100).Error
// fmt.Println(err)

// 打印实际的sql
var user []global.UserModel
global.DB.Debug().Take(&user, "id = ?", 1)

}

条件查询

go 复制代码
func main() {
	global.Connet()
	
	// global.Migrate()

// //Where查询;注意 where 的顺序,再&函数的前面
// var user global.UserModel
// global.DB.Debug().Where("id > ?",  1).Take(&user)
// fmt.Println(user)

// //结构体条件;只查询非空字段
// var user global.UserModel
// global.DB.Debug().Where(global.UserModel{
//     Name: "Alice",
//     ID:  2,
// }).Take(&user)
// fmt.Println(user)


//Where嵌套查询
// query := global.DB.Where("age > ?", 18)
// var user global.UserModel
// global.DB.Debug().Where(query).Take(&user)
// fmt.Println(user)

//Or或查询
// var userList []global.UserModel
// global.DB.Debug().Or("name = ?", "Alice").Or("ID = 4").Find(&userList)
// fmt.Println(userList)

//Not非查询
// var userList []global.UserModel
// global.DB.Debug().Not("name = ?", "Alice").Find(&userList)
// fmt.Println(userList)

//升降序查询
var userList []global.UserModel
// 降序
global.DB.Order("ID desc").Find(&userList)
fmt.Println(userList)
// 升序
global.DB.Order("ID asc").Find(&userList)
fmt.Println(userList)
}

保存查询的数据

go 复制代码
//将name保存到切片中
var nameList []string
DB.Model(UserModel{}).Select("name").Scan(&nameList)
fmt.Println(nameList)


var nameList []string
DB.Model(UserModel{}).Pluck("name", &nameList)
fmt.Println(nameList)

//使用结构体存储
var UserList []User
DB.Model(UserModel{}).Pluck("name", &nameList)
fmt.Println(nameList)

分组、去重、分页查询

go 复制代码
//分组
DB.Model(UserModel{}).Group("age").Select("age, count(*) as count").Scan(&us)
fmt.Println(us)

//去重
var nameList []string
DB.Model(UserModel{}).Distinct("name").Select("name").Scan(&nameList)
fmt.Println(nameList)

//分页查询
var users []UserModel
// 第一页
DB.Limit(10).Offset(0).Find(&users)
fmt.Println(users)
// 第二页
DB.Limit(10).Offset(10).Find(&users)
fmt.Println(users)
// 第三页
DB.Limit(10).Offset(20).Find(&users)
fmt.Println(users)

抽离查询方法

很多时候,我们会经常使用重复的查询方法

那么就可以把这些可能被复用的方法抽离出来

go 复制代码
func Age18(tx gorm.DB) gorm.DB {
	return tx.Where("age >= ?", 18)
}

var users []UserModel
DB.Scopes(Age18).Find(&users)
fmt.Println(users)

更新

有很多方法, Save、Update、UpdateColumn、Updates

不同的方法有不同的区别, 如下:

  1. Save, 有主键记录就是更新值, 否则就是创建
  2. Update, 可以更新零值, 必须要有条件
  3. UpdateColumn, 可以更新零值, 不会走更新的 Hook
  4. Updates, 如果是结构体, 则更新非零值, map 可以更新零值
go 复制代码
func main() {
	global.Connet()
	
	// global.Migrate()

// var user global.UserModel
// user.ID = 3
// user.Name = "张三"
// user.CreatedAt = time.Now()
// global.DB.Save(&user)
// fmt.Println(user)


global.DB.Model(&global.UserModel{}).Where("id = ?", 3).Update("name", "李四")

}

删除

go 复制代码
func main() {
	global.Connet()
	
	// global.Migrate()

   //var user =  global.UserModel{ID:3}
   // global.DB.Delete(&user)
//    global.DB.Delete(global.UserModel{}, "name = ?", "zyj")
//    global.DB.Delete(global.UserModel{}, []int{1,2})

}

软删除

如果你的模型包含了 gorm.DeletedAt 字段(该字段也被包含在 gorm.Model 中),那么该模型将会自动获得软删除的能力

当调用 Delete 时,GORM 并不会从数据库中删除该记录,而是将该记录的 DeletedAt 设置为当前时间,而后的一般查询方法将无法查找到此条记录。

查找被删除的记录
DB.Unscoped().Find(&users)

永久删除
db.Unscoped().Delete(&user)

表关系

一对一

go 复制代码
type UserModel struct {
	ID        int       gorm:"size:16; not null;unique"
	Name      string    gorm:"size:16; not null;unique"
	Age       int       gorm:"default:18"
	CreatedAt time.Time
	DeletedAt gorm.DeletedAt
}

type UserDetailModel struct {
	ID         int
	UserID     int       gorm:"unique"//一对一情况下要加唯一约束
	UserModel  UserModel gorm:"foreignKey:UserID"
	Email      string    gorm:"size:32"
}

//禁止生成实体外键
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
    DisableForeignKeyConstraintWhenMigrating: true,
})

插入与查询

go 复制代码
func oneToOne() {
// 	// 实现一对一关联查询的逻辑
// 	// 创建用户,连带着创建用户详情
// err := global.DB.Create(&global.UserModel{
// Name: "张三",
// Age: 18,
// UserDetailModel: &global.UserDetailModel{
// Email: "123456@qq.com",
// },
// })
// fmt.Println(err)



// // 创建用户详情,关联用户
// err := global.DB.Create(&global.UserDetailModel{
//     Email:      "654321@qq.com",
//     UserModel: &global.UserModel{ID: 115},
//     //UserID: 115,
// }).Error
// fmt.Println(err)


// 通过用户详情查用户 正查
//Preload的作用是预加载关联数据,简单来说就是:查主表的时候,顺便把关联表的数据也查出来。
var id = 1
var detail global.UserDetailModel
global.DB.Preload("UserModel").Take(&detail, "user_id = ?", id)
fmt.Println(detail.Email, detail.UserModel.Name)

// 反查
var user global.UserModel
global.DB.Preload("UserDetailModel").Take(&user, id)
fmt.Println(user.Name, user.UserDetailModel.Email)
}

func main() {
	global.Connet()
	oneToOne()
	//global.Migrate()

}

删除

go 复制代码
func oneToOne() {
// // 级联删除
// var user global.UserModel
// global.DB.Unscoped().Take(&user, 1)
// //global.DB.Unscoped().Delete(&user)  有外键会报错
// global.DB.Unscoped().Select( "UserDetailModel").Delete(&user)

// 先将外键set null在删除,不会删除外键的数据
var user global.UserModel
global.DB.Unscoped().Take(&user,3)
global.DB.Model(&user).Association( "UserDetailModel").Clear()
global.DB.Unscoped().Delete(&user)
 }

一对多

go 复制代码
type TeacherModel struct {
	ID          int
	Name        string `gorm:"size:16"`
	StudentList []StudentModel `gorm:"foreignKey:TeacherID;references:ID"`
}

type StudentModel struct {
	ID            int
	Name          string `gorm:"size:16"`
	TeacherID     int
	TeacherModel  TeacherModel `gorm:"foreignKey:TeacherID;references:ID"`
}

增加与查询

go 复制代码
func oneToMany() {
// 	//增加
// err := global.DB.Create(&global.TeacherModel{
// 	Name: "张老师",
// 	StudentList: []global.StudentModel{
// 		{Name: "小明"},
// 		{Name: "小红"},
// 	},
// }).Error
// if (err != nil) {
// 		fmt.Println("增加失败", err)
// 	}

var teacher global.TeacherModel
//查询
global.DB.Preload("StudentList").Take(&teacher, 1)
fmt.Printf("查询结果: ", teacher.Name,teacher.StudentList,len(teacher.StudentList))
}

关联性操作

go 复制代码
//更换关联对象Replace,添加关联对象Append,删除关联对象Delete
teacher := models.TeacherModel{}
global.DB.Take(&teacher, "name = ?", "张老师")
global.DB.Model(&teacher).Association("StudentList").Replace([]models.StudentModel{
	{ID: 3},
})

//取消关联性
teacher := models.TeacherModel{}
global.DB.Take(&teacher, "name = ?", "小张")
global.DB.Model(&teacher).Association("StudentList").Clear()

多对多

go 复制代码
type ArticleModel struct {
	ID       int
	Title    string `gorm:"size:32"`
	TagList  []TagModel `gorm:"many2many:article_tags;"`
}

type TagModel struct {
	ID          int
	Title       string `gorm:"size:32"`
	ArticleList []ArticleModel `gorm:"many2many:article_tags;"`//gorm自动维护表
}

增删改查

go 复制代码
func ManyToMany() {
// 创建一篇文章,新增tag
// err := global.DB.Create(&global.ArticleModel{
// 	Title: "文章1",
// 	TagList: []global.TagModel{
// 		{Title: "go"},
// 		{Title: "python"},
// 	},
// }).Error
// fmt.Println(err)


// 创建一篇文章,选择tag
// var tagIDList = []int{2}
// var tagList []global.TagModel
// global.DB.Find(&tagList, "id in ?", tagIDList)
// err := global.DB.Create(&global.ArticleModel{
// 	Title: "文章2",
// 	TagList:tagList,
// }).Error
// fmt.Println(err)


// 查文章的时候,把对应的标签带出来
// var articleList []global.ArticleModel
// global.DB.Preload("TagList").Find(&articleList)
// fmt.Println(articleList)
// }

// 将文章2的标签更新为1,2
var article global.ArticleModel
global.DB.Take(&article, "id = ?", 2)

global.DB.Model(&article).Association("TagList").Replace([]global.TagModel{
	{ID: 1},
	{ID: 2},
})}

自定义第三张表

go 复制代码
type User1Model struct {
	ID            int64
	Name          string
	CollArticleList []Article1Model `gorm:"many2many:user2_article_models;joinForeignKey:UserID;JoinReferences:ArticleID"`
}

type Article1Model struct {
	ID    int64
	Title string `gorm:"size:32"`
	//CollUserList  []User1Model `gorm:"many2many:user2_article_models;joinForeignKey:ArticleID;JoinReferences:UserID"`
}

type User2ArticleModel struct {
	UserID     int64         `gorm:"primaryKey"`
	UserModel  User1Model    `gorm:"foreignKey:UserID"`
	ArticleID  int64         `gorm:"primaryKey"`
	ArticleModel Article1Model `gorm:"foreignKey:ArticleID"`
	CreatedAt  time.Time     `json:"createdAt"`
	Title string `gorm:"size:32" json:"title"`
}

func (User2ArticleModel) TableName() string {
	return "user2_article_models"
}

func Migrate() {
	//注意!!
	// 必须要加这个才会走第三张表的创建钩子
	DB.SetupJoinTable(&User1Model{}, "CollArticleList", &User2ArticleModel{})
	err := DB.AutoMigrate(&Article1Model{}, &User1Model{}, &User2ArticleModel{})
	if err != nil {
		fmt.Println("数据库迁移失败")
		return
	} else {
		fmt.Println("数据库迁移完成")
	}
}
go 复制代码
func ManyToMany() {
	// 创建用户,连带创建文章
// err := global.DB.Create(&global.User1Model{
// 	Name: "张三",
// 	CollArticleList: []global.Article1Model{
// 		{Title: "Go笔记"},
// 		{Title: "Python笔记"},
// 	},
// }).Error
// fmt.Println(err)


//自定义查询
type UserCollArticleResponse struct {
	Name        string
	UserID      int64
	ArticleTitle string
	ArticleID   int64
	Date        time.Time
}

var userID = 1
var userArticleList []global.User2ArticleModel
var collList []UserCollArticleResponse
global.DB.Preload("UserModel").Preload("ArticleModel").Find(&userArticleList, "user_id = ?", userID)
for _, model := range userArticleList {
	collList = append(collList, UserCollArticleResponse{
		Name:        model.UserModel.Name,
		UserID:      model.UserID,
		ArticleTitle: model.ArticleModel.Title,
		ArticleID:   model.ArticleID,
		Date:        model.CreatedAt,
	})
}
fmt.Println(collList)

}

自定义数据类型

很多情况下,我们有存 json 的需求

那么就需要使用到 gorm 中的自定义数据类型了

自定义的数据类型必须实现 Scanner 和 Valuer 接口

简单说就是,在入库的时候转换一下,变成普通字符串进行存储,查询的时候,映射为结构体

go 复制代码
type Info struct {
	Like []string `json:"like"`
	Addr string   `json:"addr"`
	Age  int      `json:"age"`
}

// Scan 实现 sql.Scanner 接口, Scan 将 value 扫描至 Jsonb
func (j *Info) Scan(value interface{}) error {
	bytes, ok := value.([]byte)
	if !ok {
		return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
	}

	result := Info{}
	err := json.Unmarshal(bytes, &result)
	*j = result
	return err
}

// Value 实现 driver.Valuer 接口, Value 返回 json value
func (j Info) Value() (driver.Value, error) {
	return json.Marshal(j)
}

如果只是存储json数据,完全可以使用 gorm 的序列化,自带 json

go 复制代码
type User struct {
	ID   int64
	Name string `gorm:"size:32"`
	Info Info   `gorm:"type:longtext;serializer:json" json:"info"`
}

有些情况下,在数据库里面需要存储一些标识状态的内容

比如描述日志的等级,通常有固定的那么几个值,例如 info、warning、er

如果直接存储字符串,很明显是浪费空间

所以可以使用自定义类型

go 复制代码
type Status int8

const (
	Running Status = 1
	Warning Status = 2
)

func (s Status) MarshalJSON() (data []byte, err error) {
	var str string
	switch s {
	case Running:
		str = "运行中"
	case Warning:
		str = "警告"
	}
	return json.Marshal(str)
}

事务

事务就是用户定义的一系列数据库操作,这些操作可以视为一个完成的逻辑处理工作单元,要么全部执行,要么全部不执行,是不可分割的工作单元,

张三给李四转账 100 元,在程序里面,张三的余额就要-100,李四的余额就要+100 整个事件是一个整体,哪一步错了,整个事件都是失败的

gorm 事务默认是开启的。为了确保数据一致性,GORM 会在事务里执行写入操作(创建、更新、删除)。

go 复制代码
err := global.DB.Transaction(func(tx *gorm.DB) error {
	err := tx.Model(&zhangsan).Update(column:"money", gorm.Expr(expr:"money - 100"))
	err = errors.New(text:"出错了")
	if err != nil {
		return err
	}
	tx.Model(&lisi).Update(column:"money", gorm.Expr(expr:"money + 100"))
	return nil
})
fmt.Println(err)
go 复制代码
//手动事务
// 开始事务
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()
}

gorm配置使用 ID 为主键

默认情况下,名为 ID 的字段会作为表的主键;也可以通过标签 primaryKey 将其它字段设为主键


复数表名

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

如果不想用复数表名,可以在配置中修改

go 复制代码
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
    NamingStrategy: schema.NamingStrategy{
        TablePrefix: "t_",   // 表名前缀
        SingularTable: true, // 使用单数表名
        NoLowerCase: true,   // 关闭蛇形命名
        NameReplacer: strings.NewReplacer("CID", "Cid"), // use name replacer to change struct/field name before convert it to db name
    },
})

也可以使用 TableName 函数进行自定义表名

go 复制代码
func (User) TableName() string {
	return "profiles"
}

蛇形列名

使用 gorm:"column:name" 进行自定义列名


特殊的时间字段:

CreatedAt

对于有 CreatedAt 字段的模型,创建记录时,如果该字段值为零值,则将该字段的值设为当前时间

UpdatedAt

对于有 UpdatedAt 字段的模型,更新记录时,将该字段的值设为当前时间。创建记录时,如果该字段值为零值,则将该字段的值设为当前时间

DeletedAt

软删除

更多配置

相关推荐
GoFly开发者2 天前
好消息!Gin+GORM-Gen开发框架已集成完成,正在进行测试和编写使用文档中,需要的开发朋友可以等待使用及订阅哦
gin·gorm·gorm-gen
有浔则灵20 天前
GORM 日志与调试完全指南:从基础配置到生产实践
服务器·数据库·gorm
有浔则灵1 个月前
GORM 关联关系完全指南:从入门到精通
mysql·gorm
ErizJ2 个月前
面试 | gin gorm go-zero
面试·golang·gin·gorm·gozero
源代码•宸3 个月前
简版抖音项目——项目需求、项目整体设计、Gin 框架使用、视频模块方案设计、用户与鉴权模块方案设计、JWT
经验分享·后端·golang·音视频·gin·jwt·gorm
ZNineSun4 个月前
GORM:Go的ORM 框架
golang·orm·gorm·crud
WongLeer4 个月前
Go + GORM 多级分类实现方案对比:内存建树、循环查询与 Preload
开发语言·后端·mysql·golang·gorm
半桶水专家5 个月前
GORM 结构体字段标签(Struct Tags)详解
golang·go·gorm
唐僧洗头爱飘柔95276 个月前
【GORM(3)】Go的跨时代ORM框架!—— 数据库连接、配置参数;本文从0开始教会如何配置GORM的数据库
开发语言·数据库·后端·golang·gorm·orm框架·dsn