在 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_1
和 array_2
的查询,它将 return 这个 post。但是,如果我 运行 它与 array_1
、array_2
和 array_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 个列表中时才会显示。
我有 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_1
和 array_2
的查询,它将 return 这个 post。但是,如果我 运行 它与 array_1
、array_2
和 array_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 个列表中时才会显示。