has_many的附加范围条件:通过

时间:2016-10-08 06:31:38

标签: ruby-on-rails ruby postgresql activerecord ruby-on-rails-5

我希望用户能够找到包含一个或多个标签的所有帖子。我希望这些标签是附加标准,例如,您可以搜索只有“新闻”标签的帖子。标签,或者您可以搜索同时包含“新闻”的帖子。和科学'标签

目前,我所拥有的是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

这似乎是一种笨拙的方式来到那里。有没有办法可以通过关联或某种类似的范围来做到这一点?

1 个答案:

答案 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_idpost_id的多个标记,这会使计数失效。

我认为最后一个解决方案可能效率最高,但您应该尝试不同的解决方案,看看什么最适合您的数据。

另请参阅:Query intersection with activerecord