gorm day2
- 连接到数据库
- 创建记录
连接到数据库
gorm官方支持的数据库类型有:MySQL,postgresql,Sqlite,sql server
Mysql
import ("gorm.io/driver/mysql""gorm.io/gorm"
)func main() {// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
}
dsn 是一个字符串,代表数据源名称,它是连接数据库所需要的参数信息的集合,这个的格式就是这样:“user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local”
里面的每个部分都需要写,因为这描述了数据源的信息。
每部分的含义:
user:pass: 数据库的用户名和密码,user 是用户名,pass 是密码
@tcp(127.0.0.1:3306): 指定数据库服务器的地址和端口。这里使用 127.0.0.1:3306 表示在本地机器的 3306 端口上运行的 MySQL 服务。
/dbname: 要连接的数据库名
?charset=utf8mb4&parseTime=True&loc=Local:连接参数
charset=utf8mb4: 设置字符集为 utf8mb4。
parseTime=True: 将数据库中的时间类型自动解析为 Go 的 time.Time 类型
loc=Local: 将时间解释为本地时区。
gorm.Open()这个函数用于返回一个新的数据库连接
参数:mysql.Open(dsn):这个是使用Mysql驱动和提供的DSN连接数据库。
&gorm.Config{}: Gorm 的配置对象。在这个例子中,使用的是默认配置。
返回值:db, err: gorm.Open 返回两个值。第一个是数据库连接对象 *gorm.DB,它提供了操作数据库的方法;第二个是在尝试连接数据库时遇到的任何错误。
简言之:拿到了db就可以操作数据库了,这是因为它的数据类型是*gorm.DB
*gorm.DB解读:
*gorm.DB是一个结构体类型的指针,在gorm中。gorm.DB 结构体提供了与数据库交互的所有方法,如创建(Create)、查询(Find)、更新(Update)、删除(Delete)等操作。当你使用 gorm.Open 函数连接到数据库时,这个函数会返回一个 *gorm.DB 对象。这个对象封装了数据库操作的会话信息,并提供了执行数据库操作的方法。它并不是接口。
主要关注它的方法:
*gorm.DB 提供了一组丰富的方法来执行SQL语句,例如:
创建
Create(value interface{}): 在数据库中创建一个记录。
查询
First(out interface{}, where …interface{}): 获取第一条记录(主键升序)。
Take(out interface{}, where …interface{}): 获取一条记录,不指定排序。
Last(out interface{}, where …interface{}): 获取最后一条记录(主键降序)。
Find(out interface{}, where …interface{}): 获取所有匹配的记录。
Preload(column string): 预加载关联。
Where(query interface{}, args …interface{}): 添加条件。
Or(query interface{}, args …interface{}): 添加 OR 条件。
Not(query interface{}, args …interface{}): 添加 NOT 条件。
Limit(limit int): 设置限制返回的记录数。
Offset(offset int): 设置偏移量,用于分页。
Order(value interface{}): 设置排序方式。
Select(query interface{}, args …interface{}): 指定要检索的字段。
Joins(query string, args …interface{}): 添加 JOIN 条件。
Group(query string): 分组。
更新:
Save(value interface{}): 保存记录(执行更新操作)。
Updates(values interface{}): 更新多个字段。
Update(column string, value interface{}): 更新单个字段。
UpdateColumn(column string, value interface{}): 更新单个字段,忽略钩子方法(Before/After Update)。
UpdateColumns(values interface{}): 更新多个字段,忽略钩子方法。
删除:
Delete(value interface{}, where …interface{}): 删除记录。
Unscoped(): 包括软删除的记录在内的操作。
链式调用: Gorm 设计了链式调用的API,使得数据库操作既直观又灵活。你可以轻松地通过链式调用组合不同的方法,来构建复杂的查询。
这个链式调用就是这个类型的特点,说一说链式调用有啥好,比如我在查询时,我还有条件,那就在返回的结果在调用一次函数就可以了。这就是链式调用。
自定义驱动
这里说的就是mysql里面的配置函数:举个例子
db, err := gorm.Open(mysql.New(mysql.Config{DSN: "gorm:gorm@tcp(127.0.0.1:3306)/gorm?charset=utf8&parseTime=True&loc=Local", // DSN data source nameDefaultStringSize: 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{})
现有的数据库连接:
GORM允许通过一个现有的数据库连接来初始化*gorm.DB
import ("database/sql""gorm.io/gorm"
)sqlDB, err := sql.Open("mysql", "mydb_dsn")
gormDB, err := gorm.Open(mysql.New(mysql.Config{Conn: sqlDB,
}), &gorm.Config{})
连接其他类型的数据库都是一个流程。
连接池
GORM使用database/sql维护连接池
sqlDB, err := db.DB()// SetMaxIdleConns 设置空闲连接池中连接的最大数量
sqlDB.SetMaxIdleConns(10)// SetMaxOpenConns 设置打开数据库连接的最大数量。
sqlDB.SetMaxOpenConns(100)// SetConnMaxLifetime 设置了连接可复用的最大时间。
sqlDB.SetConnMaxLifetime(time.Hour)
可能看不懂,这里我介绍什么是连接池,有啥用,有啥好
连接池是数据库连接管理的一种技术,用于减少数据库操作的开销。在数据库应用程序中,打开和关闭数据库连接是一个非常耗时的操作,尤其是当应用程序频繁地执行数据库操作时。连接池通过重用一定数量的数据库连接来解决这个问题,而不是每次操作时都打开和关闭连接。
连接池的作用
1.提高性能: 重用现有的数据库连接,减少了频繁打开和关闭连接的开销。
2.管理数据库连接: 限制并发的数据库连接数,避免过多的连接导致数据库资源耗尽。
3.提高资源利用率: 通过合理配置连接池的参数,可以使得数据库连接资源得到有效利用,避免资源浪费。
GORM 中的连接池配置
在 GORM 中,通过 db.DB() 方法可以获取到底层的 **sql.DB 对象,这个对象代表了与数据库的连接池。**通过设置该对象的几个关键参数,可以配置连接池的行为:sqlDB, err := db.DB()
这行代码获取了 GORM 数据库实例 db 底层的 sql.DB 对象,该对象提供了对数据库连接池的直接控制。
sqlDB.SetMaxIdleConns(10)
设置空闲连接池中连接的最大数量。空闲连接池包含当前打开但未被使用的连接。当新的数据库请求到达并且连接池中有空闲连接时,可以直接重用这些连接,而不需要重新建立连接。
sqlDB.SetMaxOpenConns(100)
设置打开数据库连接的最大数量。这个限制包括了正在使用的连接和空闲连接的总和。如果这个限制被达到,新的请求将会等待,直到其他请求释放连接。
sqlDB.SetConnMaxLifetime(time.Hour)
设置了连接可复用的最大时间。这意味着一个连接只能被重用一定时间,超过这个时间后,即使连接处于空闲状态,也会被关闭。这个设置可以防止使用过时的连接,对于数据库服务器的维护和更新也是有益的。
创建记录
直接看例子
user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}result := db.Create(&user) // 通过数据的指针来创建user.ID // 返回插入数据的主键
result.Error // 返回 error
result.RowsAffected // 返回插入记录的条数
user := User{Name: “Jinzhu”, Age: 18, Birthday: time.Now()}
初始化结构体,这个换成gorm里的理解就是定义模型
result := db.Create(&user) // 通过数据的指针来创建
这个就是user示例被插入。
对于这个函数的使用解读:
db.Create(&user) 函数执行插入操作,并返回一个 *gorm.DB 类型的对象
*gorm.DB 对象提供了以下几个用于检查操作结果的属性:
Error属性用于存储操作中遇到的任何错误。如果操作成功完成,Error 将会是 nil。如果发生错误,比如违反了数据库的约束条件,Error 将包含相应的错误信息。
RowsAffected 属性表示此次操作影响(比如插入、更新或删除)了多少行数据。对于 Create 操作,如果成功插入一条记录,RowsAffected 通常会是 1。如果没有记录被插入,RowsAffected 将是 0。
可以用这个返回值的这些属性来看插入执行的情况。
用指定的字段创建记录
创建记录并更新给出的字段
user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
db.Select("Name", "Age", "CreatedAt").Create(&user)
// INSERT INTO `users` (`name`,`age`,`created_at`) VALUES ("jinzhu", 18, "2020-07-04 11:05:21.775")
解读:
这个就是gorm里的链式调用,这行代码展示了如何在使用gorm进行数据库插入(create)操作时,通过链式调用的方式,只选择某些特定的字段进行插入。
db: GORM 数据库操作对象,用于执行所有数据库操作。
.Select(“Name”, “Age”, “CreatedAt”): 这个方法用于指定在接下来的数据库操作中,只涉及到的字段列表。这里,它指定只有 Name、Age 和 CreatedAt 这三个字段会被包含在 Create 操作中。
.Create(&user): 执行插入操作,将 user 对象的数据插入到数据库中。但是由于之前调用了 .Select 方法,因此只有 Name、Age 和 CreatedAt 字段的值会被插入到数据库。
对于未被选择的表中的字段有三种情况
1.如果数据库表中的字段被定义有默认值,那么在插入时如果没有为这些字段提供值(即它们没有被 .Select 选中),数据库将会为这些字段自动填充默认值。
2.没有默认值且允许为 NULL 的字段: 如果表结构中的字段没有默认值,但允许为 NULL,那么这些字段将会被设置为 NULL。
3.没有默认值且不允许为 NULL 的字段: 如果表结构中的字段既没有默认值,也不允许为 NULL,此时如果尝试插入没有包含这些字段的记录,数据库将会抛出错误,因为这违反了表结构的约束条件。
**总的来说,**使用 .Select 选定字段进行插入时,未被选中的字段的处理结果取决于数据库表的结构定义。为了避免插入错误,建议在设计数据库表时合理使用默认值和 NULL 约束,或者确保插入语句包含所有不允许为 NULL 且没有默认值的字段。
应用场景:
当我的模型有很多字段,但是某次插入操作中我只想插入部分字段的值时,可以用Select方法来指定这些字段。这种方式能够帮助减少不必要的数据传输和处理,尤其是当某些字段有默认值或者可以为null时。
有个疑问
链式调用,这个调用的过程有先后顺序的讲究吗,比如db.Select(“Name”, “Age”, “CreatedAt”).Create(&user)
这个例子中Create()可以写Select前面调用吗?
回答:顺序是有要求的,这是因为链式调用的每个方法都有可能会修改GORM的内部状态,从而影响后序的行为,所以我个人的考虑:按调用的逻辑来进行写就好了。例子中的就很符合我们理解上的调用。
涉及配置查询或操作(如 .Select、.Where、.Limit 等)的情况下,这些配置方法应该在执行数据库操作(如 .Create、.Find、.Update、.Delete 等)之前调用。这其实也非常符合代码的可读性
创建一个记录且一同忽略传递给略去的字段值
user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
db.Omit("Name", "Age", "CreatedAt").Create(&user)
// INSERT INTO `users` (`birthday`,`updated_at`) VALUES ("2020-01-01 00:00:00.000", "2020-07-04 11:05:21.775")
这个就是创建记录的时候筛选字段的时候略过"Name", “Age”, "CreatedAt"这三个字段。
批量插入
一条记录一条记录的添加太麻烦了,所以这里有批量插入。
要有效的插入大量记录,请将一个slice(切片)传递给Create方法。GORM将单独生成一条SQL语句来插入所有数据,并会填主键的值,钩子方法也会被调用。
var users = []User{{Name: "jinzhu1"}, {Name: "jinzhu2"}, {Name: "jinzhu3"}}
db.Create(&users)for _, user := range users {user.ID // 1,2,3
}
这个就实现了一个批量插入操作
db.Create(&users)调用之后进行数据的批量插入
users是结构体切片,GORM会遍历这个切片,为每个User实例生成一个INSERT语句,并执行这些语句来插入记录到数据库中。
并且,GORM支持批量操作,并且在操作完成后,会自动更新每个 User 实例的 ID 字段(假设 ID 字段是表的自增主键)。
所以这里
for _, user := range users {
user.ID // 1,2,3
}
才能够访问到里面user实例本身没有赋值的ID属性。
使用CreateInBatches分批创建时,你可以指定每批的数量,例如
var users = []User{{name: "jinzhu_1"}, ...., {Name: "jinzhu_10000"}}// 数量为 100
db.CreateInBatches(users, 100)
首先,定义了一个 User 结构体切片 users,假设它包含了从 {Name: “jinzhu_1”} 到 {Name: “jinzhu_10000”} 总共 10,000 个 User 结构体实例。每个实例代表一个将要插入数据库的记录。
使用 db.CreateInBatches(users, 100) 执行批量插入操作。这里,users 是要插入的记录,而 100 是每批次插入的记录数量。
CreateInBatches 方法将 users 切片中的记录分成多个批次进行插入,每个批次包含 100 条记录。这意味着,总共会有 100 次插入操作,每次操作插入 100 条记录,直到所有 10,000 条记录都被插入到数据库中。
总的来说是个很强大的方法。
Upsert和Create With Associations也支持批量插入
注意使用CreateBatchSize选项初始化GORM时,所以的创建&关联INSERT都将遵循该选项。
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{CreateBatchSize: 1000,
})db := db.Session(&gorm.Session{CreateBatchSize: 1000})users = [5000]User{{Name: "jinzhu", Pets: []Pet{pet1, pet2, pet3}}...}db.Create(&users)
// INSERT INTO users xxx (5 batches)
// INSERT INTO pets xxx (15 batches)
解读:
该示例演示了如何使用 GORM 进行批量插入操作,同时展示了如何通过配置 CreateBatchSize 来控制批量插入的行为。具体来说,这段代码涉及到数据库的初始化、会话的创建、以及如何利用 CreateBatchSize 来优化批量插入操作。
数据库初始化
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{CreateBatchSize: 1000,
})
配置了全局的批量插入大小为 1000。这意味着,当使用 .Create 方法进行批量插入操作时,每批次最多包含 1000 条记录。
会话创建
db := db.Session(&gorm.Session{CreateBatchSize: 1000})
创建了一个新的 GORM 会话,并再次指定 CreateBatchSize 为 1000。这样做可以在特定会话级别覆盖全局的配置或为特定的操作定制配置。注意这里的 db 变量被重新赋值,它现在指向这个新创建的会话。
批量插入操作
users = [5000]User{{Name: "jinzhu", Pets: []Pet{pet1, pet2, pet3}}...}db.Create(&users)
使用 db.Create(&users) 执行批量插入操作。根据配置的 CreateBatchSize,这将导致用户(User)记录被分成 5 批次插入(因为有 5000 个用户,每批次 1000),每个 User 关联的宠物(Pet)也将分批插入。
插入批次的计算:
用户批次:因为有 5000 个用户,每批次最多 1000 条记录,所以用户数据将被分成 5 批进行插入。
宠物批次:如果每个用户都有 3 只宠物,则总共有 5000 * 3 = 15000 只宠物。根据 CreateBatchSize 的配置,宠物数据将被分成 15 批进行插入(每批次 1000 条记录)。
总结:
这个示例演示了如何在 GORM 中配置和使用批量插入操作,通过 CreateBatchSize 来优化插入效率。通过合理设置批量插入的大小,可以在保证性能的同时,有效地向数据库插入大量数据。这种方法尤其适用于需要一次性插入大量记录的场景,如初始化数据库、批量数据处理等。
创建钩子
什么是钩子?
在 GORM 中,钩子(Hooks)是指在执行数据库操作(如插入、查询、更新、删除等)的生命周期的特定点自动调用的方法。这些钩子允许你在操作执行前后插入自定义的逻辑,比如自动设置或更新字段的值、验证数据、记录日志等。
如何实现?
gorm定义了一系列的接口,可以通过实现这些接口中的方法来定义钩子,这些方法会在执行相应的数据库操作时被 GORM 自动调用。
常见的GORM钩子
以下是一些常见的 GORM 钩子方法及其触发时机:
BeforeCreate: 在新记录插入数据库之前调用。
AfterCreate: 在新记录插入数据库之后调用。
BeforeUpdate: 在记录更新之前调用。
AfterUpdate: 在记录更新之后调用。
BeforeSave: 在记录保存(插入或更新)之前调用。
AfterSave: 在记录保存(插入或更新)之后调用。
BeforeDelete: 在记录从数据库删除之前调用。
AfterDelete: 在记录从数据库删除之后调用。
BeforeFind: 在查询操作执行之前调用。
AfterFind: 在查询操作执行并将结果映射到实体之后调用。
实现gorm的例子
假设有一个 User 模型,我们希望在每次创建用户记录之前自动生成一个唯一的 ID,并在创建记录之后打印一条日志。可以这样实现:
package mainimport ("fmt""gorm.io/gorm""time"
)type User struct {ID stringName stringCreatedAt time.Time
}// BeforeCreate 钩子
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {u.ID = generateUniqueID() // 假设这是一个生成唯一 ID 的函数return
}// AfterCreate 钩子
func (u *User) AfterCreate(tx *gorm.DB) (err error) {fmt.Printf("New user created: %v\n", u)return
}
在这个示例中,BeforeCreate 钩子用于在创建 User 记录之前生成一个唯一的 ID,而 AfterCreate 钩子则在创建记录之后打印一条日志信息。
如果想跳过钩子,可以使用SkipHooks会话模式:
DB.Session(&gorm.Session{SkipHooks: true}).Create(&user)
DB.Session(&gorm.Session{SkipHooks: true}).Create(&users)
DB.Session(&gorm.Session{SkipHooks: true}).CreateInBatches(users, 100)
使用Map创建记录
gorm支持根据map[string]interface{}和[]map[string]interface{}{}创建记录,例子如下:
db.Model(&User{}).Create(map[string]interface{}{"Name": "jinzhu", "Age": 18,
})// batch insert from `[]map[string]interface{}{}`
db.Model(&User{}).Create([]map[string]interface{}{
{"Name": "jinzhu_1", "Age": 18},
{"Name": "jinzhu_2", "Age": 20},
})
注意map创建记录时,association不会被调用,且主键也不会自动填充。
这个就要求指定模型了。这个就做不了推断。
使用SQL表达式、Context Valuer创建记录
GORM允许使用SQL表达式插入数据,有两种方法实现这个目标。根据map[string]interface{}或自定义数据类型创建,例如:
// 通过 map 创建记录
db.Model(User{}).Create(map[string]interface{}{"Name": "jinzhu","Location": clause.Expr{SQL: "ST_PointFromText(?)", Vars: []interface{}{"POINT(100 100)"}},
})
// INSERT INTO `users` (`name`,`location`) VALUES ("jinzhu",ST_PointFromText("POINT(100 100)"));// 通过自定义类型创建记录
type Location struct {X, Y int
}// Scan 方法实现了 sql.Scanner 接口
func (loc *Location) Scan(v interface{}) error {// Scan a value into struct from database driver
}func (loc Location) GormDataType() string {return "geometry"
}func (loc Location) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {return clause.Expr{SQL: "ST_PointFromText(?)",Vars: []interface{}{fmt.Sprintf("POINT(%d %d)", loc.X, loc.Y)},
}
}type User struct {Name stringLocation Location
}db.Create(&User{Name: "jinzhu",Location: Location{X: 100, Y: 100},
})
// INSERT INTO `users` (`name`,`location`) VALUES ("jinzhu",ST_PointFromText("POINT(100 100)"))
解读:
通过map创建记录
db.Model(User{}).Create(map[string]interface{}{"Name": "jinzhu","Location": clause.Expr{SQL: "ST_PointFromText(?)", Vars: []interface{}{"POINT(100 100)"}},
})
这个是展示如何使用map[string]interface{},直接在Create方法中插入记录:
db.Model(User{}): 指定操作的模型是 User。
.Create(map[string]interface{}{…}): 通过一个映射来创建记录。这个映射指定了要插入的字段和值。
Location 字段使用了 clause.Expr 来构建一个 SQL 表达式,这里是调用了 ST_PointFromText 函数,用于生成地理位置数据。“POINT(100 100)” 是一个 WKT(Well-Known Text)表示的点,表示一个具有 X=100 和 Y=100 坐标的地理位置。
进一步解读:
clause.Expr{SQL: “ST_PointFromText(?)”, Vars: []interface{}{“POINT(100 100)”}}
1.clause.Expr 是什么?
clause.Expr 是 GORM 中用于表示原始 SQL 表达式的结构。它允许你在 GORM 的查询中直接嵌入原始 SQL 代码片段,这在 GORM 无法直接满足特定数据库操作需求时非常有用。通过使用 clause.Expr,你可以充分利用数据库特有的函数、操作或构造,同时仍然享受 GORM 提供的便利性。
2.clause.Expr 的字段
clause.Expr 结构体包含以下字段:
SQL: 一个字符串,包含了要执行的 SQL 代码片段。可以包含占位符(如 ?),用于后续替换具体的变量值。
Vars: 一个 []interface{} 切片(任意类型的切片),包含了与 SQL 字符串中占位符相对应的变量值。GORM 在执行 SQL 语句时,会将这些变量值替换到 SQL 代码片段中的占位符上。
或许现在这个例子我格式化以下,会更好理解
clause.Expr{SQL: "ST_PointFromText(?)", Vars: []interface{}{"POINT(100 100)"}
}
SQL 字段包含了一个 SQL 函数调用 ST_PointFromText(?)。ST_PointFromText 是一些数据库系统(如 PostgreSQL 的 PostGIS 扩展)提供的地理空间函数,用于根据文本表示的点(Well-Known Text,WKT 格式)创建一个地理空间数据点。
Vars 字段包含了一个切片,其中只有一个元素 “POINT(100 100)”。这个字符串是 WKT 格式的点表示,表示一个位于坐标 (100, 100) 的点。
在执行 SQL 语句时,GORM 会将 Vars 中的 “POINT(100 100)” 替换到 SQL 字段中的占位符 ? 上,最终执行的 SQL 语句会是 ST_PointFromText(‘POINT(100 100)’)。
总之,clause.Expr 提供了一种灵活的方式,使得在使用 GORM 时可以直接利用原生 SQL 语句的强大功能,尤其是在需要使用数据库特定函数或复杂查询时。
通过自定义类型创建记录
type Location struct {X, Y int
}func (loc *Location) Scan(v interface{}) error {// 实现 sql.Scanner 接口,用于从数据库读取值时如何将其转换成 `Location` 类型
}func (loc Location) GormDataType() string {return "geometry"
}func (loc Location) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {return clause.Expr{SQL: "ST_PointFromText(?)",Vars: []interface{}{fmt.Sprintf("POINT(%d %d)", loc.X, loc.Y)},
}
}
定义一个自定义类型 Location 并为其实现 GormDataType 和 GormValue 方法来插入记录:
Location 结构体定义了一个地理位置类型,包含 X 和 Y 坐标。
Scan 方法实现了 sql.Scanner 接口,允许 GORM 从数据库读取值时将其转换为 Location 类型。
GormDataType 方法返回这个类型在数据库中对应的数据类型,这里是 “geometry”。
GormValue 方法返回一个 clause.Expr,用于在插入和更新操作时如何将 Location 类型的值转换为数据库可以理解的 SQL 表达式。
最后,通过创建 User 实例并设置其 Location 字段为 Location{X: 100, Y: 100},调用 db.Create(&User{…}) 时,GORM 会使用 Location.GormValue 方法提供的 SQL 表达式来插入地理位置数据。
个人疑问解答:
使用自定义类型创建记录,这些方法是如果我要用这种方法就必须实现吗?而且要求如何实现。
回答:在 GORM 中,当你需要通过自定义类型创建记录,并且这个自定义类型需要特殊处理或者转换为数据库能理解的格式时,**确实需要实现一些特定的方法。这些方法并不是所有自定义类型都必须实现的,只有当你需要对该类型进行特殊处理时才需要。**下面是一些关键方法及其用途:
Scan(v interface{}) error
用途:实现 sql.Scanner 接口,用于从数据库读取值时将其转换成自定义类型。当 GORM 从数据库中检索字段值赋值给自定义类型时会调用这个方法。
**必要性:**如果你的自定义类型用作模型中的字段,并且这个字段从数据库读取时需要特殊处理,那么实现这个方法是必要的。
*GormValue(ctx context.Context, db gorm.DB) clause.Expr
用途:定义如何将自定义类型的值转换为 clause.Expr,即在执行数据库操作(如插入、更新)时如何将该值转换成数据库能理解的 SQL 表达式。
**必要性:**当你需要在数据库操作(如插入、更新)时对自定义类型的值进行特殊处理,比如使用数据库的函数来处理这个值,那么实现这个方法是必要的。
GormDataType() string
用途:指定自定义类型在数据库中的数据类型。这个方法告诉 GORM 在迁移(自动创建或更新数据库表结构)时应该为相应的字段使用什么数据库类型。
**必要性:**当你的自定义类型需要被映射到数据库中一个特定的数据类型,而这个数据类型无法通过 GORM 默认的类型推断得出时,实现这个方法是必要的。
高级选项
创建关联数据时,如果关联值是非零值,这些关联会被upsert,且它们的hook方法也会被调用
type CreditCard struct {gorm.ModelNumber stringUserID uint
}type User struct {gorm.ModelName stringCreditCard CreditCard
}db.Create(&User{Name: "jinzhu",CreditCard: CreditCard{Number: "411111111111"}
})
// INSERT INTO `users` ...
// INSERT INTO `credit_cards` ...
解读:
首先要理解一些概念,这些概念是理解数据库操作和ORM框架的关键。
1.什么是关联?
关联(Association)是指数据库中表与表之间的关系。在关系型数据库设计中,最常见的关联类型有三种:
一对一(One-to-One):两个表中的记录相互对应,**每个表中的记录只能在另一个表中有一个对应的记录。**例如,每个用户(User)只有一个护照(Passport),每个护照也只对应一个用户。
一对多(One-to-Many)/多对一(Many-to-One):**一个表中的记录可以对应另一个表中的多个记录,但反过来不成立。**例如,一个作者(Author)可以写多篇文章(Article),但每篇文章只能有一个作者。
**多对多(Many-to-Many):**两个表中的记录可以相互对应多个。例如,多个学生(Student)可以参加多个课程(Course),每个课程也可以有多个学生参加。
在 ORM 中,这些关系被映射到对象模型中,允许开发者以编程的方式操作这些关联,就像在处理普通的对象属性一样。
2.什么是Upsert?
Upsert 是 “update”(更新)和 “insert”(插入)两个单词的组合,指的是一种数据库操作,它会根据记录是否已经存在来决定是插入新记录还是更新现有记录。
如果记录不存在(基于某些唯一标识,如主键或唯一索引),则执行插入操作。
如果记录已存在,则执行更新操作,而不是插入一个新的重复记录。
Upsert 是一种非常有用的操作,因为它简化了需要根据记录是否存在来决定操作类型的场景,特别是在需要保持数据最新但又不想重复插入相同记录的情况下。
例子解读:
这段代码演示了如何在使用 GORM 时创建具有一对一关联的记录。具体来说,它定义了两个模型 User 和 CreditCard,其中 User 模型包含一个 CreditCard 作为它的一对一关联。然后,它创建一个 User 实例,同时包含一个 CreditCard 实例,并将它们一起插入到数据库中。
1.模型定义
type CreditCard struct {gorm.ModelNumber stringUserID uint
}type User struct {gorm.ModelName stringCreditCard CreditCard
}
CreditCard 结构体表示一个信用卡实体,包含一个 Number 字段和一个 UserID 字段。UserID 用于关联到一个 User 实体。
User 结构体表示用户实体,包含一个 Name 字段和一个 CreditCard 字段。在 GORM 中,这样的字段定义表示 User 和 CreditCard 之间存在一对一的关系。
2.创建关联数据
db.Create(&User{Name: "jinzhu",CreditCard: CreditCard{Number: "411111111111"}
})
这行代码创建了一个 User 实例,名为 “jinzhu”,并为这个用户分配了一个信用卡号为 “411111111111” 的 CreditCard 实例。
当这个 User 实例通过 db.Create(&User{…}) 被插入数据库时,GORM 会首先插入 User 记录到 users 表中,然后插入 CreditCard 记录到 credit_cards 表中。CreditCard 记录的 UserID 字段会被设置为新创建的 User 记录的 ID,从而建立它们之间的一对一关联。
关于“创建关联数据时,如果关联值是非零值,这些关联会被upsert,且它们的hook方法也会被调用”的含义
非零值(Non-zero value):在 Go 中,非零值指的是除默认零值以外的任何值。对于结构体来说,如果它的任何字段被设置了非默认值,则该结构体被认为是非零值。在这个例子中,CreditCard{Number: “411111111111”} 是一个非零值,因为它的 Number 字段被设置了。
Upsert:Upsert 是 “update” 和 “insert” 的组合,意味着如果关联记录在数据库中不存在,则会被插入(insert);如果已经存在,则会被更新(update)。
Hook 方法:GORM 提供了多个 hook 方法,如 BeforeCreate、AfterCreate 等,这些方法可以在执行数据库操作前后被自动调用。在这个上下文中,如果 CreditCard 实例是非零值并被插入或更新到数据库中,与该操作相关的 hook 方法(如果有实现的话)也会被调用。
问题解读:User创建与CreditCard的关联关系我能理解,但是CreditCard与User的关联关系是怎么创建的,是通过UserID字段吗,这个字段有什么要求吗?
回答:CreditCard 与 User 的关联关系通常是通过 UserID 字段来创建的,这种关系是数据库中一对一关系的实现方式之一,其中 UserID 字段作为外键,指向 User 表的主键字段。这样,每张 CreditCard 就可以关联到一个特定的 User。
UserID 字段的要求
数据类型匹配:UserID 字段的数据类型必须与 User 表的主键字段的数据类型相匹配。例如,如果 User 表的主键是整型(如 uint 或 int),那么 CreditCard 表的 UserID 字段也必须是整型。
外键约束:在数据库层面,UserID 字段通常会被设置为外键,指向 User 表的主键。这个外键约束确保了 CreditCard 表中的每个 UserID 值都必须在 User 表中存在,从而维护了数据的完整性。
唯一性(可选):对于一对一关系,如果你希望确保每个 User 最多只能有一张 CreditCard,则可以为 CreditCard 表的 UserID 字段添加唯一性约束。这样做可以防止一个 User 关联多张 CreditCard。
在这个例子中,CreditCard 结构体中的 UserID 字段就是建立与 User 关系的关键。在 GORM 中进行操作时(如使用 db.Create(&user) 创建记录),GORM 会自动处理这些关联关系,包括在插入 CreditCard 记录时填充 UserID 字段的值。
总结:
CreditCard 与 User 的关联关系是通过 UserID 字段实现的,该字段指向 User 表的主键。
UserID 字段需要匹配主键的数据类型,并且通常会设置为外键。
为了保持一对一关系的唯一性,可以为 UserID 字段添加唯一性约束。
在 GORM 中,通过在模型中适当定义字段和标签,可以方便地声明和操作这种一对一关系。
您也可以通过Select、Omit跳过关联保存,例如:
db.Omit("CreditCard").Create(&user)// 跳过所有关联
db.Omit(clause.Associations).Create(&user)
什么是关联保存?
在 GORM 中,“跳过关联保存”(Skipping Association Save)是一个用于在执行创建(Create)或更新(Update)操作时,选择性地忽略模型中的一些关联字段的功能。通过使用 .Omit 方法,你可以指定哪些字段或关联不应该被保存到数据库中。
跳过关联保存的用途
性能优化: 如果你只需要保存主实体而不是其关联实体,跳过关联保存可以减少数据库操作,从而提高性能。
业务逻辑需求: 在某些业务场景下,可能需要独立管理关联数据的创建和更新。例如,可能需要先创建一个用户记录,稍后根据业务逻辑单独添加或更新信用卡信息。
数据完整性: 有时关联数据可能需要通过特定的业务逻辑进行验证或处理,直接保存可能会绕过这些逻辑,导致数据不一致。
例子解读:
db.Omit(“CreditCard”).Create(&user)
这行代码在创建 user 记录时,会忽略 User 模型中的 CreditCard 字段。即使 user 对象中包含了 CreditCard 的信息,该信息也不会被保存到数据库的 credit_cards 表中。这里 “CreditCard” 是模型中定义的关联字段的名称。
db.Omit(clause.Associations).Create(&user)
这行代码在创建 user 记录时,会忽略 User 模型中的所有关联字段。clause.Associations 是一个特殊的标识符,用于表示模型中的所有关联。使用这个选项可以确保在保存 user 记录时,任何关联的 CreditCard、以及可能存在的其他关联如 Addresses、Orders 等都不会被保存。
默认值
我可以通过标签default为字段定义默认值:
type User struct {ID int64Name string `gorm:"default:galeone"`Age int64 `gorm:"default:18"`
}
插入记录到数据库时,默认值会被用于填充值为零值的字段
注意像0,’ ',false,等零值,不会将这些字段定义的默认值保存到数据库。您需要使用指针类型或Scanner/Valuer来避免这个问题,例如:
type User struct {gorm.ModelName stringAge *int `gorm:"default:18"`Active sql.NullBool `gorm:"default:true"`
}
注意 若要数据库有默认、虚拟/生成的值,你必须为字段设计default标签。若要在迁移时跳过默认值定义,你可以使用default:(-),例如:
type User struct {ID string `gorm:"default:uuid_generate_v3()"` // db funcFirstName stringLastName stringAge uint8FullName string `gorm:"->;type:GENERATED ALWAYS AS (concat(firstname,' ',lastname));default:(-);"`
}
使用虚拟/生成的值时,你可能需要禁用它的创建、更新权限,查看字段级权限获取详情。
Upsert及冲突
Gorm为不同数据库提供了兼容的Upsert支持。
import "gorm.io/gorm/clause"// 在冲突时,什么都不做
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)// 在`id`冲突时,将列更新为默认值
db.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "id"}},DoUpdates: clause.Assignments(map[string]interface{}{"role": "user"}),
}).Create(&users)
// MERGE INTO "users" USING *** WHEN NOT MATCHED THEN INSERT *** WHEN MATCHED THEN UPDATE SET ***; SQL Server
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE ***; MySQL// 使用SQL语句
db.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "id"}},DoUpdates: clause.Assignments(map[string]interface{}{"count": gorm.Expr("GREATEST(count, VALUES(count))")}),
}).Create(&users)
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `count`=GREATEST(count, VALUES(count));// 在`id`冲突时,将列更新为新值
db.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "id"}},DoUpdates: clause.AssignmentColumns([]string{"name", "age"}),
}).Create(&users)
// MERGE INTO "users" USING *** WHEN NOT MATCHED THEN INSERT *** WHEN MATCHED THEN UPDATE SET "name"="excluded"."name"; SQL Server
// INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age"; PostgreSQL
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name`=VALUES(name),`age=VALUES(age); MySQL// 在冲突时,更新除主键以外的所有列到新值。
db.Clauses(clause.OnConflict{UpdateAll: true,
}).Create(&users)
// INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age", ...;