【问题标题】:Query in ActiveRecord for objects that contain one or more ids from multiple arrays在 ActiveRecord 中查询包含来自多个数组的一个或多个 id 的对象
【发布时间】:2021-11-05 11:03:35
【问题描述】:

我有包含三个模型的 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]

我想要一个查询,该查询将返回带有每个数组中至少一个带有 id 的标签的帖子。

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

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

如果我使用array_1 和array_2 运行查询,它将返回此帖子。但是,如果我使用 array_1、array_2 和 array_3 运行它,它不会返回此帖子。

我尝试了以下查询:

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

但这不会返回帖子。

返回帖子的查询应该是什么?

任何帮助将不胜感激!

【问题讨论】:

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


    【解决方案1】:

    由于您已使用 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)
    

    编辑:

    我们可以使用子查询来返回帖子并维护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 上的一个作用域,并尽可能多地链接它们。

    【讨论】:

    • 这非常好,非常接近我想要的,但是我如何让它返回一个我可以链接的 Active Record 关系对象,而不是一个简单的结果数组?
    • 我认为您无法使用此方法返回 AR 关系。此方法直接执行 sql,因此它绕过了关系步骤。如果您可以执行两个查询,则可以使用merge 方法将其与活动记录关系结合起来。 some_active_record_relation_on_posts.merge(Post.find_by_sql(query))
    • @NikhilVengal +1 你肯定走在正确的轨道上,但你可以让这个查询在 AR 中返回。请参阅我的帖子了解如何使用您的交叉路口但返回 ActiveRecord::Relation
    • @engineersmnky 好电话。使用PostTag 作为中介绝对是正确的方法。请注意,您无需联系 arel sql 或intersect,就可以更惯用地做这件事 - 请参阅这篇文章的编辑。
    【解决方案2】:

    正如@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))
    

    这应该返回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 表的内部联接,每个都将 PostTag 表过滤到特定的 tag_ids 以确保 Post 仅在所有 3 个列表中都存在时才会显示。

    【讨论】:

      【解决方案3】:

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

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

      并且将返回带有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 })
      

      【讨论】:

      • 感谢@csebryam 的回复。不幸的是,这会返回任何具有 any 标记的帖子,这些帖子来自与之关联的任一数组。我希望它只返回存在 every 数组中具有 至少一个 标签的帖子。
      【解决方案4】:

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

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

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

      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 的影响,那么只需简单地返回一个包含您从by_group_tags 查询的所有帖子ID 的关系,例如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)
      

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

      【讨论】:

      • 这是一个非常聪明的解决方案。让我试试,然后再回来找你。
      • 不幸的是,当我进一步将此关系与其他 Active Record 关系链接时,group 函数会导致问题
      • @BetjaminRichards 如果你想去更远的链,那么只需简单地返回一个包含你从 by_group_tags 查询的所有 id 的关系,例如 where,看看我的更新。跨度>
      猜你喜欢
      • 1970-01-01
      • 2018-04-27
      • 2020-02-28
      • 2014-04-21
      • 1970-01-01
      • 2022-01-12
      • 1970-01-01
      • 2015-12-21
      相关资源
      最近更新 更多