我希望用户能够找到包含一个或多个标签的所有帖子。我希望这些标签是附加标准,例如,您可以搜索只有“新闻”标签的帖子。标签,或者您可以搜索同时包含“新闻”的帖子。和科学'标签
目前,我所拥有的是Post模型,Tag模型和名为Marking的连接模型。发布has_many :tags, through: :markings
。通过将一个Tag id数组传递给Post类方法,我得到了我所需要的东西:
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
这似乎是一种笨拙的方式来到那里。有没有办法可以通过关联或某种类似的范围来做到这一点?
答案 0 :(得分:2)
因此,构建这类查询的一般经验法则是尽量减少“Ruby-land”中的工作,并最大限度地提高“Database-land”中的工作量。在上面的解决方案中,您将使用集合array
中的任何标记获取一组标记,这可能是一个非常大的集合(所有具有任何这些标记的帖子)。这在ruby数组中表示并处理(group_by
在Ruby世界中,group
在Database-land中是等效的。)
除了难以阅读之外,对于任何大量标记,该解决方案都会变慢。
有几种方法可以解决这个问题,而不需要在Ruby世界中做任何繁重的工作。一种方法是使用子查询,如下所示:
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
的多个标记,这会使计数失效。
我认为最后一个解决方案可能效率最高,但您应该尝试不同的解决方案,看看什么最适合您的数据。