在 ActiveRecord 中查询包含来自多个数组的一个或多个 id 的对象

Query in ActiveRecord for objects that contain one or more ids from multiple arrays

我有 Rails 5.2 项目和三个模型:

class Post
  has_many :post_tags
  has_many :tags, through: :post_tags
end

class PostTags
  belongs_to :post
  belongs_to :tag
end

class Tags
  has_many :post_tags
  has_many :posts, through: :post_tags
end

我有许多标签 ID 数组,例如:

array_1 = [3, 4, 5]
array_2 = [5, 6, 8]
array_3 = [9, 11, 13]

我想要一个查询,该查询将 return post 标记有至少一个标签,其中每个数组都有一个 id。

例如,假设我有一个带有以下标签 ID 的 post:

> post = Post.find(1)
> post.tag_ids
> [4, 8]

如果我 运行 使用 array_1array_2 的查询,它将 return 这个 post。但是,如果我 运行 它与 array_1array_2array_3 它不会 return 这个 post.

我尝试了以下查询:

Post.joins(:tags).where('tags.id IN (?) AND tags.id IN (?)', array_1, array_2)

但这并不return post。

return post 的查询应该是什么?

如有任何帮助,我们将不胜感激!

在您的 where 条件中使用 AND 是检查相交的值(两个数组包含相同的值)。

array_1 = [3, 4, 5]
array_2 = [5, 6, 8]

并且 return 会得到 id: 5 的结果,因为它在两个数组中。

使用 OR 可以满足您的需求。这些中的任何一个都应该适合你:

Post.joins(:tags).where('tags.id IN (?) OR tags.id IN (?)', array_1, array_2)

Post.joins(:tags).where(tags: { id: array_1 + array_2 })

我的想法是,您可以按 posts.id 分组并将其所有标签位置在输入数组位置中求和,假设您使用 3 group_tags 进行查询,那么您将得到如下结果:

post_id  group_tags_1  group_tags_2  group_tags_3 ....
1        2             0             0
2        1             1             1

所以最终的结果是 Post,id 为 2,因为它至少有一个来自每个组的标签。

def self.by_group_tags(group_tags)
  having_enough_tags = \
   group_tags.map do |tags|
     sanitize_sql_array(["SUM(array_position(ARRAY[?], tags.id::integer)) > 0", tags])
   end
  
   Post  
    .joins(:tags)
    .group("posts.id")
    .having(
      having_enough_tags.join(" AND ")
    )
end

# Post.by_group_tags([[1,2], [3,4]])
# Post.by_group_tags([[1,2], [3,4], [5,6,7]])

更新: 如果你想进入更深的链并且不应该受到 group 的影响,那么只需简单的 return 一个包含你从 by_group_tags 查询的所有 post id 的关系,例如a where 如下

class Post
  def self.by_group_tags(group_tags)
    # ...
  end

  def self.scope_by_group_tags(group_tags)
    post_ids = Post.by_group_tags(group_tags).pluck(:id)
    Post.where(id: post_ids)
  end
end

# Post.scope_by_group_tags([[1,2], [3,4]]).first(10)

缺点:调用查询相同的 Posts 两次。

因为你已经用 postgresql 标记了这个问题,你可以使用 intersect 关键字执行你想要的查询。不幸的是,activerecord 本身不支持 intersect 因此您必须构建 sql 才能使用此方法。

array_1 = [3, 4, 5]
array_2 = [5, 6, 8]

query = [array_1, array_2].map do |tag_ids|
  Post.joins(:tags).where(tags: { id: tag_ids }).to_sql
end.join(' intersect ')

Post.find_by_sql(query)

编辑:

我们可以使用子查询 return 帖子并维护 activerecord 关系。

array_1 = [3, 4, 5]
array_2 = [5, 6, 8]

Post
  .where(post_tags: PostTag.where(tag_id: array_1))
  .where(post_tags: PostTag.where(tag_id: array_2))

对于奖励积分,您可以将 where(post_tag: PostTag.where(tag_id: array_1)) 变成 Posts 上的范围,并根据需要链接任意数量的范围。

如@NikhilVengal 所述。您应该能够像这样使用 3 个范围查询的交集

scopes = [array_1,array_2,array_3].map do |arr| 
    Post.joins(:post_tags).where(PostTag.arel_table[:tag_id].in(arr)).arel
end

subquery = scopes.reduce do |memo,scope| 
    # Arel::Nodes::Intersect.new(memo,scope)
    memo.intersect(scope)
end 

Post.from(Arel::Nodes::As.new(subquery,Post.arel_table))

这应该 return Post 个对象是 3 个查询的交集。

或者我们可以创建 3 个连接

joins = [array_1,array_2,array_3].map.with_index do |arr,idx| 
  alias = PostTag.arel_table.alias("#{PostTag.arel_table.name}_#{idx}")
  Arel::Nodes::InnerJoin.new(
    alias,
    Arel::Nodes::On.new(
      Post.arel_table[:id].eq(alias.arel_table[:post_id])
        .and(alias.arel_table[:tag_id].in(arr))
     )
   )
end

Post.joins(joins).distinct

这将创建 3 个带有 Post table 的内部连接,每个连接都带有 Post 标签 table 过滤到特定的 tag_ids 确保Post 仅当它存在于所有 3 个列表中时才会显示。