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
不同的方法有不同的区别, 如下:
- Save, 有主键记录就是更新值, 否则就是创建
- Update, 可以更新零值, 必须要有条件
- UpdateColumn, 可以更新零值, 不会走更新的 Hook
- 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
软删除