🌙 1.模型定义
数据表定义如下:
CREATE TABLE `goods`
(
`id` int NOT NULL AUTO_INCREMENT COMMENT '自增id,商品id',
`name` varchar(30) NOT NULL COMMENT '商品名',
`price` decimal(10, 2) NOT NULL COMMENT '商品价格',
`type_id` int NOT NULL COMMENT '商品类型id',
`createtime` bigint NOT NULL DEFAULT '0' COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
2
3
4
5
6
7
8
9
转为struct:
type Goods struct {
Id int `gorm:"column:id" db:"id" json:"id" form:"id"` //自增id,商品id
Name string `gorm:"column:name" db:"name" json:"name" form:"name"` //商品名
Price float64 `gorm:"column:price" db:"price" json:"price" form:"price"` //商品价格
TypeId int `gorm:"column:type_id" db:"type_id" json:"type_id" form:"type_id"` //商品类型id
Createtime int64 `gorm:"column:createtime" db:"createtime" json:"createtime" form:"createtime"` //创建时间
}
2
3
4
5
6
7
默认gorm对struct字段名称使用Snake Case命名风格转换成mysql表字段名(小写字母)
Snake Case: 各个单词之间使用下划线分割,例如:CreateTime ---> create_time
默认情况下,使用ID作为主键,使用结构体名称的 Snake Case 风格的复数形式作为表名,使用 CreatedAt、UpdatedAt 字段追踪创建、更新时间。
🌙 2.模型标签
我们可以使用标签去修改这些默认的定义,gorm多个标签可以使用分号分割,不同类别的标签使用空格分割。
标签定义:
`gorm:"标签内容"`
`gorm:"primaryKey;column:userid" json:"userid" xorm:"userid"`
2
gorm常用标签:
标签 | 说明 | 例子 |
---|---|---|
column | 指定列名 | gorm:"column:createtime" |
primaryKey | 指定主键 | gorm:"column:id;PRIMARY_key" |
例如:
type Model struct {
Userid int64 `json:"userid" xorm:"userid" gorm:"primaryKey;column:userid"`
Userid1 int64 `json:"userid1" xorm:"userid1" gorm:"column:userid1"`
Userid2 int64 `json:"userid2" xorm:"userid2" gorm:"column:userid2"`
Userid3 int64 `json:"userid3" xorm:"userid3" gorm:"column:userid3"`
Ct int64 `json:"ct" xorm:"ct" gorm:"column:ct"`
Ut int64 `json:"ut" xorm:"ut" gorm:"column:ut"`
}
2
3
4
5
6
7
8
🌙 3.表名映射
- 复数表名,比如结构体
User
, 默认表名users
- 实现
Tabler
接口(TableName
不支持动态变化,它会被缓存下来以便后续使用)
type Tabler interface {
TableName() string
}
// TableName 会被 User 的表名重写为 "user_list"
func (u User) TableName() string {
return "user_list"
}
2
3
4
5
6
7
8
- 动态表名,使用 Scopes
func UserTable(u user) func (tx *gorm.DB) *gorm.DB {
return func (tx *gorm.DB) *gorm.DB {
if user.Admin {
return tx.Table("admin_user")
}
return tx.Table("users")
}
}
db.Scopes(UserTable(*user)).Create(&user)
2
3
4
5
6
7
8
9
10
11
- 直接使用Tables
db.Tables("table_name").Create(&user)
🌙 4.Model
- GORM 自带一个
gorm.Model
结构体,其包括字段ID
、CreatedAt
、UpdatedAt
、DeletedAt
:
type Model struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt DeletedAt `gorm:"index"`
}
2
3
4
5
6
GORM约定使用CreateAt、UpdatedAt 追踪创建、更新时间。如果定义了这种字段,GORM 在创建、更新时会自动填充当前时间。
要使用不同名称的字段,可以配置 autoCreateTime、autoUpdateTime 标签
如果想要保存 UNIX 秒时间戳,而不是time,只需要简单地将 time.Time 修改为 int 即可。
type User struct {
CreateAt time.Time // 默认创建时间,0值为当前时间填充
UpdatedAt int // 默认更新时间,使用时间戳秒填充
Updated1 int64 `gorm:autoUpdateTime:nano` // 自定义,使用时间戳纳秒更新时间
Updated2 int64 `gorm:autoUpdateTime:milli` // 自定义,使用时间戳毫秒更新时间
Created int64 `gorm:autocreateTime` //自定义,使用时间戳秒创建时间
}
2
3
4
5
6
7
8
我们在使用的时候可以嵌入到结构体中(表中的字段也需要去定义):
type User struct {
gorm.Model // 继承上述字段
Username string `gorm:"column:username;json:username"` // `gorm:"column:username"`: Mysql表的字段名为username
Password string `gorm:"column:password"`
Admin bool `gorm:"-"` // 忽略
}
2
3
4
5
6
7
等效于:
type User struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt DeletedAt `gorm:"index"
Username string `gorm:"column:username" json:"username"` // `gorm:"column:username"`: Mysql表的字段名为username
Password string `gorm:"column:password"`
Admin bool `gorm:"-"` // 忽略
}
2
3
4
5
6
7
8
9
10
- 对于正常的结构体字段,也可以通过标签
embedded
将其嵌入,例如:
type Author struct {
Name string
Email string
}
type Blog {
ID int
Author Author `gorm:"embedded"`
Likes int32
}
// 等效于
type Blog struct {
ID int
Name string
Email string
Likes int32
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 可以使用标签
embeddedPrefix
来为db中的字段名称添加前缀。例如:
type Blog {
ID int
Author Author `gorm:"embedded;embeddedPrefix:author_"`
Likes int32
}
// 等效于
type Blog struct {
ID int
AuthorName string
AuthorEmail string
Likes int32
}
2
3
4
5
6
7
8
9
10
11
12
13
🌙 5.数据库连接
GORM 官方支持的数据库类型有:MySQL、PostgreSQL、SQLite、SQL Server。连接数据库主要的两个步骤:
- 配置DSN(Data Source Name)。
- 使用gorm.Open连接数据库。
🌙 5.1.DSN
gorm库使用DSN作为链接数据库的参数,DSN即数据源名称,用来描述数据库连接信息。一般包括数据库域名地址、账号、用户名等,格式:
[username[:password]@[protocol][(address)]/dbname?param1=value&...¶mN=valueN
username= "root" // 用户名
password= "123456" // 密码
host= "127.0.0.1" // 域名
port= 3306 // 端口
Dbname= "test" // 数据库名
params:?charset=utf8&parseTime=True&loc=Local&timeout=10
charset=utf8mb4 客户端字符集
parseTime=True 支持把数据库datetime和date数据类型准尉golang的time.Time类型
loc=Local 使用系统本地时区
timeout=10 设置10s后连接超时
2
3
4
5
6
7
8
9
10
11
12
13
14
🌙 5.2.连接数据库
- 通过
mysql.Open
连接数据库
package dao
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"log"
)
var DB *gorm.DB
func init() {
// 连接数据库
username := "root" // 用户名
password := "123456" // 密码
host := "127.0.0.1" // 域名
port := 3306 // 端口
Dbname := "test" // 数据库名
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=10", username, password, host, port, Dbname)
// ?charset=utf8&parseTime=True&loc=Local&timeout=10:
// charset=utf8mb4 客户端字符集
// parseTime=True 支持把数据库datetime和date数据类型准尉golang的time.Time类型
// loc=Local 使用系统本地时区
// timeout=10 设置10s后连接超时
// https://gorm.io/zh_CN/docs/connecting_to_the_database.html#MySQL
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
// 其他配置...
})
if err != nil {
log.Fatalln("db connect failed err", err)
}
DB = db
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
- 通过
mysql.New
创建连接数据库 MySQL 驱动程序提供了一些高级配置,可以在初始化过程中使用,例如:
db, err := gorm.Open(mysql.New(mysql.Config{
DSN: dsn, // DSN data source name
DefaultStringSize: 256, // string 类型字段的默认长度
DisableDatetimePrecision: true, // 禁用 datetime 精度, MySQL 5.6 之前不支持
DontSupportRenameIndex: true, // 重名索引时才有删除并新建的方式,MySQL 5.7 之前 和 MariaDB不支持重命名索引
DontSupportRenameColumn: true, // 用`change`重命名列,MySQL 8 之前 和 MariaDB不支持重命列
SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置
}), &gorm.Config{})
2
3
4
5
6
7
8
- 自定义MySQL驱动: GORM 允许通过 DriverName 选项自定义 MySQL 驱动:
db, err := gorm.Open(mysql.New(mysql.Config{
DriverName: "my_mysql_driver",
DSN: dsn, // DSN data source name
}), &gorm.Config{})
2
3
4
🌙 6.调试模式
打开调试模式,可以在console查看sql的打印:
db.Debug()
// 例如
err := DB.Debug().Create(user).Error
2
3
4
🌙 7.连接池配置
sqlDb,_ := DB.DB()
// 设置数据库连接池最大连接数
sqlDb.SetMaxOpenCoons(100)
// 连接池最大允许的空闲连接数,如果没有SQL任务需要执行的连接数大于20.超过的链接会被连接池关闭
sqlDb.SetMaxIdleConns(20)
2
3
4
5
🌙 8.增删改查(crud)
🌙 8.1 Create插入数据
🌙 8.1.1指定字段Select
DB.Select("username", "password").Create(&user)
例如:
func SaveUser1(user *User) {
// 指定插入字段username, password
result := DB.Select("username", "password").Create(user)
affected := result.RowsAffected
fmt.Println("affect rows", affected)
err := result.Error
if err != nil {
log.Panicln("insert user error", err)
}
}
func SaveUser(user *User) {
// 连接数据库 DB.Debug()开启debug模式
err := DB.Debug().Create(user).Error
if err != nil {
log.Panicln("insert user error", err)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
🌙 8.1.2忽略字段Omit
// 除了username
DB.Omit("").Create(&user)
2
🌙 8.1.3批量插入
// 多个user
var users = []Users{{Username: "u1"}, {Username: "u2"}, {Username: "u3"}}
DB.Create(&users)
2
3
也可以使用CreateInBatches
分批创建,可以指定每批的数量:
var users = []Users{{Username: "u1"}, {Username: "u2"},..., {Username: "u10000"}}
// 每次新增100条
DB.CreateInBatches(users, 100)
2
3
🌙 8.1.4使用结构体形式
DB.Create(&user)
- 使用map形式:主键不会被填充,需要指定Model
// 单个
DB.Model(&User{}).Create(map[string]interface{}{
"Name": "u1",
"Age": 18
})
// 多个
DB.Model(&User{}).Create(map[string]interface{}{
{"Name": "u1","Age": 18},
{"Name": "u2","Age": 19},
})
2
3
4
5
6
7
8
9
10
11
🌙 8.1.4sql表达式
// 使用md5加密
// insert into users (username, password) values ("u1", md5("123456"))
DB.Model(&User{}).Create(map[string]interface{}{
"Name": "u1",
"Password": clause.Expr{SQL: "md5(?)", Vars:[]interface{}{"123456"}},
})
2
3
4
5
6
🌙 8.1.5原生sql语句
// ? 占位符号
DB.Exec("insert into users (username, password) values (?,?)", user.Username, user.Password)
2
一般md5加密可以使用
crypto/md5
库
🌙 8.2 Update更新数据
🌙 8.2.1使用Save
先查询再更新
func UpdateGoods() {
var good Goods
// Take 赋值
err := DB.Where("id=?", good.Id).Take(&good).Error
if err != nil {
log.Panicln("search goods error", err)
}
// 修改Price
good.Price = 5.5
// 保存
err = DB.Save(&good).Error
if err != nil {
log.Panicln("update goods error", err)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
🌙 8.2.2使用Update
更新
Update
: 更新单个字段
Updates
: 更新多个字段使用结构体或者map
func UpdateGoodById(id int, price string, name string) {
// 指定Model
// 更新单列
err := DB.Model(&Goods{}).Where("id", id).Update("price", price).Error
// 更新多列 结构体或者map
err = DB.Model(&Goods{}).Where("id", id).Updates(Goods{
Name: name,
Price: price,
}).Error
if err != nil {
log.Panicln("update goods error", err)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
🌙 8.2.3更新选定的字段Select
func UpdateGoods1() {
goods := Goods{
Id: 1,
Name: "apple",
Price: "3.5",
Type: 1,
Createtime: time.Now().Unix(),
}
err := DB.Model(&goods).Select("name").Updates(goods).Error
if err != nil {
log.Panicln("update goods error", err)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
🌙 8.2.4排除更新的字段Omit
func UpdateGoods2() {
goods := Goods{
Id: 1,
Name: "apple",
Price: "3.5",
Type: 2,
Createtime: time.Now().Unix(),
}
// 不更新type, id
err := DB.Model(&goods).Omit("type", "id").Updates(goods).Error
if err != nil {
log.Panicln("update goods error", err)
}
}
// 组合使用
Select("*").Omit("type")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
gorm更新必须带条件进行更新,否则会返回错误gorm.ErrMissingWhereClause
,或者启用AllowGlobalUpdate
模式
DB.Session(&gorm.Session{AllowGlobalUpdate: true}).Model(&User{}).Update("name", "text")
🌙 8.2.5使用表达式
DB.Model(&user).Update("age", gorm.Expr("age+1"))
DB.Model(&user).Update(map[string]interface{"age": gorm.Expr("age+1")})
2
🌙 8.2.6子查询更新
goods := Goods{}
DB.Where("id=?", 1).Take(&goods)
Db.Model(&goods).Update("name", DB.Model(&User{}).Select("username")).Where("id=?", 2)
2
3
🌙 8.3 删除数据
删除数据使用DB.Delete
, 一般先查询后删除:
func DeleteGoodById(id int) {
// 默认直接根据主键ID删除
DB.Delete(&Goods{}, id)
// 推荐:先查询或删除
goods := Goods{}
err := DB.Where("id=?", id).Take(&goods).Error
if err != nil {
log.Panicln("delete goods error", err)
return
}
DB.Delete(&goods)
}
2
3
4
5
6
7
8
9
10
11
12
13
*同样的道理,不带条件进行删除,必须加一些条件,或者使用原生SQL,或者启用AllowGlobalUpdate
模式:
DB.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&User{})
🌙 8.3.1 gorm中软删除和硬删除
在gorm中,删除数据可以使用Delete方法或Unscoped方法。Delete方法用于软删除,即将记录的DeletedAt字段设置为当前时间。Unscoped方法用于硬删除,即直接从数据库中删除记录。
- 软删除: 软删除是指在数据库中不会真正删除记录,而是将记录的状态标记为已删除。在GORM中,软删除可以通过添加deleted_at字段来实现,当记录被删除时,GORM会将deleted_at字段设置为当前时间戳,表示该记录已被删除。软删除的优点是可以很容易地恢复已删除的记录,而且不会影响到其他相关的记录。
import (
"gorm.io/gorm"
)
type User struct {
gorm.Model
Name string
Email string
DeletedAt gorm.DeletedAt // 添加deleted_at字段
}
// 软删除用户
func SoftDeleteUser(db *gorm.DB, id uint) error {
result := db.Delete(&User{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
+硬删除: 硬删除是指从数据库中真正删除记录。在GORM中,可以使用Delete方法来执行硬删除操作。硬删除的优点是可以释放数据库空间,但缺点是一旦删除就无法恢复,可能会影响到其他相关的记录。
总的来说,软删除和硬删除各有优缺点,具体使用哪种方式取决于具体的业务需求。
// 硬删除用户
func HardDeleteUser(db *gorm.DB, id uint) error {
result := db.Unscoped().Delete(&User{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
2
3
4
5
6
7
8
9
10
11
需要注意的是,gorm的软删除和硬删除都不会自动更新关联表中的外键,需要手动更新外键或使用级联删除。
🌙 8.3.2 Mysql软删除和硬删除
+软删除: 在MySQL中,软删除是通过添加一个标志位来实现的,通常会在表中添加一个is_deleted字段,用于标识记录是否被删除。软删除的好处是可以通过修改标志位来还原已删除的记录,但是查询时需要加上WHERE is_deleted = 0的条件。
-- 创建表时添加is_deleted字段
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
email VARCHAR(50) NOT NULL,
is_deleted BOOLEAN NOT NULL DEFAULT FALSE
);
-- 软删除用户
UPDATE users SET is_deleted = TRUE WHERE id = 1;
2
3
4
5
6
7
8
9
10
+硬删除: 在MySQL中,硬删除是指直接从表中删除记录,使用DELETE语句可以执行硬删除操作。硬删除的好处是可以释放数据库空间,但是一旦删除就无法恢复。
下面是执行硬删除的示例SQL:
-- 硬删除用户
DELETE FROM users WHERE id = 1;
2
需要注意的是,硬删除会直接从表中删除记录,如果存在外键关联,可能会导致其他表的记录也被删除,因此在执行硬删除时需要谨慎处理。
🌙 8.4 查询数据
🌙 8.4.1 Take 查询一条记录(单个)
var user Users
DB.Where("id=?", 1).Take(&user).Error
2
🌙 8.4.2 First 查询满足条件的第一条数据(单个)
DB.Where("age>?", 18).First(&user).Error
🌙 8.4.3 Last 查询满足条件的最后一条数据(单个)
DB.Where("age>?", 18).Last(&user).Error
🌙 8.4.4 Find 查询满足条件的多条记录(多个)
var users []Users
DB.Where("age>?", 18).Find(&users)
2
🌙 8.4.5 Pluck 查询一列值(多个)
var names []string
// 查询那么列
DB.Model(&Users{}).Pluck("name", &names)
2
3
当 First、Last、Take 方法找不到记录时,GORM 会返回 ErrRecordNotFound 错误, 可以通过对比 gorm.ErrRecordNotFound 进行判断 或者使用 Find 和 Limit 的组合进行出查询, 找不到记录时会返回nil。
🌙 8.4.6 Where 条件查询
Where方法可以与其他查询方法一起使用,例如Find、First、Last等。
Where方法接收一个字符串作为查询条件,?表示占位符,后面的参数会替换占位符, 也可以使用map或结构体作为查询条件
var users []User
// 1.直接写条件
db.Where("name = ?", "jinzhu").Find(&users)
// 2.使用map作为条件
db.Where(map[string]interface{}{"name": "jeek", "age": 18}).Find(&users)
// 3.使用结构体作为条件
type UserQuery struct {
Name string
}
db.Where(&UserQuery{Name: "jinzhu"}).Find(&users)
2
3
4
5
6
7
8
9
10
11
12
13
🌙 8.4.7 Order 指定排序
Order方法用于指定查询结果的排序方式。可以按照一个或多个字段进行排序,也可以指定排序方式(升序或降序)。
var users []User
db.Order("age desc").Find(&users)
db.Order("age desc, name asc").Find(&users)
2
3
🌙 8.4.8 Limit 指定查询的数量
可以使用Limit方法限制查询结果的数量,从而实现分页查询或限制查询结果的数量。
Limit方法可以与其他查询方法一起使用,例如Find、First、Last、Where、Order等
var users []User
db.Limit(10).Find(&users)
2
🌙 8.4.9 Offset 指定查询的偏移量
var users []User
db.Offset(10).Find(&users)
2
🌙 8.4.10 分页查询
使用Limit和Offset方法来实现分页查询
var users []User
pageNum := 2
pageSize := 10
offset := (pageNum - 1) * pageSize
db.Limit(pageSize).Offset(offset).Find(&users)
2
3
4
5
分页查询封装成一个通用的方法:
type PageInfo struct {
PageNum int `json:"pageNum"`
PageSize int `json:"pageSize"`
Total int64 `json:"total"`
}
func Paginate(db *gorm.DB, pageNum int, pageSize int, out interface{}) (PageInfo, error) {
var count int64
if err := db.Model(out).Count(&count).Error; err != nil {
return PageInfo{}, err
}
offset := (pageNum - 1) * pageSize
if err := db.Limit(pageSize).Offset(offset).Find(out).Error; err != nil {
return PageInfo{}, err
}
return PageInfo{
PageNum: pageNum,
PageSize: pageSize,
Total: count,
}, nil
}
// 使用:
var users []User
pageInfo, err := Paginate(db, 1, 10, &users)
if err != nil {
// 处理错误
}
// 将查询结果和分页信息返回给前端
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
🌙 8.4.11 Group 指定分组
可以按照一个或多个字段进行分组,也可以使用Group方法进行子查询。
var result []struct {
Age int
Count int
}
db.Select("age, count(*) as count").Group("age").Find(&result)
2
3
4
5
Group方法只能与Select方法一起使用,不能与其他查询方法一起使用。
🌙 8.4.12 Joins 进行联表查询
可以使用Joins方法进行内连接、左连接、右连接、全连接等不同类型的联表查询。
var result []struct {
Name string
Email string
}
db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&result)
2
3
4
5
🌙 8.4.13 Scan 查询结果扫描
Scan是用于将查询结果扫描到一个结构体中的方法。它可以将查询结果映射到一个结构体中,或者将查询结果映射到一个结构体的切片中。
import (
"gorm.io/gorm"
)
type User struct {
ID uint
Name string
Email string
}
// 查询所有用户
func GetUsersByRawSQL(db *gorm.DB) ([]User, error) {
var users []User
// 使用Raw执行原生sql,使用Scan将结果映射到users切片中
result := db.Raw("SELECT * FROM users").Scan(&users)
if result.Error != nil {
return nil, result.Error
}
return users, nil
}
// 将查询结果映射到结构体切片
func ScanUsers() {
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
defer db.Close()
users, err := GetUsersByRawSQL(db)
if err != nil {
panic(err)
}
for _, user := range users {
fmt.Println(user.ID, user.Name, user.Email)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38