gorm day4
- 高级查询
高级查询
智能选择字段
GORM允许通过Select方法选择特定的字段,如果你在应用程序中经常使用此功能,你也可以定义一个较小的结构体,以实现调用API时自动选择特定的字段,例如:
go
type User struct {
ID uint
Name string
Age int
Gender string
// 假设后面还有几百个字段...
}
type APIUser struct {
ID uint
Name string
}
// 查询时会自动选择 `id`, `name` 字段
db.Model(&User{}).Limit(10).Find(&APIUser{})
// SELECT `id`, `name` FROM `users` LIMIT 10
解读:
在 GORM 中使用一个结构体模型(User)去查询,但最终将查询结果映射到另一个结构体(APIUser)中,这种方法允许你从包含许多字段的数据表中仅选择和映射部分字段到结果中,这对于性能优化和数据传输减少是非常有用的。
结构体定义:
User 结构体 代表数据库中的 users 表,假设这个表有很多字段,但在这个例子中只展示了几个字段(ID, Name, Age, Gender)。
APIUser 结构体定义了你想从 User 模型中查询并返回的字段(ID, Name)。
查询操作:
db.Model(&User{}).Limit(10).Find(&APIUser{})
.Find(&APIUser{}):执行查询,并期望将查询结果映射到 APIUser 结构体切片中。这里需要注意的是,正确的用法应该是将结果映射到 APIUser 类型的切片变量,如 var apiUsers []APIUser。
对应的SQL语句:SELECT id
, name
FROM users
LIMIT 10
注意QueryFields模式会根据当前model的所有字段名称进行select
go
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
QueryFields: true,
})
db.Find(&user)
// SELECT `users`.`name`, `users`.`age`, ... FROM `users` // 带上这个选项
// Session Mode
db.Session(&gorm.Session{QueryFields: true}).Find(&user)
// SELECT `users`.`name`, `users`.`age`, ... FROM `users`
可能还是看不懂这有啥区别:
我来说一下区别,没用之前就是SELECT * FROM users,用了就是SELECT users
.name
, users
.age
, ... FROM users
。
Locking(FOR UPDATE)
GORM支持多种类型的锁,例如:
go
db.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&users)
// SELECT * FROM `users` FOR UPDATE
db.Clauses(clause.Locking{
Strength: "SHARE",
Table: clause.Table{Name: clause.CurrentTable},
}).Find(&users)
// SELECT * FROM `users` FOR SHARE OF `users`
db.Clauses(clause.Locking{
Strength: "UPDATE",
Options: "NOWAIT",
}).Find(&users)
// SELECT * FROM `users` FOR UPDATE NOWAIT
在 GORM 中,Clauses 方法允许你添加特定的 SQL 子句到查询中。在这些示例中,使用了 clause.Locking 来添加行锁定(Row Locking)到查询中,这是在处理并发数据库访问时非常有用的特性。
我认为要看懂这个首先要从语法层面知道用法:
GORM 中的 Clauses() 方法
在 GORM 中,Clauses() 方法用于向查询添加一或多个特定的 SQL 子句。这种方法提供了向生成的 SQL 语句中注入额外条件或修饰符的能力,从而使得查询更加灵活和强大。
语法解析:
Clauses() 方法接受一个或多个实现了 clause.Interface 接口的参数。GORM 预定义了许多如 clause.Where、clause.OrderBy 以及本例中的 clause.Locking 等结构体,用于表示不同类型的 SQL 子句。
使用 clause.Locking
clause.Locking 是 GORM 中用于控制行级锁(Row-Level Locking)的一个结构体,它通过 Clauses() 方法应用到查询中。clause.Locking 允许你指定锁的强度(如排他锁FOR UPDATE或共享锁FOR SHARE)和其他选项(如NOWAIT)。
例子1:基本的行锁定(FOR UPDATE)
go
db.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&users)
这行代码通过 FOR UPDATE 子句添加了一个排他锁(Exclusive Lock)到查询中。这意味着查询执行时,被选中的行将被锁定以防止其他事务修改这些行,直到当前事务完成。
Strength: "UPDATE":指定使用排他锁。这会锁定选中的行,防止其他事务读取或修改这些行,直到当前事务提交。
生成的 SQL 语句是:SELECT * FROM users FOR UPDATE
例子 2: 共享锁 (FOR SHARE)
go
db.Clauses(clause.Locking{
Strength: "SHARE",
Table: clause.Table{Name: clause.CurrentTable},
}).Find(&users)
Strength: "SHARE":指定使用共享锁,允许其他事务读取被锁定的行,但不能修改。
Table: clause.Table{Name: clause.CurrentTable}:这里通过 Table 属性指定锁定操作应用于当前查询的主表上。clause.CurrentTable 是一个占位符,表示当前操作的主表.
例子 3: FOR UPDATE NOWAIT
db.Clauses(clause.Locking{
Strength: "UPDATE",
Options: "NOWAIT",
}).Find(&users)
Options: "NOWAIT":表示如果无法立即获得锁,则不等待而直接失败。这可以避免数据库操作因等待锁而阻塞。
在这些例子中,Clauses() 方法充当了向 GORM 查询注入特定 SQL 子句的载体。通过传递 clause.Locking 结构体,你可以细致地控制查询时的行锁定行为,包括锁的类型和行为选项。这种方式使得你能够构建复杂且性能优化的数据库操作,尤其是在处理并发和数据一致性时。
子查询
什么是子查询?
子查询(Subquery)是嵌套在其他 SQL 查询中的查询。 子查询可以出现在 SELECT、FROM 或 WHERE 等子句中,允许你构建复杂的查询,如在查询的一个部分中使用另一个查询的结果。子查询通常在圆括号内指定,可以返回一个或多个值,根据其在外部查询中的用途而定。
子查询的类型
标量子查询: 返回单个值的子查询。它常用在可以放置单个值的地方,如条件表达式中。例如,用在 SELECT、WHERE 或 HAVING 子句中比较一个列和子查询返回的单一结果。
列子查询: 返回一列多个值的子查询。这种类型的子查询可以用在 IN 或 ANY 等操作符后面,用来比较列中的值与子查询返回的一系列值。
行子查询: 返回一行或多行多列的结果。这种子查询可以用在比较操作 中,特别是当需要比较多个列时。
表子查询: 返回一个完整的表。这种子查询常用在 FROM 子句中,将子查询的结果当作一个临时表来参与查询。
举个例子:
用在 WHERE 子句中的标量子查询
go
SELECT * FROM employees
WHERE salary > (
SELECT AVG(salary) FROM employees
);
这个查询返回薪水高于公司平均薪水的所有员工。
用在 FROM 子句中的表子查询
go
SELECT AVG(tmp.salary) AS avg_salary
FROM (
SELECT salary FROM employees WHERE department_id = 1
) AS tmp;
这个查询计算特定部门(ID 为 1)员工的平均薪水。
这个当时我看的不是很懂,解读一下:
1.SELECT salary FROM employees WHERE department_id = 1
这个就被称为表子查询,就是一个简单的查询,它从 employees 表中选择 department_id 为 1 的所有员工的薪水。这里的 department_id = 1 表明我们只对特定部门(假设为部门ID为1的部门)的员工感兴趣。
2.(...) AS tmp(临时表)
将上面的查询放在括号中,后面跟着 AS tmp,表示整个括号内的查询结果被视为一个临时表 ,这个临时表在外层查询中被命名为 tmp。tmp 是一个别名(alias),用于在外层查询中引用这个临时表的结果。
3.AVG(tmp.salary) AS avg_salary
在外层查询中,我们对这个临时表 tmp 使用了 AVG() 函数来计算平均薪水 。tmp.salary 指的是临时表 tmp 中的 salary 列。 因为 tmp 实际上就是内嵌查询的结果集,所以你可以像引用真实表中的列一样引用它的列。
AS avg_salary 是对 AVG(tmp.salary) 计算结果的列名进行重命名。 这意味着结果集中这一列的名称将是 avg_salary。
现在看懂这个例子就不难了,内部的子查询结果被当作成一张临时表,然后命名为tmp,然后再对这个tmp做一次查询,这个就叫做外层查询。自己体会一下,这种就是表子查询。
用在 SELECT 子句中的子查询
go
SELECT name, (
SELECT COUNT(*) FROM orders WHERE orders.customer_id = customers.id
) AS orders_count
FROM customers;
这个查询为每个客户返回他们的订单数量。
我还是有点看不懂,这里做个解读:
这个 SQL 例子展示了如何使用子查询来为每个客户计算他们的订单数量。这是通过在 SELECT 语句中嵌入另一个 SELECT 语句(即子查询)来实现的。
主查询:
go
SELECT name, (...) AS orders_count FROM customers;
这是外层的查询,它从 customers 表中选择所有客户的 name 字段。
对于每个客户,它还尝试计算一个名为 orders_count 的值,这个值是通过内嵌的子查询计算得到的。
子查询
go
(SELECT COUNT(*) FROM orders WHERE orders.customer_id = customers.id)
这是嵌入在主查询中的子查询,它为每个正在处理的客户计算订单数量。
COUNT(*) 函数计算 orders 表中满足特定条件的行数。 在这个例子中,条件是 orders.customer_id = customers.id,这意味着我们只计算那些 customer_id 与当前正在处理的客户的 id 相匹配的订单行数。
子查询针对主查询中每个客户的行执行一次。这种类型的子查询称为相关子查询 ,因为它使用了外层查询(主查询)中的值(在这里是 customers.id)。
举个例子:
go
假设 customers 表有如下数据:
id name
1 Alice
2 Bob
go
并且 orders 表有如下数据:
id customer_id
1 1
2 1
3 2
执行这个查询后,你将得到类似以下的结果:
go
name orders_count
Alice 2
Bob 1
子查询在GORM中:
子查询可以嵌套在查询中,GORM允许 在使用*gorm.DB对象作为参数时生成子查询
go
db.Where("amount > (?)", db.Table("orders").Select("AVG(amount)")).Find(&orders)
// SELECT * FROM "orders" WHERE amount > (SELECT AVG(amount) FROM "orders");
subQuery := db.Select("AVG(age)").Where("name LIKE ?", "name%").Table("users")
db.Select("AVG(age) as avgage").Group("name").Having("AVG(age) > (?)", subQuery).Find(&results)
// SELECT AVG(age) as avgage FROM `users` GROUP BY `name` HAVING AVG(age) > (SELECT AVG(age) FROM `users` WHERE name LIKE "name%")
解读:
例子1:使用子查询过滤主查询的结果
go
db.Where("amount > (?)", db.Table("orders").Select("AVG(amount)")).Find(&orders)
子查询部分:db.Table("orders").Select("AVG(amount)") 这部分是一个子查询,它从 orders 表中计算所有订单的平均金额(AVG(amount))。
主查询:db.Where("amount > (?)", ...) 这部分是主查询,它查询 orders 表中金额(amount)大于子查询计算出的平均金额的所有订单。
结果:生成的 SQL 语句是 SELECT * FROM "orders" WHERE amount > (SELECT AVG(amount) FROM "orders");。这个查询返回 orders 表中,订单金额超过平均订单金额的所有订单。
例子2:在 HAVING 子句中使用子查询
go
subQuery := db.Select("AVG(age)").Where("name LIKE ?", "name%").Table("users")
db.Select("AVG(age) as avgage").Group("name").Having("AVG(age) > (?)", subQuery).Find(&results)
子查询定义:db.Select("AVG(age)").Where("name LIKE ?", "name%").Table("users") 这部分定义了一个子查询,它计算所有名字包含 "name" 的用户的平均年龄。
主查询:在主查询中,使用了 Group("name") 对 users 表的记录按照 name 进行分组,并计算每个分组的平均年龄(AVG(age) as avgage)。
HAVING 子句:在 Having("AVG(age) > (?)", subQuery) 中,主查询使用了 HAVING 子句来过滤那些组的平均年龄大于子查询计算出的平均年龄的组。
结果:生成的 SQL 是 SELECT AVG(age) as avgage FROM users GROUP BY name HAVING AVG(age) > (SELECT AVG(age) FROM users WHERE name LIKE "name%")。这个查询返回每个 name 分组的平均年龄,并且只包括那些平均年龄大于名字包含 "name" 的所有用户平均年龄的分组。
From 子查询
GORM允许您再Table方法中通过FROM子句使用子查询,例如:
go
db.Table("(?) as u", db.Model(&User{}).Select("name", "age")).Where("age = ?", 18}).Find(&User{})
// SELECT * FROM (SELECT `name`,`age` FROM `users`) as u WHERE `age` = 18
subQuery1 := db.Model(&User{}).Select("name")
subQuery2 := db.Model(&Pet{}).Select("name")
db.Table("(?) as u, (?) as p", subQuery1, subQuery2).Find(&User{})
// SELECT * FROM (SELECT `name` FROM `users`) as u, (SELECT `name` FROM `pets`) as p
例子1: 使用单个子查询
go
db.Table("(?) as u", db.Model(&User{}).Select("name", "age")).Where("age = ?", 18}).Find(&User{})
这个例子中,首先构造了一个子查询:db.Model(&User{}).Select("name", "age")。这个子查询从 users 表中选择 name 和 age 两列。
然后,子查询被作为一个临时表 u 引入到外层查询中:.Table("(?) as u", ...)
在外层查询中,使用了 Where("age = ?", 18) 来筛选年龄等于 18 的记录。
最后,使用 Find(&User{}) 来执行查询并将结果映射到 User 结构体中。
go
例1对应的SQL语句
SELECT * FROM (SELECT `name`, `age` FROM `users`) as u WHERE `age` = 18
例子 2: 结合两个子查询
go
subQuery1 := db.Model(&User{}).Select("name")
subQuery2 := db.Model(&Pet{}).Select("name")
db.Table("(?) as u, (?) as p", subQuery1, subQuery2).Find(&User{})
这个例子中,定义了两个子查询 subQuery1 和 subQuery2。subQuery1 从 users 表中选择 name,而 subQuery2 从 pets 表中选择 name。
这两个子查询随后被引入到外层查询中,分别作为临时表 u 和 p:.Table("(?) as u, (?) as p", subQuery1, subQuery2)
使用 Find(&User{}) 执行外层查询,尽管这里的用法可能有点混淆,因为结果映射到了 User 结构体,而实际 SQL 语句似乎意图是联合两个不同表的查询结果。
go
例子2对应的sql
SELECT * FROM (SELECT `name` FROM `users`) as u, (SELECT `name` FROM `pets`) as p
GROUP条件
使用GROUP调价你可以更轻松的编写复杂SQL
go
db.Where(
db.Where("pizza = ?", "pepperoni").Where(db.Where("size = ?", "small").Or("size = ?", "medium")),
).Or(
db.Where("pizza = ?", "hawaiian").Where("size = ?", "xlarge"),
).Find(&Pizza{}).Statement
// SELECT * FROM `pizzas` WHERE (pizza = "pepperoni" AND (size = "small" OR size = "medium")) OR (pizza = "hawaiian" AND size = "xlarge")
这个例子我觉得在展示如何在GORM中构建一个复杂的查询条件,涉及到AND、OR逻辑以及如何组合它们。
看例子:
查询分解
第一部分 - 辣香肠披萨,小或中尺寸
go
db.Where("pizza = ?", "pepperoni").Where(db.Where("size = ?", "small").Or("size = ?", "medium"))
这部分首先指定了披萨种类为辣香肠(pepperoni)
然后,它要求尺寸为小(small)或中(medium)。这是通过将两个 Where 条件用 .Or 方法连接起来实现的,表示这两个条件中的任一个满足即可。
第二部分:
go
.Or(db.Where("pizza = ?", "hawaiian").Where("size = ?", "xlarge"))
.Or 方法用于将前面的查询条件与新的条件组合,表示这是一个新的可选分支。
这部分指定了披萨种类为夏威夷(hawaiian)并且尺寸为特大(xlarge)
组合条件:
这两部分条件通过 OR 逻辑连接,意味着满足任一条件的记录都会被选出。
go
SELECT * FROM `pizzas` WHERE (pizza = "pepperoni" AND (size = "small" OR size = "medium")) OR (pizza = "hawaiian" AND size = "xlarge")
生成的SQL
带多个列的IN
带多个列的IN查询
go
db.Where("(name, age, role) IN ?", [][]interface{}{{"jinzhu", 18, "admin"}, {"jinzhu2", 19, "user"}}).Find(&users)
// SELECT * FROM users WHERE (name, age, role) IN (("jinzhu", 18, "admin"), ("jinzhu 2", 19, "user"));
解读:
这个例子主要说明了如何执行包含多个列的查询,IN查询通常用于指定某个字段的多个可能值 ,但在这个高级用法中,它被用来同时指定多个字段的组合值。这种查询对于检索符合一组特定条件的记录非常有用。
go
db.Where("(name, age, role) IN ?", [][]interface{}{{"jinzhu", 18, "admin"}, {"jinzhu2", 19, "user"}}).Find(&users)
(name, age, role) IN ?:这部分定义了查询条件。它表示你想要选择那些在 name、age 和 role 三个字段上同时匹配给定值的记录。这里,查询条件是一个组合,涉及到三个字段。
[][]interface{}{{"jinzhu", 18, "admin"}, {"jinzhu2", 19, "user"}}:这是一个切片的切片,其中每个内部切片代表一组要匹配的值。 在这个例子中,我们有两组值:
第一组是 "jinzhu"(名字)、18(年龄)和 "admin"(角色)。
第二组是 "jinzhu2"、19 和 "user"。
生成的SQL:
go
SELECT * FROM users WHERE (name, age, role) IN (("jinzhu", 18, "admin"), ("jinzhu 2", 19, "user"));
总结:
这种查询非常强大,能够让你精确地指定多维的过滤条件,从而检索出符合一系列特定属性组合的记录。这在处理复杂的数据检索需求时非常有用。
命名参数
GORM支持sql.NamedArg和map[string]interface{}形式的命名参数,例如:
go
db.Where("name1 = @name OR name2 = @name", sql.Named("name", "jinzhu")).Find(&user)
// SELECT * FROM `users` WHERE name1 = "jinzhu" OR name2 = "jinzhu"
db.Where("name1 = @name OR name2 = @name", map[string]interface{}{"name": "jinzhu"}).First(&user)
// SELECT * FROM `users` WHERE name1 = "jinzhu" OR name2 = "jinzhu" ORDER BY `users`.`id` LIMIT 1
解读:
这两个例子展示了在GORM中如何使用命名参数进行查询,命名参数是SQL查询中的一个特性,允许你在查询字符串中使用占位符,并通过一个参数对象为这些占位符提供具体的值,这种方式使得查询更加易读和维护,特别是在查询条件比较复杂时。
例子 1: 使用 sql.Named :
go
db.Where("name1 = @name OR name2 = @name", sql.Named("name", "jinzhu")).Find(&user)
这里,"name1 = @name OR name2 = @name" 是 SQL 查询字符串 ,其中 @name 是一个命名参数的占位符 。
sql.Named("name", "jinzhu") 为占位符 @name 提供了值 "jinzhu"。这表示无论 name1 还是 name2 字段的值为 "jinzhu" 的记录都将被选出。这里就是name对应了jinzhu 。
.Find(&user) 执行查询,并将结果映射到 user 变量中。注意这里 user 应该是一个结构体实例或者结构体切片的指针,用于接收查询结果。
生成的 SQL 是:SELECT * FROM users WHERE name1 = "jinzhu" OR name2 = "jinzhu"。
例子 2: 使用 map[string]interface{} :
go
db.Where("name1 = @name OR name2 = @name", map[string]interface{}{"name": "jinzhu"}).First(&user)
在这个例子中,使用了 map[string]interface{} 来代替 sql.Named,以提供命名参数的值。键 "name" 对应于查询字符串中的 @name 占位符。
.First(&user) 执行查询,并将结果映射到 user 变量中,但这次只获取符合条件的第一条记录。与 .Find(&user) 类似,但 .First(&user) 会附加一个 LIMIT 1 条件到 SQL 查询中,以确保只返回一条记录。
生成的 SQL 是:SELECT * FROM users WHERE name1 = "jinzhu" OR name2 = "jinzhu" ORDER BY users.id LIMIT 1。
总结:
这两种的用法效果是一样的。并且两种用法都是非常清晰的。
命名参数的好处:
1.可读性,2.维护性,3,灵活性。
Find至map
GORM允许扫描结果至map[string]interface{}或[]map[string]interface{},此时别忘了指定Model或table,例如:
go
result := map[string]interface{}{}
db.Model(&User{}).First(&result, "id = ?", 1)
var results []map[string]interface{}
db.Table("users").Find(&results)
例子1:查询单条记录
go
result := map[string]interface{}{}
db.Model(&User{}).First(&result, "id = ?", 1)
这个就比较简单,这行代码尝试查询 User 模型的表中 id 等于 1 的第一条记录。
查询的结果被映射到一个名为 result 的 map[string]interface{} 变量中。这意味着查询出的每一列都将作为键值对存在于 result 中,其中键是列名 ,值是列的值。
例如,如果 users 表中 id 为 1 的记录有 name 为 "John Doe" 和 age 为 30,则 result 将是 {"id": 1, "name": "John Doe", "age": 30}。
例子 2: 查询多条记录
go
var results []map[string]interface{}
db.Table("users").Find(&results)
这行代码查询 users 表中的所有记录。
关于这个result的数据类型,这个是一个map类型的切片,切片的元素类型是map[string]interface{}.
这样就实现了每条记录都以一个映射(字典)的形式表示,每个映射包含了一条记录的所有列的数据。
例如,如果 users 表中有两条记录,第一条记录的 name 为 "John Doe"、age 为 30,第二条记录的 name 为 "Jane Doe"、age 为 25,则 results 将是 [{"id": 1, "name": "John Doe", "age": 30}, {"id": 2, "name": "Jane Doe", "age": 25}]。
FirstOrInit
获取第一条匹配的记录,或者根据给定的条件初始化一个实例(仅支持struct和map条件)。
go
// 未找到 user,则根据给定的条件初始化一条记录
db.FirstOrInit(&user, User{Name: "non_existing"})
// user -> User{Name: "non_existing"}
// 找到了 `name` = `jinzhu` 的 user
db.Where(User{Name: "jinzhu"}).FirstOrInit(&user)
// user -> User{ID: 111, Name: "Jinzhu", Age: 18}
// 找到了 `name` = `jinzhu` 的 user
db.FirstOrInit(&user, map[string]interface{}{"name": "jinzhu"})
// user -> User{ID: 111, Name: "Jinzhu", Age: 18}
解读:
FirstOrInit 方法是一个非常有用的功能,它尝试根据指定的条件查询第一条记录。如果找到了记录,它会将查询结果映射到提供的模型实例中。如果没有找到记录,则它会根据提供的条件初始化模型实例,但不会在数据库中创建新记录。这对于需要查询数据,如果数据不存在则使用默认值的场景特别有用。
示例 1: 使用结构体条件初始化记录
go
db.FirstOrInit(&user, User{Name: "non_existing"})
这行代码尝试在数据库中查找名为 "non_existing" 的用户。
因为这个名字的用户不存在,FirstOrInit 将不会在数据库中创建新记录,而是初始化 user 变量为 User{Name: "non_existing"}。
最终,user 变量会包含一个 Name 字段值为 "non_existing" 的 User 结构体实例。
示例 2: 使用结构体条件查询记录
go
db.Where(User{Name: "jinzhu"}).FirstOrInit(&user)
这行代码尝试在数据库中查找名为 "jinzhu" 的用户。
假设找到了这样一条记录,FirstOrInit 方法将查询结果映射到 user 变量中。
假设找到的用户 ID 为 111,年龄为 18,那么 user 变量将被赋值为 User{ID: 111, Name: "Jinzhu", Age: 18}。
示例 3: 使用字典条件查询记录
go
db.FirstOrInit(&user, map[string]interface{}{"name": "jinzhu"})
这行代码的功能与示例 2 相同,但这次使用的是字典类型的条件,而不是结构体。
它同样尝试在数据库中查找名为 "jinzhu" 的用户,并根据查询结果初始化 user 变量。
如果找到了符合条件的用户记录,user 变量将被初始化为该用户的信息。假设找到的用户信息与示例 2 中相同,则 user 变量同样会被赋值为 User{ID: 111, Name: "Jinzhu", Age: 18}。
总结:需要注意的是,即使条件中提供了多个字段,FirstOrInit 也只会在初始化时使用这些条件字段,不会在数据库中创建新记录。
如果没有找到记录,可以使用包含更多的属性的结构体初始化user,Attrs 不会被用于生成查询SQL。
Attrs 不会被用于生成查询 SQL 的含义
当你在使用 FirstOrCreate 方法时加入 Attrs,这里的 Attrs 提供的属性值仅用于创建记录时的默认值,并不会被用来改变查找记录时的查询条件 。换句话说,Attrs 中指定的任何字段和值都不会影响 FirstOrCreate 方法执行的 SELECT 查询,它们只会在需要创建新记录时被用作记录的初始化值。
这种设计使得你可以明确区分查找条件(通过 Where 或直接在 FirstOrCreate 中指定)和新记录的默认值(通过 Attrs 指定),提供了更好的灵活性和控制力。
go
// 未找到 user,则根据给定的条件以及 Attrs 初始化 user
db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrInit(&user)
// SELECT * FROM USERS WHERE name = 'non_existing' ORDER BY id LIMIT 1;
// user -> User{Name: "non_existing", Age: 20}
// 未找到 user,则根据给定的条件以及 Attrs 初始化 user
db.Where(User{Name: "non_existing"}).Attrs("age", 20).FirstOrInit(&user)
// SELECT * FROM USERS WHERE name = 'non_existing' ORDER BY id LIMIT 1;
// user -> User{Name: "non_existing", Age: 20}
// 找到了 `name` = `jinzhu` 的 user,则忽略 Attrs
db.Where(User{Name: "Jinzhu"}).Attrs(User{Age: 20}).FirstOrInit(&user)
// SELECT * FROM USERS WHERE name = jinzhu' ORDER BY id LIMIT 1;
// user -> User{ID: 111, Name: "Jinzhu", Age: 18}
解读:
FirstOrInit 方法与 Attrs 的结合使用为处理数据库查询和记录初始化提供了一个灵活的机制 。这里的关键点在于如何利用 Attrs 为初始化提供默认值 ,特别是当查询的记录不存在时。
示例 1: 使用结构体作为 Attrs:
go
db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrInit(&user)
查询条件:Where(User{Name: "non_existing"}) 指定查询条件为寻找 name 字段值为 "non_existing" 的用户。
Attrs 使用:.Attrs(User{Age: 20}) 指定如果需要初始化 user(即当查询不到记录时),则使用这里提供的属性作为默认值。在这个例子中,如果没有找到符合条件的用户,则会初始化一个 User 结构体,其中 Name 字段为 "non_existing"(来自 Where 条件),Age 字段为 20(来自 Attrs)。
FirstOrInit 动作:执行查询,如果找不到记录,则根据 Where 条件和 Attrs 提供的默认值初始化 user。生成的 user 变量将是 User{Name: "non_existing", Age: 20}。
示例 2: 使用键值对作为 Attrs
go
db.Where(User{Name: "non_existing"}).Attrs("age", 20).FirstOrInit(&user)
这个例子与第一个示例类似,但是 Attrs 是通过键值对 "age", 20 来指定默认值的,这是另一种指定 Attrs 的方式。效果与使用结构体相同,未找到记录时会初始化 user 为 User{Name: "non_existing", Age: 20}。
这个例子相比例1可能显得方便快捷,但是例1的好处就是结构明显。
示例 3: 记录存在时 Attrs 被忽略
go
db.Where(User{Name: "Jinzhu"}).Attrs(User{Age: 20}).FirstOrInit(&user)
查询条件:Where(User{Name: "Jinzhu"}) 尝试找到 name 为 "Jinzhu" 的用户。
Attrs 使用:尽管指定了 Attrs(User{Age: 20}) 作为默认值,但这些默认值只在初始化时使用(即当记录不存在时)。
FirstOrInit 动作:因为找到了名为 "Jinzhu" 的用户,Attrs 提供的默认值将被忽略。假设数据库中这条记录的 Age 是 18,那么 user 将被初始化为数据库中的记录,即 User{ID: 111, Name: "Jinzhu", Age: 18}。
总结:FirstOrInit 方法结合 Attrs 提供了一种强大的方式来查询记录,如果记录不存在,则根据提供的条件和默认属性初始化一个新的结构体实例。
Assign:不管是否找到记录,Assign都会将属性赋值给struct,但这些属性不会被用于生成查询SQL,也不会被保存到数据库。
go
// 未找到 user,根据条件和 Assign 属性初始化 struct
db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrInit(&user)
// user -> User{Name: "non_existing", Age: 20}
// 找到 `name` = `jinzhu` 的记录,依然会更新 Assign 相关的属性
db.Where(User{Name: "Jinzhu"}).Assign(User{Age: 20}).FirstOrInit(&user)
// SELECT * FROM USERS WHERE name = jinzhu' ORDER BY id LIMIT 1;
// user -> User{ID: 111, Name: "Jinzhu", Age: 20}
解读:
GORM 的 Assign 方法与 FirstOrInit 方法一起使用时,提供了一种无论记录是否存在都会应用特定属性值的机制。与 Attrs 不同,Assign 指定的属性值将在找到记录的情况下更新给定的模型实例,如果没有找到记录,则会在初始化模型实例时应用这些值。
示例 1: 记录不存在时使用 Assign
go
db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrInit(&user)
查询条件:Where(User{Name: "non_existing"}) 指定了查找 name 字段值为 "non_existing" 的用户。
Assign 使用:.Assign(User{Age: 20}) 表明无论查询结果如何,都应将 Age 设置为 20。因为这个名字的用户不存在,FirstOrInit 将根据给定的条件和 Assign 提供的属性值初始化 user 实例。所以,user 将初始化为 User{Name: "non_existing", Age: 20}。
FirstOrInit 动作:执行查询,如果找不到记录,则根据 Where 条件和 Assign 提供的属性值初始化 user。
示例 2: 记录存在时更新 Assign 相关的属性
go
db.Where(User{Name: "Jinzhu"}).Assign(User{Age: 20}).FirstOrInit(&user)
查询条件:Where(User{Name: "Jinzhu"}) 尝试找到 name 为 "Jinzhu" 的用户。
Assign 使用:即使找到了记录,Assign(User{Age: 20}) 也会将 Age 的值设置为 20。这与 Attrs 的行为不同,Attrs 只在记录不存在时应用默认值。
FirstOrInit 动作:因为找到了名为 "Jinzhu" 的用户,FirstOrInit 方法将查询到的记录映射到 user 实例,并应用 Assign 指定的属性值。即使数据库中该用户的 Age 原本不是 20,user 实例中的 Age 也会被更新为 20。最终,user 变量的状态会反映这个更新,例如 User{ID: 111, Name: "Jinzhu", Age: 20}。
总结:
Assign 方法在与 FirstOrInit 一起使用时,为处理记录的初始化和更新提供了强大的灵活性。它确保了无论记录是否存在,特定的属性值都会被应用到模型实例中:
如果记录不存在, FirstOrInit 会根据查询条件和 Assign 提供的属性值初始化一个新的模型实例。
如果记录存在, FirstOrInit 会更新模型实例中 Assign 指定的属性值,即使这些值与数据库中的当前值不同。
FirstOrCreate
获取第一条匹配的记录,或者根据给定的条件创建一条新纪录(仅支持struct和map条件)
go
// 未找到 user,则根据给定条件创建一条新纪录
db.FirstOrCreate(&user, User{Name: "non_existing"})
// INSERT INTO "users" (name) VALUES ("non_existing");
// user -> User{ID: 112, Name: "non_existing"}
// 找到了 `name` = `jinzhu` 的 user
db.Where(User{Name: "jinzhu"}).FirstOrCreate(&user)
// user -> User{ID: 111, Name: "jinzhu", "Age": 18}
解读:
GORM 的 FirstOrCreate 方法是一个非常实用的功能,它尝试根据指定的条件查找一条记录。如果找到了记录,它会将找到的记录映射到给定的模型实例中。如果没有找到记录,它会使用相同的条件创建一条新记录,并将新记录映射到模型实例中。这对于"如果不存在则创建"这类操作非常方便,省去了手动检查记录是否存在的步骤。
示例 1: 记录不存在时创建新记录
go
db.FirstOrCreate(&user, User{Name: "non_existing"})
操作说明:这个调用尝试在数据库中找到一个名为 "non_existing" 的用户。
结果:因为这样的用户不存在,FirstOrCreate 将根据提供的条件创建一条新的用户记录,其中 name 字段值为 "non_existing"。
数据库操作:执行了一个 INSERT 操作来创建新记录:INSERT INTO "users" (name) VALUES ("non_existing");。
模型实例更新:user 变量将被更新为新创建的记录的详情,包括数据库自动生成的 ID(比如说 112)和名字 "non_existing"。
示例 2: 记录存在时获取记录
go
db.Where(User{Name: "jinzhu"}).FirstOrCreate(&user)
操作说明:这个调用尝试在数据库中找到一个名为 "jinzhu" 的用户。
结果:因为找到了这样的用户,FirstOrCreate 不会创建新记录,而是将找到的记录的详情映射到 user 变量中。
数据库操作:没有执行 INSERT 操作,因为已经存在符合条件的记录。
模型实例更新:user 变量将包含找到的用户的详细信息,比如 ID、Name 和其他可能从数据库中检索到的字段。
总结:
FirstOrCreate 方法提供了一种高效的方式来确保数据库中存在特定的记录。如果记录已存在,它会返回这条记录;如果不存在,则会创建新的记录。这种方法特别适用于处理那些需要确保数据唯一性的场景,如用户注册、商品编码等,减少了代码量并提高了数据处理的效率。
如果没有找到记录,可以使用包含更多属性的结构体创建记录,Attrs不会被用于生成查询SQL
go
// 未找到 user,根据条件和 Assign 属性创建记录
db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrCreate(&user)
// SELECT * FROM users WHERE name = 'non_existing' ORDER BY id LIMIT 1;
// INSERT INTO "users" (name, age) VALUES ("non_existing", 20);
// user -> User{ID: 112, Name: "non_existing", Age: 20}
// 找到了 `name` = `jinzhu` 的 user,则忽略 Attrs
db.Where(User{Name: "jinzhu"}).Attrs(User{Age: 20}).FirstOrCreate(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;
// user -> User{ID: 111, Name: "jinzhu", Age: 18}
解读:
这两个例子使用 GORM 的 FirstOrCreate 方法,结合 Attrs 来处理数据库记录的查找或创建。FirstOrCreate 方法会根据提供的条件查找记录,如果找到了记录,就返回这条记录;如果没有找到,则根据提供的条件和 Attrs 创建新的记录 。
示例 1: 记录不存在时使用 Attrs 创建记录
go
db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrCreate(&user)
查询条件: Where(User{Name: "non_existing"}) 指定了查找 name 字段值为 "non_existing" 的用户。
Attrs 使用: .Attrs(User{Age: 20}) 指定了如果记录需要被创建时的默认值。这里指定 Age 为 20。
操作: 因为没有找到名为 "non_existing" 的用户,所以根据 Where 和 Attrs 指定的条件创建了新的用户记录,并将这条记录的详情映射到 user 变量中。 生成的 SQL 包括了一个 SELECT 语句来查找记录,和一个 INSERT 语句来创建新记录。
示例 2: 记录存在时 Attrs 被忽略
go
db.Where(User{Name: "jinzhu"}).Attrs(User{Age: 20}).FirstOrCreate(&user)
查询条件: 查找 name 字段值为 "jinzhu" 的用户。
Attrs 使用: 尽管指定了 Age 为 20 作为默认值,但这个 Attrs 只有在创建新记录时才会被使用。
操作: 因为找到了名为 "jinzhu" 的用户,FirstOrCreate 直接将找到的记录映射到 user 变量中,Attrs 提供的默认值在这种情况下被忽略。
Attrs 不会被用于生成查询 SQL 的含义
我简单理解就是:Attrs不影响查询操作。
不管是否找到记录,Assign都会将属性赋值给struct,并将结果写回数据库。
go
// 未找到 user,根据条件和 Assign 属性创建记录
db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrCreate(&user)
// SELECT * FROM users WHERE name = 'non_existing' ORDER BY id LIMIT 1;
// INSERT INTO "users" (name, age) VALUES ("non_existing", 20);
// user -> User{ID: 112, Name: "non_existing", Age: 20}
// 找到了 `name` = `jinzhu` 的 user,依然会根据 Assign 更新记录
db.Where(User{Name: "jinzhu"}).Assign(User{Age: 20}).FirstOrCreate(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;
// UPDATE users SET age=20 WHERE id = 111;
// user -> User{ID: 111, Name: "jinzhu", Age: 20}
这个例子我理解了,这里就不解读了。仔细看看还是很简单的。
优化器、索引提示
优化器提示用于控制查询优化器选择某个查询执行计划,GORM通过gorm.io/hints提供支持。
这里对这些名词做解读:
优化器提示 (Optimizer Hints)是一种给数据库查询优化器(Query Optimizer)提供额外指导的机制 ,用来影响数据库如何执行 SQL 查询。优化器提示允许开发者向数据库提出建议或强制执行特定的查询计划,比如使用或忽略特定的索引,改变 JOIN 操作的顺序,或限制查询的执行时间等。这些提示不会改变查询的逻辑结果,但可以优化查询的执行性能或行为。
为什么使用优化器提示?
性能优化: 在某些情况下,数据库的自动查询优化器可能无法选择最优的执行计划,特别是在处理复杂查询或大数据集时。通过使用优化器提示,开发者可以手动指导优化器采取更有效的执行路径。
查询行为控制 :优化器提示可以用来控制查询的特定行为 ,比如限制查询的最大执行时间,这对于防止长时间运行的查询消耗过多资源非常有用。
常见的优化器提示
索引提示:指导优化器使用或避免使用特定的索引。
JOIN 顺序:建议优化器以特定的顺序执行 JOIN 操作。
查询执行时间:MySQL 5.7.8+ 引入了 MAX_EXECUTION_TIME(N) 提示,用于限制查询的最大执行时间为 N 毫秒。
并行查询:在支持并行执行的数据库中,可以通过提示控制查询的并行度。
使用优化器提示的注意事项
数据库依赖:不同的数据库管理系统支持不同的优化器提示,甚至在同一数据库的不同版本之间也可能存在差异。使用前需要检查数据库文档以确认支持的提示及其语法。
谨慎使用:虽然优化器提示可以提升查询性能,但不当使用可能导致性能下降或查询计划变得不可预测。应该基于充分的测试和性能分析来使用这些提示。
维护成本:随着数据库数据量和结构的变化,原本优化的提示可能变得不再适用,需要定期审查和调整。
小例子:
go
SELECT /*+ MAX_EXECUTION_TIME(1000) */ * FROM orders WHERE order_date >= '2021-01-01';
这个示例中,MAX_EXECUTION_TIME(1000) 是一个优化器提示,用于告诉数据库这个查询的最大执行时间不应超过1000毫秒。
总结:优化器提示是数据库性能调优的一个强大工具,但需要根据具体的应用场景和数据库特性谨慎使用。
在gorm中使用优化器提示:
解读:
go
import "gorm.io/hints"
db.Clauses(hints.New("MAX_EXECUTION_TIME(10000)")).Find(&User{})
// SELECT * /*+ MAX_EXECUTION_TIME(10000) */ FROM `users`
这个例子展示了如何在 GORM 中使用 SQL 优化器提示(Optimizer Hints),特别是通过 hints.New 方法添加自定义的优化器提示。优化器提示是一种告诉数据库查询优化器如何优化查询的方式,可以用于影响查询执行计划而无需更改查询本身的语义。
这个例子中使用了 gorm.io/hints 包来添加一个优化器提示 MAX_EXECUTION_TIME(10000), 这个提示的作用是限制查询的最大执行时间为 10000 毫秒(10 秒)。这意味着如果查询执行时间超过了这个限制,数据库会尝试停止查询。
Clauses(hints.New(...)): 通过 Clauses 方法将优化器提示应用到接下来的查询中。
Find(&User{}):执行查询,查询 users 表并将结果映射到 User 的实例或切片中。查询的 SQL 语句将包含优化器提示:SELECT * /*+ MAX_EXECUTION_TIME(10000) */ FROM users。
需要注意的是,不是所有数据库系统都支持优化器提示,而且不同数据库支持的优化器提示可能各不相同。示例中的 MAX_EXECUTION_TIME 是 MySQL 特有的功能。
索引提示允许传递索引提示到数据库,以防止查询计划器出现混乱。
go
import "gorm.io/hints"
db.Clauses(hints.UseIndex("idx_user_name")).Find(&User{})
// SELECT * FROM `users` USE INDEX (`idx_user_name`)
db.Clauses(hints.ForceIndex("idx_user_name", "idx_user_id").ForJoin()).Find(&User{})
// SELECT * FROM `users` FORCE INDEX FOR JOIN (`idx_user_name`,`idx_user_id`)"
解读:
在数据库查询过程中,索引是用来提高数据检索速度的一种数据结构。 数据库的查询计划器(或查询优化器)负责选择查询执行的最优路径,包括决定是否使用索引以及使用哪个索引。然而,在某些情况下,查询优化器可能不会选择最佳的索引,导致查询性能下降。为了解决这个问题,许多数据库提供了索引提示(Index Hints),允许开发者在 SQL 查询中直接指定使用或忽略特定的索引。 这就是所谓的"防止查询计划器出现混乱"。
索引提示的使用
在 GORM 中,可以通过 gorm.io/hints 包提供的功能向数据库发送索引提示。这些提示被用来明确告诉数据库查询优化器在执行特定查询时应该使用或强制使用哪些索引。
示例 1: 使用索引
go
import "gorm.io/hints"
db.Clauses(hints.UseIndex("idx_user_name")).Find(&User{})
这个示例通过 hints.UseIndex("idx_user_name") 指定在查询 users 表时应该使用 idx_user_name 索引。
生成的 SQL 语句是:SELECT * FROM users USE INDEX (idx_user_name), 这指示数据库在查询 users 表时优先使用 idx_user_name 索引。
示例 2: 强制使用多个索引,并指定索引用途
go
import "gorm.io/hints"
db.Clauses(hints.ForceIndex("idx_user_name", "idx_user_id").ForJoin()).Find(&User{})
这个示例通过 hints.ForceIndex("idx_user_name", "idx_user_id").ForJoin() 指定在执行连接(JOIN)操作时强制使用 idx_user_name 和 idx_user_id 这两个索引。
生成的 SQL 语句是:SELECT * FROM users FORCE INDEX FOR JOIN (idx_user_name, idx_user_id)。这不仅指示数据库在查询 users 表时强制使用这两个索引,而且还明确了这个强制使用的上下文是在进行 JOIN 操作时。
FORjoin是什么?
ForJoin() 是用于在数据库查询中明确指定索引提示应用场景的一个方法。它是 GORM 中 gorm.io/hints 包提供的一部分,用于构造和发送特定的索引提示给数据库。当使用 ForJoin() 方法时,它告诉数据库查询优化器该索引提示仅适用于 JOIN 操作。
用途
在 SQL 查询中,JOIN 操作是用来结合两个或多个表中的行的常用方法。 JOIN 操作的性能可能会因为使用或不使用特定的索引而显著不同。ForJoin() 方法允许开发者向数据库优化器提供额外的指导,推荐或强制在执行 JOIN 操作时使用特定的索引, 以提高查询效率和性能。
示例:
go
db.Clauses(hints.ForceIndex("idx_user_name", "idx_user_id").ForJoin()).Find(&User{})
这里,ForceIndex("idx_user_name", "idx_user_id") 构造了一个索引提示,指明在查询时应该强制使用 idx_user_name 和 idx_user_id 这两个索引。紧接着的 ForJoin() 方法进一步指定,这个强制索引的使用仅限于执行 JOIN 操作的场景。
生成的SQL语句:
SELECT * FROM users FORCE INDEX FOR JOIN (idx_user_name, idx_user_id)
迭代
GORM支持通过行进行迭代
go
GORM支持通过行进行迭代
rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Rows()
defer rows.Close()
for rows.Next() {
var user User
// ScanRows 方法用于将一行记录扫描至结构体
db.ScanRows(rows, &user)
// 业务逻辑...
}
解读:
这种方式在处理大量数据或需要逐行处理查询结果时非常有用,特别是当直接加载整个查询结果集到内存中不可行或不高效时。
步骤解析
1. 执行查询并获取 rows 对象
go
rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Rows()
db.Model(&User{}) 指定了查询的模型是 User 结构体对应的表。
.Where("name = ?", "jinzhu") 添加了一个条件,只选择 name 字段等于 "jinzhu" 的记录。
.Rows() 方法执行查询,并返回一个 *sql.Rows 对象,用于后续的行迭代处理。同时返回的 err 需要检查,以确保查询没有错误发生。
2.确保关闭 rows 对象
go
defer rows.Close()
在获取 rows 对象后,使用 defer 关键字确保 rows.Close() 会在函数返回之前被调用。这是非常重要的,因为它确保了数据库连接资源的正确释放。
3.迭代处理每一行数据
go
for rows.Next() {
var user User
// ScanRows 方法用于将一行记录扫描至结构体
db.ScanRows(rows, &user)
// 业务逻辑...
}
for rows.Next() 循环遍历查询结果中的每一行。对于每一次迭代,rows.Next() 准备下一行记录以供读取,如果没有更多的行,则循环结束。
在循环体内,每次迭代都会创建一个新的 User 实例 user,用于存放当前行的数据。
使用 db.ScanRows(rows, &user) 将当前行的数据扫描到 user 实例中。ScanRows 方法接收 rows 对象和一个指向模型实例的指针,然后将当前行的数据映射到该模型实例的字段中。
在 ScanRows 之后,你可以对 user 实例进行任何业务逻辑处理,如打印数据、执行进一步的处理等。注意这个user不是切片,就是结构体,因为从循环来看就是每条记录放到一个结构体中。
如果你想收集所有行,那应该这么搞:
go
var users []User // 定义一个 User 结构体的切片,用于存储所有用户记录
for rows.Next() {
var user User // 定义一个 User 结构体实例,用于存储当前行的数据
db.ScanRows(rows, &user) // 将当前行的数据扫描到 user 实例中
users = append(users, user) // 将 user 实例添加到 users 切片中
}
FindInBatches
用于批量查询并处理数据
go
// 每次批量处理 100 条
result := db.Where("processed = ?", false).FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error {
for _, result := range results {
// 批量处理找到的记录
}
tx.Save(&results)
tx.RowsAffected // 本次批量操作影响的记录数
batch // Batch 1, 2, 3
// 如果返回错误会终止后续批量操作
return nil
})
result.Error // returned error
result.RowsAffected // 整个批量操作影响的记录数
解读:
这段代码展示了如何在 GORM 中使用 FindInBatches 方法来批量查询并处理数据。这种方法特别适用于处理大量数据时,可以避免一次性加载过多数据到内存中,从而有效控制内存使用和优化性能。
FindInBatches 方法允许你分批次地加载记录进行处理。你可以指定每批次处理的记录数,以及一个回调函数来处理每批次加载的数据。
代码例子解读:
db.Where("processed = ?", false) :首先指定查询条件,这里是查找所有 processed 字段值为 false 的记录。
.FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error {...}):通过 FindInBatches 方法按批次查询满足条件的记录。每批次处理 100 条记录,results 是用于存储每批次查询结果的切片变量 ,tx 是当前批次的事务对象,可以用来执行数据库操作,batch 是当前批次的序号(从 1 开始)。
在回调函数中,可以对每批次查询到的记录进行处理。在这个示例中,回调函数遍历 results 切片中的每条记录,并对其进行处理(具体处理逻辑未展示)。
tx.Save(&results):处理完毕后,使用 tx 对象的 Save 方法保存对 results 的更改。
tx.RowsAffected:这表示本次批量操作影响的记录数。
batch:当前是第几批次的处理。
如果回调函数返回错误,则会终止后续的批量操作。
操作结果
result.Error:这是整个 FindInBatches 操作过程中返回的错误(如果有的话)。如果在任何一个批次的处理中回调函数返回了错误,这个错误会被捕获并赋值给 result.Error。
result.RowsAffected:这表示整个 FindInBatches 操作影响的总记录数。
查询钩子
go
func (u *User) AfterFind(tx *gorm.DB) (err error) {
if u.Role == "" {
u.Role = "user"
}
return
}
查询钩子(Query Hook)是 GORM 提供的一个功能,允许你在查询操作的特定时刻自动执行一些自定义逻辑。 这些钩子函数可以定义在模型(Model)上 ,GORM 会在执行查询操作(如 Find、First、Last 等)前后自动调用这些函数,使得你可以在数据被加载到结构体之后 对其进行进一步的处理或验证。
示例解读:
上面是定义了一个钩子函数AfterFind
钩子类型:AfterFind 是查询后的钩子,意味着它会在 GORM 完成查询并将结果映射到结构体之后被调用。
功能:此函数检查 User 结构体的 Role 字段,如果 Role 为空,则将其设置为 "user"。这可以确保每个 User 实例在被访问之前都有一个有效的 Role 值。
参数:钩子函数接受一个 *gorm.DB 类型的参数 tx,它代表当前的数据库上下文或事务。虽然在这个示例中没有使用 tx,但它可以用于执行数据库操作或获取当前操作的上下文信息。
返回值:返回一个 error 类型的值。如果一切正常,返回 nil。如果需要中断后续操作或报告错误,可以返回相应的错误。
查询钩子的用途
数据后处理:在数据从数据库加载到结构体后,自动执行一些数据处理或修正的逻辑。
验证:检查加载的数据是否满足特定的条件或完整性要求。
缓存更新:在获取数据后更新缓存。
日志记录:记录查询操作的日志。
权限检查:基于加载的数据执行权限检查。
Pluck
Pluck用于从数据库查询单个例,并将结果扫描到切片,如果您想要查询多列,那应该使用Select和Scan。
go
var ages []int64
db.Model(&users).Pluck("age", &ages)
var names []string
db.Model(&User{}).Pluck("name", &names)
db.Table("deleted_users").Pluck("name", &names)
// Distinct Pluck
db.Model(&User{}).Distinct().Pluck("Name", &names)
// SELECT DISTINCT `name` FROM `users`
// 超过一列的查询,应该使用 `Scan` 或者 `Find`,例如:
db.Select("name", "age").Scan(&users)
db.Select("name", "age").Find(&users)
解读:
Pluck 方法在 GORM 中用于从数据库查询单个列的多个值,并将这些值扫描到一个切片中。这个方法非常适合当你只需要从表中获取一列数据时使用,比如获取所有用户的年龄或者名字列表。 如果你需要查询多个列,则应该使用 Select 和 Scan 或者 Find 方法,这两种方法可以让你获取并映射整行数据到一个或多个结构体实例中。
使用 Pluck 查询单个列
示例 1: 查询年龄列
go
var ages []int64
db.Model(&User{}).Pluck("age", &ages)
这里,Pluck("age", &ages) 会查询 User 模型对应表中所有记录的 age 列,并将这些年龄值放入 ages 切片中。
示例 2: 查询名字列
go
var names []string
db.Model(&User{}).Pluck("name", &names)
示例 3: 使用 Table 方法
go
db.Table("deleted_users").Pluck("name", &names)
如果你想查询非模型对应的表,可以使用 Table 方法指定表名,然后用 Pluck 获取指定列的值。
示例 4: 查询不重复的名字
go
db.Model(&User{}).Distinct().Pluck("Name", &names)
使用 Select 和 Scan 或 Find 查询多个列
当需要从数据库查询多个列时,应使用 Select 结合 Scan 或 Find 方法。这允许你指定多个列,并将查询结果映射到一个或多个结构体实例中。
示例 5: 使用 Scan
go
db.Select("name", "age").Scan(&users)
这将查询 name 和 age 两列的值,并将结果映射到 users 切片中。users 切片的每个元素是 User 结构体实例,包含查询到的 name 和 age 值。
示例 6: 使用 Find
go
db.Select("name", "age").Find(&users)
Find 方法的使用和 Scan 类似,也是查询指定的列,并将结果映射到 users 切片中。
总结
使用 Pluck 查询并获取单个列的值列表非常方便,特别是当你只需要一列数据时。
当需要查询多个列并将数据映射到结构体时,应该使用 Select 结合 Scan 或 Find 方法。
Scope
Scope允许你指定常用的查询,可以在调用方法时引用这些查询
go
func AmountGreaterThan1000(db *gorm.DB) *gorm.DB {
return db.Where("amount > ?", 1000)
}
func PaidWithCreditCard(db *gorm.DB) *gorm.DB {
return db.Where("pay_mode_sign = ?", "C")
}
func PaidWithCod(db *gorm.DB) *gorm.DB {
return db.Where("pay_mode_sign = ?", "C")
}
func OrderStatus(status []string) func (db *gorm.DB) *gorm.DB {
return func (db *gorm.DB) *gorm.DB {
return db.Where("status IN (?)", status)
}
}
db.Scopes(AmountGreaterThan1000, PaidWithCreditCard).Find(&orders)
// 查找所有金额大于 1000 的信用卡订单
db.Scopes(AmountGreaterThan1000, PaidWithCod).Find(&orders)
// 查找所有金额大于 1000 的货到付款订单
db.Scopes(AmountGreaterThan1000, OrderStatus([]string{"paid", "shipped"})).Find(&orders)
// 查找所有金额大于 1000 且已付款或已发货的订单
解读:
GORM 的 Scope 功能提供了一种灵活的方式来封装和复用查询条件。通过定义一个或多个 Scope 函数,你可以轻松地在不同的查询中重用相同的查询逻辑,使得代码更加简洁和易于维护。
Scope 函数
一个 Scope 函数是一个接收 *gorm.DB 对象并返回 *gorm.DB 对象的函数。这样的设计允许你在函数内部对传入的 DB 对象应用一系列查询条件 ,然后返回修改后的 DB 对象以供进一步操作。
示例 1: 定义 Scope 函数
go
func AmountGreaterThan1000(db *gorm.DB) *gorm.DB {
return db.Where("amount > ?", 1000)
}
这个 Scope 函数添加了一个查询条件,用于筛选 amount 大于 1000 的记录。
go
func PaidWithCreditCard(db *gorm.DB) *gorm.DB {
return db.Where("pay_mode_sign = ?", "C")
}
func PaidWithCod(db *gorm.DB) *gorm.DB {
return db.Where("pay_mode_sign = ?", "C")
}
这两个同理,都是定义Scope函数
示例 2: 使用 Scope 函数
go
db.Scopes(AmountGreaterThan1000, PaidWithCreditCard).Find(&orders)
在这个查询中,通过 Scopes 方法同时应用了两个 Scope 函数:AmountGreaterThan1000 和 PaidWithCreditCard。这意味着 GORM 会查找所有 amount 大于 1000 且通过信用卡支付的订单。
动态Scope函数
Scope 还可以是动态的,接收参数并根据这些参数返回不同的查询条件。
go
func OrderStatus(status []string) func (db *gorm.DB) *gorm.DB {
return func (db *gorm.DB) *gorm.DB {
return db.Where("status IN (?)", status)
}
}
这个动态 Scope 函数接收一个状态列表,返回一个新的 Scope 函数,该函数用于筛选订单状态在给定列表中的订单。
使用场景:
复用查询逻辑:将常用的查询逻辑定义为 Scope 函数,可以在不同的查询中复用这些逻辑,减少代码重复。
简化查询构建: 对于复杂的查询条件,使用 Scope 可以使查询构建过程更加清晰和简洁。
动态查询:通过传递参数给 Scope 函数,可以根据不同的需求动态构建查询条件。
Count
Count用于获取匹配的记录数
go
var count int64
db.Model(&User{}).Where("name = ?", "jinzhu").Or("name = ?", "jinzhu 2").Count(&count)
// SELECT count(1) FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2'
db.Model(&User{}).Where("name = ?", "jinzhu").Count(&count)
// SELECT count(1) FROM users WHERE name = 'jinzhu'; (count)
db.Table("deleted_users").Count(&count)
// SELECT count(1) FROM deleted_users;
// Count with Distinct
db.Model(&User{}).Distinct("name").Count(&count)
// SELECT COUNT(DISTINCT(`name`)) FROM `users`
db.Table("deleted_users").Select("count(distinct(name))").Count(&count)
// SELECT count(distinct(name)) FROM deleted_users
// Count with Group
users := []User{
{Name: "name1"},
{Name: "name2"},
{Name: "name3"},
{Name: "name3"},
}
db.Model(&User{}).Group("name").Count(&count)
count // => 3
解读:
在 GORM 中,Count 方法用于获取匹配特定查询条件的记录数。这个方法生成并执行一个 SQL 查询,该查询使用 COUNT 函数来计算符合条件的记录总数,并将这个数值存储到提供的变量中。
示例解析
示例 1: 基础计数查询
go
var count int64
db.Model(&User{}).Where("name = ?", "jinzhu").Or("name = ?", "jinzhu 2").Count(&count)
这个查询在 users 表中查找 name 为 "jinzhu" 或 "jinzhu 2" 的记录数。
生成的 SQL 语句是:SELECT count(*) FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2'。
结果存储在 count 变量中。
示例 2: 单一条件计数
go
db.Model(&User{}).Where("name = ?", "jinzhu").Count(&count)
这个查询计算 name 等于 "jinzhu" 的用户数。
SQL 语句:SELECT count(*) FROM users WHERE name = 'jinzhu'。
示例 3: 在指定表上计数
go
db.Table("deleted_users").Count(&count)
直接在 deleted_users 表上执行计数查询,不限定条件。
SQL 语句:SELECT count(*) FROM deleted_users。
示例 4: 使用 Distinct 进行计数
go
db.Model(&User{}).Distinct("name").Count(&count)
计算具有唯一 name 值的用户总数。
SQL 语句:SELECT COUNT(DISTINCT(name)) FROM users``。
示例 5: 使用 Group 进行计数
go
db.Model(&User{}).Group("name").Count(&count)
对 name 进行分组,然后计算不同 name 的组数。
注意:使用 Group 与 Count 结合时,Count 返回的是分组的数量,而不是记录的总数。因此,如果 name 有重复值,count 反映的是不同 name 的数量。
总结:
GORM 的 Count 方法提供了一种简便的方式来获取数据库表中符合特定条件的记录数。
通过结合 Where、Or、Distinct、和 Group 等方法,可以构建灵活的计数查询以满足不同的需求。
当使用 Count 方法时,需要提供一个变量(如 int64 类型)来接收计算出的记录数。
特别地,在使用 Group 进行分组计数时,返回的是分组的数量而不是总记录数,这一点在分析数据时需要特别注意。