目录
一、表结构定义细节
1 - 约定
- gorm约定:GORM 倾向于约定,而不是配置。默认情况下,GORM 使用 ID 作为主键,使用结构体名的 蛇形复数 作为表名,字段名的 蛇形 作为列名,并使用 CreatedAt、UpdatedAt 字段追踪创建、更新时间
- 蛇形约定:可以看到之前我们创建的struct是
type Product struct
,而在数据库生成的表名为products
- gorm.Model:GORM 定义一个 gorm.Model 结构体,其包括字段 ID、CreatedAt、UpdatedAt、DeletedAt
- 可以自定义TAG,如
gorm:"primaryKey"
-> 主键;gorm:"index"
-> 索引
- 可以自定义TAG,如
// gorm.Model 的定义
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
2 - 嵌入结构体
- 匿名字段:GORM 会将其字段包含在父结构体中
type User struct {
gorm.Model
Name string
}
// 等效于
type User struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string
}
- embedded嵌入:对于正常的结构体字段,你也可以通过标签 embedded 将其嵌入
type Author struct {
Name string
Email string
}
type Blog struct {
ID int
Author Author `gorm:"embedded"`
Upvotes int32
}
// 等效于
type Blog struct {
ID int64
Name string
Email string
Upvotes int32
}
- embeddedPrefix嵌入前缀:可以使用标签 embeddedPrefix 来为 db 中的字段名添加前缀
type Blog struct {
ID int
Author Author `gorm:"embedded;embeddedPrefix:author_"`
Upvotes int32
}
// 等效于
type Blog struct {
ID int64
AuthorName string
AuthorEmail string
Upvotes int32
}
3 - 字段标签
- 字段标签使用:声明 model 时,tag 是可选的,GORM 支持以下 tag: tag 名大小写不敏感,但建议使用 camelCase 风格
- 案例:字段标签
package main
import (
"log"
"os"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
type User struct {
UserID uint `gorm:"primarykey"`
Name string `gorm:"column:user_name;type:varchar(50);index:idx_user_name;unique;default:'test'"`
}
func main() {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "root:xxx@tcp(192.168.124.51:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
//设置全局的logger,这个logger在我们执行每个sql语句的时候会打印每一行sql
//sql才是最重要的,本着这个原则我尽量的给大家看到每个api背后的sql语句是什么
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info, // Log level
Colorful: true, // 禁用彩色打印
},
)
// 全局模式
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger,
})
if err != nil {
panic(err)
}
_ = db.AutoMigrate(&User{}) //此处应该有sql语句
db.Create(&User{})
}
二、CRUD接口
- 从这里开始我将db的初始化封装到函数中,后续的源码不提供
- main函数中通过执行
db := Init()
来初始化db - import与User的struct也跟这里的一样
- main函数中通过执行
package main
import (
"database/sql"
"fmt"
"log"
"os"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
type User struct {
ID uint
Name string
Email *string
Age uint8
Birthday *time.Time
MemberNumber sql.NullString
ActivatedAt sql.NullTime
CreatedAt time.Time
UpdatedAt time.Time
}
func Init() *gorm.DB {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "root:xxxx@tcp(192.168.124.51:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
//设置全局的logger,这个logger在我们执行每个sql语句的时候会打印每一行sql
//sql才是最重要的,本着这个原则我尽量的给大家看到每个api背后的sql语句是什么
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info, // Log level
Colorful: true, // 禁用彩色打印
},
)
// 全局模式
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger,
})
if err != nil {
panic(err)
}
return db
}
1 - 创建、修改与返回
- 更新非零值的方案
- 将string 设置为 *string
- 使用sql的NULLxxx来解决
- updates语句不会更新零值,但是update语句会更新
func main() {
db := Init()
_ = db.AutoMigrate(&User{}) //此处应该有sql语句
user := User{
Name: "bobby2",
}
fmt.Println(user.ID)
result := db.Create(&user)
fmt.Println(user.ID) // 返回插入数据的主键
fmt.Println(result.Error) // 返回 error
fmt.Println(result.RowsAffected) // 返回插入记录的条数
//db.Model(&User{ID:1}).Update("Name", "")
//updates语句不会更新零值,但是update语句会更新
//empty := ""
//db.Model(&User{ID:1}).Updates(User{Email: &empty})
//解决仅更新非零值字段的方法有两种
/*
1. 将string 设置为 *string
2. 使用sql的NULLxxx来解决
*/
}
2 - 批量插入
- gorm的批量插入:要有效地插入大量记录,请将一个 slice 传递给 Create 方法。 GORM 将生成单独一条SQL语句来插入所有数据,并回填主键的值,钩子方法也会被调用
- 生成单独的sql语句
func main() {
db := Init()
//单一的 SQL 语句
var users = []User{{Name: "bobby1"}, {Name: "bobby2"}, {Name: "bobby3"}}
db.Create(&users)
for _, user := range users {
fmt.Println(user.ID) // 2,3,4
}
}
- 分批次提交:使用 CreateInBatches 分批创建时,你可以指定每批的数量
func main() {
db := Init()
//单一的 SQL 语句
var users = []User{{Name: "bobby1"}, {Name: "bobby2"}, {Name: "bobby3"}}
//db.Create(&users)
//为什么不一次性提交所有的 还要分批次,sql语句有长度限制
db.CreateInBatches(users, 2)
for _, user := range users {
fmt.Println(user.ID) // 2,3,4
}
}
- map创建
func main() {
db := Init()
//单一的 SQL 语句
var users = []User{{Name: "bobby1"}, {Name: "bobby2"}, {Name: "bobby3"}}
//db.Create(&users)
//为什么不一次性提交所有的 还要分批次,sql语句有长度限制
db.CreateInBatches(users, 2)
for _, user := range users {
fmt.Println(user.ID) // 2,3,4
}
db.Model(&User{}).Create(map[string]interface{}{
"Name": "jinzhu", "Age": 18,
})
}
3 - 查询
- 检索单个对象:GORM 提供了 First、Take、Last 方法,以便从数据库中检索单个对象。当查询数据库时它添加了 LIMIT 1 条件,且没有找到记录时,它会返回 ErrRecordNotFound 错误
// 获取第一条记录(主键升序)
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;
// 获取一条记录,没有指定排序字段
db.Take(&user)
// SELECT * FROM users LIMIT 1;
// 获取最后一条记录(主键降序)
db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;
result := db.First(&user)
result.RowsAffected // 返回找到的记录数
result.Error // returns error or nil
// 检查 ErrRecordNotFound 错误
errors.Is(result.Error, gorm.ErrRecordNotFound)
- 用主键检索:如果主键是数字类型,您可以使用 内联条件 来检索对象。 传入字符串参数时,需要特别注意 SQL 注入问题
db.First(&user, 10)
// SELECT * FROM users WHERE id = 10;
db.First(&user, "10")
// SELECT * FROM users WHERE id = 10;
db.Find(&users, []int{1,2,3})
// SELECT * FROM users WHERE id IN (1,2,3);
result := db.First(&user, []int{1, 2, 3})
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
fmt.Println("未找到")
}
fmt.Println(user.ID)
- 检索全部对象
func main() {
db := Init()
//检索全部对象
var users []User
result := db.Find(&users)
fmt.Println("总共记录:", result.RowsAffected)
for _, user := range users {
fmt.Println(user.ID)
}
}
4 - 条件查询
- String 条件
// Get first matched record
db.Where("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;
// Get all matched records
db.Where("name <> ?", "jinzhu").Find(&users)
// SELECT * FROM users WHERE name <> 'jinzhu';
// IN
db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
// SELECT * FROM users WHERE name IN ('jinzhu','jinzhu 2');
// LIKE
db.Where("name LIKE ?", "%jin%").Find(&users)
// SELECT * FROM users WHERE name LIKE '%jin%';
// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;
// Time
db.Where("updated_at > ?", lastWeek).Find(&users)
// SELECT * FROM users WHERE updated_at > '2000-01-01 00:00:00';
// BETWEEN
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
// SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00';
- Struct & Map 条件:可以不需要关心数据库的列名具体是什么
// Struct
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 ORDER BY id LIMIT 1;
// Map
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;
// Slice of primary keys
db.Where([]int64{20, 21, 22}).Find(&users)
// SELECT * FROM users WHERE id IN (20, 21, 22);
- 非零值的查询区别:使用map的时候不会忽略零值,依然会出现在查询条件中;而非map的会忽略零值
db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu";
db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;
- or条件
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)
// SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin';
// Struct
db.Where("name = 'jinzhu'").Or(User{Name: "jinzhu 2", Age: 18}).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' OR (name = 'jinzhu 2' AND age = 18);
// Map
db.Where("name = 'jinzhu'").Or(map[string]interface{}{"name": "jinzhu 2", "age": 18}).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' OR (name = 'jinzhu 2' AND age = 18);
- 查询总结:实际开发中优先选择struct、其次选择map;如果struct和map都不好写就选择string
- string:这种最灵活
- struct:这个有坑,会忽略零值
- map:不会忽略零值
5 - 更新
- 保存所有字段:Save 会保存所有的字段,即使字段是零值
var user User
db.First(&user)
//1. 通过save方法更新
user.Name = "bobby test"
user.Age = 100
user.ID = 0
db.Save(&user) //save方法是一个集create和update于一体的操作
- 更新单个列:当使用 Update 更新单个列时,你需要指定条件,否则会返回 ErrMissingWhereClause 错误,查看 Block Global Updates 获取详情。当使用了 Model 方法,且该对象主键有值,该值会被用于构建条件
// 条件更新
db.Model(&User{}).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE active=true;
// User 的 ID 是 `111`
db.Model(&user).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;
// 根据条件和 model 的值进行更新
db.Model(&user).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true;
- 更新多列:Updates 方法支持 struct 和 map[string]interface{} 参数。当使用 struct 更新时,默认情况下,GORM 只会更新非零值的字段
注意 当通过 struct 更新时,GORM 只会更新非零字段。 如果您想确保指定字段被更新,你应该使用 Select 更新选定字段,或使用 map 来完成更新操作
// 根据 `struct` 更新属性,只会更新非零值的字段
db.Model(&user).Updates(User{Name: "hello", Age: 18, Active: false})
// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111;
// 根据 `map` 更新属性
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
- 更新选定字段:如果您想要在更新时选定、忽略某些字段,您可以使用 Select、Omit
// 使用 Map 进行 Select
// User's ID is `111`:
db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello' WHERE id=111;
db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
// 使用 Struct 进行 Select(会 select 零值的字段)
db.Model(&user).Select("Name", "Age").Updates(User{Name: "new_name", Age: 0})
// UPDATE users SET name='new_name', age=0 WHERE id=111;
// Select 所有字段(查询包括零值字段的所有字段)
db.Model(&user).Select("*").Update(User{Name: "jinzhu", Role: "admin", Age: 0})
// Select 除 Role 外的所有字段(包括零值字段的所有字段)
db.Model(&user).Select("*").Omit("Role").Update(User{Name: "jinzhu", Role: "admin", Age: 0})
- 批量更新:如果您尚未通过 Model 指定记录的主键,则 GORM 会执行批量更新
// 根据 struct 更新
db.Model(User{}).Where("role = ?", "admin").Updates(User{Name: "hello", Age: 18})
// UPDATE users SET name='hello', age=18 WHERE role = 'admin';
// 根据 map 更新
db.Table("users").Where("id IN ?", []int{10, 11}).Updates(map[string]interface{}{"name": "hello", "age": 18})
// UPDATE users SET name='hello', age=18 WHERE id IN (10, 11);
6 - 删除与软删除
- 删除一条记录:删除一条记录时,删除对象需要指定主键,否则会触发 批量 Delete
// Email 的 ID 是 `10`
db.Delete(&email)
// DELETE from emails where id = 10;
// 带额外条件的删除
db.Where("name = ?", "jinzhu").Delete(&email)
// DELETE from emails where id = 10 AND name = "jinzhu";
- 根据主键删除:GORM 允许通过主键(可以是复合主键)和内联条件来删除对象,它可以使用数字(如以下例子。也可以使用字符串——译者注) -> 但是不建议这么使用
db.Delete(&User{}, 10)
// DELETE FROM users WHERE id = 10;
db.Delete(&User{}, "10")
// DELETE FROM users WHERE id = 10;
db.Delete(&users, []int{1,2,3})
// DELETE FROM users WHERE id IN (1,2,3);
- 批量删除:如果指定的值不包括主属性,那么 GORM 会执行批量删除,它将删除所有匹配的记录
db.Where("email LIKE ?", "%jinzhu%").Delete(&Email{})
// DELETE from emails where email LIKE "%jinzhu%";
db.Delete(&Email{}, "email LIKE ?", "%jinzhu%")
// DELETE from emails where email LIKE "%jinzhu%";
- 软删除:
- 如果您的模型包含了一个 gorm.deletedat 字段(gorm.Model 已经包含了该字段),它将自动获得软删除的能力
- 拥有软删除能力的模型调用 Delete 时,记录不会从数据库中被真正删除。但 GORM 会将 DeletedAt 置为当前时间, 并且你不能再通过普通的查询方法找到该记录
// user 的 ID 是 `111`
db.Delete(&user)
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111;
// 批量删除
db.Where("age = ?", 20).Delete(&User{})
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;
// 在查询时会忽略被软删除的记录
db.Where("age = 20").Find(&user)
// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL;
- 如果您不想引入 gorm.Model,您也可以这样启用软删除特性
type User struct {
ID int
Deleted gorm.DeletedAt
Name string
}
- 查找被软删除的记录:您可以使用 Unscoped 找到被软删除的记录
db.Unscoped().Where("age = 20").Find(&users)
// SELECT * FROM users WHERE age = 20;
- 永久删除:您也可以使用 Unscoped 永久删除匹配的记录
db.Unscoped().Delete(&order)
// DELETE FROM orders WHERE id=10;
三、表的关联
1 - Belongs To
- Belongs To:
- belongs to 会与另一个模型建立了一对一的连接。 这种模型的每一个实例都“属于”另一个模型的一个实例
- 例如,您的应用包含 user 和 company,并且每个 user 能且只能被分配给一个 company。下面的类型就表示这种关系。 注意,在 User 对象中,有一个和 Company 一样的 CompanyID。 默认情况下, CompanyID 被隐含地用来在 User 和 Company 之间创建一个外键关系, 因此必须包含在 User 结构体中才能填充 Company 内部结构体
//官方文档
// `User` 属于 `Company`,`CompanyID` 是外键
type User struct {
gorm.Model
Name string
CompanyID int
Company Company
}
type Company struct {
ID int
Name string
}
- 关联创建
func main() {
db := Init()
db.Create(&User{
Name: "bobby",
Company: Company{
Name: "学习",
},
})
}
- 指定外键id才不会新建一个company
func main() {
db := Init()
db.Create(&User{
Name: "bobby2",
Company: Company{
ID: 1,
},
})
}
2 - 关联查询
- 预加载:gorm可以通过Preload、Joins预加载belongs to关联的记录
func main() {
db := Init()
var user User
db.Preload("Company").First(&user)
var user2 User
db.Joins("Company").First(&user2)
fmt.Println(user.Name, user.Company.ID)
fmt.Println(user2.Name, user.Company.ID)
}
3 - belongs To重写外键
- 重写外键:要定义一个 belongs to 关系,数据库的表中必须存在外键。默认情况下,外键的名字,使用拥有者的类型名称加上表的主键的字段名字;例如,定义一个User实体属于Company实体,那么外键的名字一般使用CompanyID
type User struct {
gorm.Model
Name string
CompanyRefer int
Company Company `gorm:"foreignKey:CompanyRefer"`
// 使用 CompanyRefer 作为外键
}
type Company struct {
ID int
Name string
}
4 - belongs To重写引用
- 重写引用:对于 belongs to 关系,GORM 通常使用数据库表,主表(拥有者)的主键值作为外键参考。 正如上面的例子,我们使用主表Company中的主键字段ID作为外键的参考值;如果在Company实体中设置了User实体,那么GORM会自动把Company中的ID属性保存到User的CompanyID属性中
type User struct {
gorm.Model
Name string
CompanyID string
Company Company `gorm:"references:Code"` // 使用 Code 作为引用
}
type Company struct {
ID int
Code string
Name string
}
5 - Has Many
- has many:has many 与另一个模型建立了一对多的连接。 不同于 has one,拥有者可以有零或多个关联模型;例如,您的应用包含 user 和 credit card 模型,且每个 user 可以有多张 credit card
// User 有多张 CreditCard,UserID 是外键
type User struct {
gorm.Model
CreditCards []CreditCard
}
type CreditCard struct {
gorm.Model
Number string
UserID uint
}
// Retrieve user list with edger loading credit cards
func GetAll(db *gorm.DB) ([]User, error) {
var users []User
err := db.Model(&User{}).Preload("CreditCards").Find(&users).Error
return users, err
}
6 - 外键约束总结
- 外键约束总结:
- 在大型的系统中,我个人不建议使用外键约束,外键约束也有很大的优点: 数据的完整性
- 外键约束会让给你的数据很完整,即使是业务代码有些人考虑的不严谨
- 在大型的系统,高并发的系统中一般不使用外键约束,自己在业务层面保证数据的一致性
7 - Many To Many
更多的Mang To Mang相关的查询官方文档说明:https://gorm.io/zh_CN/docs/many_to_many.html
- Many To Many:Many to Many 会在两个 model 中添加一张连接表;例如,您的应用包含了 user 和 language,且一个 user 可以说多种 language,多个 user 也可以说一种 language
- 当使用 GORM 的 AutoMigrate 为 User 创建表时,GORM 会自动创建连接表
// User 拥有并属于多种 language,`user_languages` 是连接表
type User struct {
gorm.Model
Languages []Language `gorm:"many2many:user_languages;"`
}
type Language struct {
gorm.Model
Name string
}
四、表名自定义
- gorm默认使用蛇形复数来创建表名:如果我们需要自定义表名,应该如何处理?
- 自定义表名有2种方式:
- 我们自己定义表名是什么
- 统一的给所有的表名加上一个前缀
1 - 自定义表名
type Language struct {
gorm.Model
Name string
}
// 在gorm中可以通过给某一个struct添加TableName方法来自定义表名
func (Language) TableName() string {
return "my_language"
}
func main() {
db := Init()
fmt.Println(db)
}
2 - 添加表名前缀
- 通过在gorm的config中添加:注意NamingStrategy和Tablename不能同时配置,Tablename的优先级更高
// 全局模式
//NamingStrategy和Tablename不能同时配置
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: "myjp_",
},
Logger: newLogger,
})
if err != nil {
panic(err)
}
db.AutoMigrate(&Language{})
3 - 自动添加时间
type Language struct {
Name string
AddTime time.Time //每个记录创建的时候自动加上当前时间加入到AddTime中
}
func (l *Language) BeforeCreate(tx *gorm.DB) (err error) {
l.AddTime = time.Now()
return
}
func main() {
db := Init()
db.Create(&Language{
Name: "python",
})
}
本文含有隐藏内容,请 开通VIP 后查看