连接
需要下载mysql的驱动
go get gorm.io/driver/mysql
go get gorm.io/gorm
简单连接
username := "root" //账号
password := "root" //密码
host := "127.0.0.1" //数据库地址,可以是Ip或者域名
port := 3306 //数据库端口
Dbname := "gorm" //数据库名
timeout := "10s" //连接超时,10秒
// root:root@tcp(127.0.0.1:3306)/gorm?
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=%s", username, password, host, port, Dbname, timeout)
//连接MYSQL, 获得DB类型实例,用于后面的数据库读写操作。
db, err := gorm.Open(mysql.Open(dsn))
if err != nil {
panic("连接数据库失败, error=" + err.Error())
}
// 连接成功
fmt.Println(db)
高级配置
跳过默认事务
为了确保数据一致性,GORM 会在事务里执行写入操作(创建、更新、删除)。如果没有这方面的要求,您可以在初始化时禁用它,这样可以获得60%的性能提升
db, err := gorm.Open(mysql.Open("gorm.db"), &gorm.Config{
SkipDefaultTransaction: true,
})
命名策略
gorm采用的命名策略是,表名是蛇形复数,字段名是蛇形单数
例如
type Student struct {
Name string
Age int
MyStudent string
}
gorm会为我们这样生成表结构
CREATE TABLE `students` (`name` longtext,`age` bigint,`my_student` longtext)
我们也可以修改这些策略
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: "f_", // 表名前缀
SingularTable: false, // 单数表名
NoLowerCase: false, // 关闭小写转换
},
})
显示日志
gorm的默认日志是只打印错误和慢SQL
我们可以自己设置
var mysqlLogger logger.Interface
// 要显示的日志等级
mysqlLogger = logger.Default.LogMode(logger.Info)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: mysqlLogger,
})
如果你想自定义日志的显示
那么可以使用如下代码
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // (日志输出的目标,前缀和日志包含的内容)
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info, // 日志级别
IgnoreRecordNotFoundError: true, // 忽略ErrRecordNotFound(记录未找到)错误
Colorful: true, // 使用彩色打印
},
)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger,
})
部分展示日志
var model Student
session := DB.Session(&gorm.Session{Logger: newLogger})
session.First(&model)
// SELECT * FROM `students` ORDER BY `students`.`name` LIMIT 1
如果只想某些语句显示日志
DB.Debug().First(&model)
模型定义
模型是标准的 struct,由 Go 的基本数据类型、实现了 Scanner
和 Valuer
接口的自定义类型及其指针或别名组成
定义一张表
type Student struct {
ID uint // 默认使用ID作为主键
Name string
Email *string // 使用指针是为了存空值
}
常识:小写属性是不会生成字段的
自动生成表结构
// 可以放多个
DB.AutoMigrate(&Student{})
AutoMigrate
的逻辑是只新增,不删除,不修改(大小会修改)
例如将Name修改为Name1,进行迁移,会多出一个name1的字段
生成的表结构如下
CREATE TABLE `f_students` (`id` bigint unsigned AUTO_INCREMENT,`name` longtext,`email` longtext,PRIMARY KEY (`id`))
默认的类型太大了
修改大小
我们可以使用gorm的标签进行修改
有两种方式
Name string `gorm:"type:varchar(12)"`
Name string `gorm:"size:2"`
字段标签
type
定义字段类型
size
定义字段大小
column
自定义列名
primaryKey
将列定义为主键
unique
将列定义为唯一键
default
定义列的默认值
not null
不可为空
embedded
嵌套字段
embeddedPrefix
嵌套字段前缀
comment
注释
多个标签之前用 ;
连接
type StudentInfo struct {
Email *string `gorm:"size:32"` // 使用指针是为了存空值
Addr string `gorm:"column:y_addr;size:16"`
Gender bool `gorm:"default:true"`
}
type Student struct {
Name string `gorm:"type:varchar(12);not null;comment:用户名"`
UUID string `gorm:"primaryKey;unique;comment:主键"`
Info StudentInfo `gorm:"embedded;embeddedPrefix:s_"`
}
// 建表语句
CREATE TABLE `students` (
`name` varchar(12) NOT NULL COMMENT '用户名',
`uuid` varchar(191) UNIQUE COMMENT '主键',
`s_email` varchar(32),
`s_y_addr` varchar(16),
`s_gender` boolean DEFAULT true,
PRIMARY KEY (`uuid`)
)
单表查询
先使用gorm对单张表进行增删改查
表结构
type Student struct {
ID uint `gorm:"size:3"`
Name string `gorm:"size:8"`
Age int `gorm:"size:3"`
Gender bool
Email *string `gorm:"size:32"`
}
添加记录
email := "xxx@qq.com"
// 创建记录
student := Student{
Name: "枫枫",
Age: 21,
Gender: true,
Email: &email,
}
DB.Create(&student)
有两个地方需要注意
- 指针类型是为了更好的存null类型,但是传值的时候,也记得传指针
- Create接收的是一个指针,而不是值
由于我们传递的是一个指针,调用完Create之后,student这个对象上面就有该记录的信息了,如创建的id
DB.Create(&student)
fmt.Printf("%#v\n", student)
// main.Student{ID:0x2, Name:"zhangsan", Age:23, Gender:false, Email:(*string)(0x11d40980)}
批量插入
Create方法还可以用于插入多条记录
var studentList []Student
for i := 0; i < 100; i++ {
studentList = append(studentList, Student{
Name: fmt.Sprintf("机器人%d号", i+1),
Age: 21,
Gender: true,
Email: &email,
})
}
DB.Create(&studentList)
查询单条记录
var student Student
DB.Take(&student)
fmt.Println(student)
获取单条记录的方法很多,我们对比sql就很直观了
DB = DB.Session(&gorm.Session{Logger: Log})
var student Student
DB.Take(&student)
// SELECT * FROM `students` LIMIT 1
DB.First(&student)
// SELECT * FROM `students` ORDER BY `students`.`id` LIMIT 1
DB.Last(&student)
// SELECT * FROM `students` ORDER BY `students`.`id` DESC LIMIT 1
根据主键查询
var student Student
DB.Take(&student, 2)
fmt.Println(student)
student = Student{} // 重新赋值
DB.Take(&student, "4")
fmt.Println(student)
CopyErrorOK!
Take的第二个参数,默认会根据主键查询,可以是字符串,可以是数字
根据其他条件查询
var student Student
DB.Take(&student, "name = ?", "机器人27号")
fmt.Println(student)
CopyErrorOK!
使用?作为占位符,将查询的内容放入?
SELECT * FROM `students` WHERE name = '机器人27号' LIMIT 1
CopyErrorOK!
这样可以有效的防止sql注入
他的原理就是将参数全部转义,如
DB.Take(&student, "name = ?", "机器人27号' or 1=1;#")
SELECT * FROM `students` WHERE name = '机器人27号\' or 1=1;#' LIMIT 1
CopyErrorOK!
根据struct查询
var student Student
// 只能有一个主要值
student.ID = 2
//student.Name = "枫枫"
DB.Take(&student)
fmt.Println(student)
获取查询结果
获取查询的记录数
count := DB.Find(&studentList).RowsAffected
是否查询失败
err := DB.Find(&studentList).Error
查询失败有查询为空,查询条件错误,sql语法错误
可以使用判断
var student Student
err := DB.Take(&student, "xx").Error
switch err {
case gorm.ErrRecordNotFound:
fmt.Println("没有找到")
default:
fmt.Println("sql错误")
}
查询多条记录
var studentList []Student
DB.Find(&studentList)
for _, student := range studentList {
fmt.Println(student)
}
// 由于email是指针类型,所以看不到实际的内容
// 但是序列化之后,会转换为我们可以看得懂的方式
var studentList []Student
DB.Find(&studentList)
for _, student := range studentList {
data, _ := json.Marshal(student)
fmt.Println(string(data))
}
根据主键列表查询
var studentList []Student
DB.Find(&studentList, []int{1, 3, 5, 7})
DB.Find(&studentList, 1, 3, 5, 7) // 一样的
fmt.Println(studentList)
CopyErrorOK!
根据其他条件查询
DB.Find(&studentList, "name in ?", []string{"枫枫", "zhangsan"})
更新
更新的前提的先查询到记录
Save保存所有字段
用于单个记录的全字段更新
它会保存所有字段,即使零值也会保存
var student Student
DB.Take(&student)
student.Age = 23
// 全字段更新
DB.Save(&student)
// UPDATE `students` SET `name`='枫枫',`age`=23,`gender`=true,`email`='xxx@qq.com' WHERE `id` = 1
零值也会更新
var student Student
DB.Take(&student)
student.Age = 0
// 全字段更新
DB.Save(&student)
// UPDATE `students` SET `name`='枫枫',`age`=0,`gender`=true,`email`='xxx@qq.com' WHERE `id` = 1
更新指定字段
可以使用select选择要更新的字段
var student Student
DB.Take(&student)
student.Age = 21
// 全字段更新
DB.Select("age").Save(&student)
// UPDATE `students` SET `age`=21 WHERE `id` = 1
批量更新
例如我想给年龄21的学生,都更新一下邮箱
var studentList []Student
DB.Find(&studentList, "age = ?", 21).Update("email", "is21@qq.com")
还有一种更简单的方式
DB.Model(&Student{}).Where("age = ?", 21).Update("email", "is21@qq.com")
// UPDATE `students` SET `email`='is22@qq.com' WHERE age = 21
这样的更新方式也是可以更新零值的
更新多列
如果是结构体,它默认不会更新零值
email := "xxx@qq.com"
DB.Model(&Student{}).Where("age = ?", 21).Updates(Student{
Email: &email,
Gender: false, // 这个不会更新
})
// UPDATE `students` SET `email`='xxx@qq.com' WHERE age = 21
如果想让他更新零值,用select就好
email := "xxx1@qq.com"
DB.Model(&Student{}).Where("age = ?", 21).Select("gender", "email").Updates(Student{
Email: &email,
Gender: false,
})
// UPDATE `students` SET `gender`=false,`email`='xxx1@qq.com' WHERE age = 21
如果不想多写几行代码,则推荐使用map
DB.Model(&Student{}).Where("age = ?", 21).Updates(map[string]any{
"email": &email,
"gender": false,
})
更新选定字段
Select选定字段
Omit忽略字段
删除
根据结构体删除
// student 的 ID 是 `10`
db.Delete(&student)
// DELETE from students where id = 10;
CopyErrorOK!
删除多个
db.Delete(&Student{}, []int{1,2,3})
// 查询到的切片列表
db.Delete(&studentList)
高级查询
重新构造一些数据用于查询
func main(){
var studentList []Student
DB.Find(&studentList).Delete(&studentList)
studentList = []Student{
{ID: 1, Name: "李元芳", Age: 32, Email: PtrString("lyf@yf.com"), Gender: true},
{ID: 2, Name: "张武", Age: 18, Email: PtrString("zhangwu@lly.cn"), Gender: true},
{ID: 3, Name: "枫枫", Age: 23, Email: PtrString("ff@yahoo.com"), Gender: true},
{ID: 4, Name: "刘大", Age: 54, Email: PtrString("liuda@qq.com"), Gender: true},
{ID: 5, Name: "李武", Age: 23, Email: PtrString("liwu@lly.cn"), Gender: true},
{ID: 6, Name: "李琦", Age: 14, Email: PtrString("liqi@lly.cn"), Gender: false},
{ID: 7, Name: "晓梅", Age: 25, Email: PtrString("xiaomeo@sl.com"), Gender: false},
{ID: 8, Name: "如燕", Age: 26, Email: PtrString("ruyan@yf.com"), Gender: false},
{ID: 9, Name: "魔灵", Age: 21, Email: PtrString("moling@sl.com"), Gender: true},
}
DB.Create(&studentList)
}
func PtrString(email string) *string {
return &email
}
Where
等价于sql语句中的where
var users []Student
// 查询用户名是枫枫的
DB.Where("name = ?", "枫枫").Find(&users)
fmt.Println(users)
// 查询用户名不是枫枫的
DB.Where("name <> ?", "枫枫").Find(&users)
fmt.Println(users)
// 查询用户名包含 如燕,李元芳的
DB.Where("name in ?", []string{"如燕", "李元芳"}).Find(&users)
fmt.Println(users)
// 查询姓李的
DB.Where("name like ?", "李%").Find(&users)
fmt.Println(users)
// 查询年龄大于23,是qq邮箱的
DB.Where("age > ? and email like ?", "23", "%@qq.com").Find(&users)
fmt.Println(users)
// 查询是qq邮箱的,或者是女的
DB.Where("gender = ? or email like ?", false, "%@qq.com").Find(&users)
fmt.Println(users)
使用结构体查询
使用结构体查询,会过滤零值
并且结构体中的条件都是and关系
// 会过滤零值
DB.Where(&Student{Name: "李元芳", Age: 0}).Find(&users)
fmt.Println(users)
使用map查询
不会过滤零值
DB.Where(map[string]any{"name": "李元芳", "age": 0}).Find(&users)
// SELECT * FROM `students` WHERE `age` = 0 AND `name` = '李元芳'
fmt.Println(users)
Not条件
和where中的not等价
// 排除年龄大于23的
DB.Not("age > 23").Find(&users)
fmt.Println(users)
Or条件
和where中的or等价
DB.Or("gender = ?", false).Or(" email like ?", "%@qq.com").Find(&users)
fmt.Println(users)
Select 选择字段
DB.Select("name", "age").Find(&users)
fmt.Println(users)
// 没有被选中,会被赋零值
可以使用扫描Scan,将选择的字段存入另一个结构体中
type User struct {
Name string
Age int
}
var students []Student
var users []User
DB.Select("name", "age").Find(&students).Scan(&users)
fmt.Println(users)
这样写也是可以的,不过最终会查询两次,还是不这样写
SELECT `name`,`age` FROM `students`
SELECT `name`,`age` FROM `students`
这样写就只查询一次了
type User struct {
Name string
Age int
}
var users []User
DB.Model(&Student{}).Select("name", "age").Scan(&users)
fmt.Println(users)
还可以这样
var users []User
DB.Table("students").Select("name", "age").Scan(&users)
fmt.Println(users)
Scan是根据column列名进行扫描的
type User struct {
Name123 string `gorm:"column:name"`
Age int
}
var users []User
DB.Table("students").Select("name", "age").Scan(&users)
fmt.Println(users)
排序
根据年龄倒序
var users []Student
DB.Order("age desc").Find(&users)
fmt.Println(users)
// desc 降序
// asc 升序
注意order的顺序
分页查询
var users []Student
// 一页两条,第1页
DB.Limit(2).Offset(0).Find(&users)
fmt.Println(users)
// 第2页
DB.Limit(2).Offset(2).Find(&users)
fmt.Println(users)
// 第3页
DB.Limit(2).Offset(4).Find(&users)
fmt.Println(users)
通用写法
var users []Student
// 一页多少条
limit := 2
// 第几页
page := 1
offset := (page - 1) * limit
DB.Limit(limit).Offset(offset).Find(&users)
fmt.Println(users)
去重
var ageList []int
DB.Table("students").Select("age").Distinct("age").Scan(&ageList)
fmt.Println(ageList)
或者
DB.Table("students").Select("distinct age").Scan(&ageList)
分组查询
var ageList []int
// 查询男生的个数和女生的个数
DB.Table("students").Select("count(id)").Group("gender").Scan(&ageList)
fmt.Println(ageList)
有个问题,哪一个是男生个数,那个是女生个数
所以我们应该精确一点
type AggeGroup struct {
Gender int
Count int `gorm:"column:count(id)"`
}
var agge []AggeGroup
// 查询男生的个数和女生的个数
DB.Table("students").Select("count(id)", "gender").Group("gender").Scan(&agge)
fmt.Println(agge)
如何再精确一点,具体的男生名字,女生名字
type AggeGroup struct {
Gender int
Count int `gorm:"column:count(id)"`
Name string `gorm:"column:group_concat(name)"`
}
var agge []AggeGroup
// 查询男生的个数和女生的个数
DB.Table("students").Select("count(id)", "gender", "group_concat(name)").Group("gender").Scan(&agge)
fmt.Println(agge)
总之,使用gorm不会让你忘记原生sql的编写
这一点我还是很喜欢的
执行原生sql
type AggeGroup struct {
Gender int
Count int `gorm:"column:count(id)"`
Name string `gorm:"column:group_concat(name)"`
}
var agge []AggeGroup
DB.Raw(`SELECT count(id), gender, group_concat(name) FROM students GROUP BY gender`).Scan(&agge)
fmt.Println(agge)