has_many :through 的附加范围条件
Additive scope conditions for has_many :through
我希望用户能够找到所有具有一个或多个标签的帖子。我希望标签是附加条件,例如,您可以搜索仅包含 'News' 标签的帖子,或者您可以搜索同时包含 'News' 和 [=25] 的帖子=] 标签。
目前我拥有的是一个 Post 模型、一个标记模型和一个称为标记的连接模型。 Posthas_many :tags, through: :markings
。我通过将标签 ID 数组传递给 Post class 方法来获得我需要的东西:
post.rb
def self.from_tag_id_array array
post_array = []
Marking.where(tag_id: array).group_by(&:post_id).each do |p_id,m_array|
post_array << p_id if m_array.map(&:tag_id).sort & array.sort == array.sort
end
where id: post_array
end
这似乎是一种笨拙的方法。有没有一种方法可以通过关联范围或类似的东西来做到这一点?
因此,构建此类查询的一般经验法则是尽量减少 "Ruby-land" 中的工作并最大限度地增加 "Database-land" 中的工作。在你上面的解决方案中,你正在获取一组标记,其中包含集合 array
中的任何标签,这可能是一个非常大的集合(所有帖子都具有这些标签中的任何一个)。这在 ruby 数组中表示并进行处理(group_by
在 Ruby 世界中,group
在数据库领域中是等效的)。
所以除了难以阅读之外,该解决方案对于任何大型标记集都会很慢。
在 Ruby-world 中,有几种方法可以在不做任何繁重工作的情况下解决问题。一种方法是使用子查询,如下所示:
scope :with_tag_ids, ->(tag_ids) {
tag_ids.map { |tag_id|
joins(:markings).where(markings: { tag_id: tag_id })
}.reduce(all) { |scope, subquery| scope.where(id: subquery) }
}
这会生成这样的查询(再次针对 tag_ids 5 和 8)
SELECT "posts".*
FROM "posts"
WHERE "posts"."id" IN (SELECT "posts"."id" FROM "posts" INNER JOIN "markings" ON "markings"."post_id" = "posts"."id" WHERE "markings"."tag_id" = 5)
AND "posts"."id" IN (SELECT "posts"."id" FROM "posts" INNER JOIN "markings" ON "markings"."post_id" = "posts"."id" WHERE "markings"."tag_id" = 8)
请注意,由于此处的所有内容都是在 SQL 中直接计算的,因此在 Ruby 中不会生成或处理任何数组。这通常会更好地扩展。
或者,您可以使用 COUNT
并在没有子查询的单个查询中执行此操作:
scope :with_tag_ids, ->(tag_ids) {
joins(:markings).where(markings: { tag_id: tag_ids }).
group(:post_id).having('COUNT(posts.id) = ?', tag_ids.count)
}
生成 SQL 如下:
SELECT "posts".*
FROM "posts"
INNER JOIN "markings" ON "markings"."post_id" = "posts"."id"
WHERE "markings"."tag_id" IN (5, 8)
GROUP BY "post_id"
HAVING (COUNT(posts.id) = 2)
这假设您没有多个标记具有同一对 tag_id
和 post_id
,这会导致计数失败。
我想最后一个解决方案可能是最有效的,但您应该尝试不同的解决方案,看看哪种解决方案最适合您的数据。
另请参阅:Query intersection with activerecord
我希望用户能够找到所有具有一个或多个标签的帖子。我希望标签是附加条件,例如,您可以搜索仅包含 'News' 标签的帖子,或者您可以搜索同时包含 'News' 和 [=25] 的帖子=] 标签。
目前我拥有的是一个 Post 模型、一个标记模型和一个称为标记的连接模型。 Posthas_many :tags, through: :markings
。我通过将标签 ID 数组传递给 Post class 方法来获得我需要的东西:
post.rb
def self.from_tag_id_array array
post_array = []
Marking.where(tag_id: array).group_by(&:post_id).each do |p_id,m_array|
post_array << p_id if m_array.map(&:tag_id).sort & array.sort == array.sort
end
where id: post_array
end
这似乎是一种笨拙的方法。有没有一种方法可以通过关联范围或类似的东西来做到这一点?
因此,构建此类查询的一般经验法则是尽量减少 "Ruby-land" 中的工作并最大限度地增加 "Database-land" 中的工作。在你上面的解决方案中,你正在获取一组标记,其中包含集合 array
中的任何标签,这可能是一个非常大的集合(所有帖子都具有这些标签中的任何一个)。这在 ruby 数组中表示并进行处理(group_by
在 Ruby 世界中,group
在数据库领域中是等效的)。
所以除了难以阅读之外,该解决方案对于任何大型标记集都会很慢。
在 Ruby-world 中,有几种方法可以在不做任何繁重工作的情况下解决问题。一种方法是使用子查询,如下所示:
scope :with_tag_ids, ->(tag_ids) {
tag_ids.map { |tag_id|
joins(:markings).where(markings: { tag_id: tag_id })
}.reduce(all) { |scope, subquery| scope.where(id: subquery) }
}
这会生成这样的查询(再次针对 tag_ids 5 和 8)
SELECT "posts".*
FROM "posts"
WHERE "posts"."id" IN (SELECT "posts"."id" FROM "posts" INNER JOIN "markings" ON "markings"."post_id" = "posts"."id" WHERE "markings"."tag_id" = 5)
AND "posts"."id" IN (SELECT "posts"."id" FROM "posts" INNER JOIN "markings" ON "markings"."post_id" = "posts"."id" WHERE "markings"."tag_id" = 8)
请注意,由于此处的所有内容都是在 SQL 中直接计算的,因此在 Ruby 中不会生成或处理任何数组。这通常会更好地扩展。
或者,您可以使用 COUNT
并在没有子查询的单个查询中执行此操作:
scope :with_tag_ids, ->(tag_ids) {
joins(:markings).where(markings: { tag_id: tag_ids }).
group(:post_id).having('COUNT(posts.id) = ?', tag_ids.count)
}
生成 SQL 如下:
SELECT "posts".*
FROM "posts"
INNER JOIN "markings" ON "markings"."post_id" = "posts"."id"
WHERE "markings"."tag_id" IN (5, 8)
GROUP BY "post_id"
HAVING (COUNT(posts.id) = 2)
这假设您没有多个标记具有同一对 tag_id
和 post_id
,这会导致计数失败。
我想最后一个解决方案可能是最有效的,但您应该尝试不同的解决方案,看看哪种解决方案最适合您的数据。
另请参阅:Query intersection with activerecord