在我的数据库中,我Users
有check_ins
。 check_in
与restaurant_id
的一家餐馆相关联。什么是最有效的方式让所有在特定餐厅办理登机手续的用户超过X次?
答案 0 :(得分:3)
要编写效果Active Record查询,首先必须知道如何编写有效的SQL查询。与任何编程问题一样,第一步是将其分解为较小的任务。
当你只需要一个查询时,不要做两个查询。
users_with_check_in_counts = User.select('users.*, COUNT(*) AS check_in_count')
.joins('LEFT OUTER JOIN check_ins ON users.id = check_ins.user_id')
.where(check_ins: { restaurant_id: 1 })
.group(:id)
.having('check_in_count > ?', 3)
.all
# => [ #<User id=2, name="Nick", ..., check_in_count=4>,
# #<User id=4, name="Jordan", ..., check_in_count=4> ]
nick = users_with_check_in_counts.first
puts nick.check_in_count
# => 4
您的check_ins
表可能看起来像这样:
id | restaurant_id | user_id | ...
-----+---------------+---------+-----
1 | 1 | 1 | ...
2 | 1 | 2 |
3 | 1 | 2 |
4 | 1 | 2 |
5 | 1 | 2 |
6 | 1 | 3 |
7 | 1 | 3 |
8 | 1 | 3 |
9 | 1 | 4 |
10 | 1 | 4 |
11 | 1 | 4 |
12 | 1 | 4 |
13 | 2 | 1 |
... | ... | ... | ...
在上表中,我们在restaurant_id = 1
餐厅办理了12次办理登机手续。签入user_id = 1
的用户,2
签入四次,3
签到两次,4
签到四次。
这种天真的方法是将其分解为以下任务:
获取餐厅的check_ins
条记录:
SELECT * FROM check_ins WHERE restaurant_id = 1;
通过user_id
分组并计算每组中的记录数量,获取餐馆每个用户的签到次数:
SELECT check_ins.*, COUNT(user_id) AS check_in_count
FROM check_ins
WHERE restaurant_id = 1
GROUP BY user_id
将结果限制为至少包含 N 记录的组,例如N = 3
:
SELECT check_ins.*, COUNT(user_id) AS check_in_count
FROM check_ins
WHERE restaurant_id = 1
GROUP BY user_id
HAVING check_in_count >= 3
将其转换为Active Record查询:
check_in_counts = CheckIn.where(restaurant_id: 1).group(:user_id)
.having("user_count > ?", 3).count
# => { 2 => 4, 4 => 4 }
编写第二个查询以获取关联用户:
User.find(check_in_counts.keys)
# => [ #<User id=2, ...>, #<User id=4, ...> ]
这很有效,但有一些有点臭 - 哦,我们正在使用关系数据库。如果我们有一个从check_ins
获取记录的查询,我们应该在同一个查询中获取相关的users
。
现在,我们可以从上面的(3)中获取我们的SQL查询并添加JOIN users ON check_ins.user_id = users.id
来获取关联的用户记录,这是相对明显的,但这使我们处于绑定状态,因为我们仍然希望Active Record能够提供us用户对象,而不是CheckIn对象。为此,我们需要一个不同的查询,一个以users
开头并加入check_ins
的查询。
为此,我们使用LEFT OUTER JOIN
:
SELECT *
FROM users
LEFT OUTER JOIN check_ins ON users.id = check_ins.user_id
WHERE restaurant_id = 1;
以上查询会给我们这样的结果:
id | name | ... | restaurant_id | user_id
----+--------+-----+---------------+---------
1 | Sarah | 1 | 1 | 1
2 | Nick | 1 | 1 | 2
2 | Nick | 1 | 1 | 2
2 | Nick | 1 | 1 | 2
2 | Nick | 1 | 1 | 2
3 | Carmen | 1 | 1 | 3
3 | Carmen | 1 | 1 | 3
3 | Carmen | 1 | 1 | 3
4 | Jordan | 1 | 1 | 4
4 | Jordan | 1 | 1 | 4
4 | Jordan | 1 | 1 | 4
4 | Jordan | 1 | 1 | 4
这看起来很熟悉:它包含来自check_ins
的所有数据,其中users
的数据添加到每一行。这就是LEFT OUTER JOIN
的作用。现在,就像之前一样,我们可以使用GROUP BY
按用户ID分组,COUNT
使用HAVING
计算每个组中的记录,以便将结果限制为具有特定数量的用户签到:
SELECT users.*, COUNT(*) AS check_in_count
FROM users
LEFT OUTER JOIN check_ins ON users.id = check_ins.user_id
WHERE restaurant_id = 1
GROUP BY users.id
HAVING check_in_count >= 3;
这给了我们:
id | name | ... | check_in_count
----+--------+-----+----------------
2 | Nick | ... | 4
4 | Jordan | | 4
完美!
现在我们所要做的就是将其转换为Active Record查询。这很简单:
users_with_check_in_counts = User.select('users.*, COUNT(*) AS check_in_count')
.joins('LEFT OUTER JOIN check_ins ON users.id = check_ins.user_id')
.where(check_ins: { restaurant_id: 1 })
.group(:id)
.having('check_in_count > ?', 3)
.all
# => [ #<User id=2, name="Nick", ..., check_in_count=4>,
# #<User id=4, name="Jordan", ..., check_in_count=4> ]
nick = users_with_check_in_counts.first
puts nick.check_in_count
# => 4
最重要的是,它只执行一次查询。
这是一个非常长的Active Record查询。如果你的应用中只有一个地方你会有这样的查询,那么就可以这样使用它。如果我是你,我会把它变成一个范围:
class User < ActiveRecord::Base
scope :with_check_in_count, ->(opts) {
opts[:at_least] ||= 1
select('users.*, COUNT(*) AS check_in_count')
.joins('LEFT OUTER JOIN check_ins ON users.id = check_ins.user_id')
.where(check_ins: { restaurant_id: opts[:restaurant_id] })
.group(:id)
.having('check_in_count >= ?', opts[:at_least])
}
# ...
end
然后:
User.with_check_in_count(at_least: 3, restaurant_id: 1)
# ...or just...
User.with_check_in_count(restaurant_id: 1)
答案 1 :(得分:0)
我无法使用您的确切模型架构进行检查,但这样的事情应该有效:
check_in_counts = CheckIn.group(:user_id).having(restaurant_id: 3).having('COUNT(id) > 10').count
这将返回Hash
user_id
=&gt; check_in_count
值,可用于获取所有User
个对象:
users = User.find(check_in_counts.keys)