【问题标题】:How to find items with *all* matching categories如何查找具有*所有*匹配类别的项目
【发布时间】:2015-04-28 06:46:15
【问题描述】:

我有两个模型,Item 和 Category,由一个连接表连接。我想查询 Item 以仅查找与类别列表匹配的项目。我的模型看起来像:

class Item < ActiveRecord::Base
  has_and_belongs_to_many :categories
end

class Category < ActiveRecord::Base
  has_and_belongs_to_many :items
end

我可以轻松找到与任何类别列表匹配的项目。以下将返回属于类别 1、2 或 3 的项目。

Item.includes(:categories).where(categories: {id:[1,2,3]})

我只想查找属于所有 3 个类别的项目。使用 ActiveRecord 完成此任务的最佳方法是什么?

我是否需要自己编写 where 条件,如果需要,PostgreSQL 的正确语法是什么?我尝试了各种风格的“WHERE ALL IN (1,2,3)”,但只是得到语法错误。

更新:

根据对Find Products matching ALL Categories (Rails 3.1) 的接受回答,我可以非常接近。

category_ids = [7,10,12,13,52,1162]

Item.joins(:categories).
  where(categories: {id: category_ids}).
  group('items.id').
  having("count(categories_items.category_id) = #{category_ids.size}")

不幸的是,当链接 .count.size 时,我得到一个哈希而不是记录计数:

{189 => 6, 3067 => 6, 406 => 6}

我可以计算结果哈希中的键来获得真正的记录数,但这是一个非常不优雅的解决方案。

【问题讨论】:

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


【解决方案1】:

活动记录

对于 ActiveRecord,您可以在您的 Item 类中放置这样的方法:

def self.with_all_categories(category_ids)
  select(:id).distinct.
    joins(:categories).
    where('categories.id' => category_ids).
    group(:id).
    having('count(categories.id) = ?', category_ids.length)
end

然后您可以像这样过滤您的查询:

category_ids = [1,2,3]
Item.where(id: Item.with_all_categories(category_ids))

你也可以使用作用域让它更友好一点:

class Item
  scope :with_all_categories, ->(category_ids) { where(id: Item.ids_with_all_categories(category_ids)) }

  def self.ids_with_all_categories(category_ids)
    select(:id).distinct.
      joins(:categories).
      where('categories.id' => category_ids).
      group(:id).
      having('count(categories.id) = ?', category_ids.length)
  end
end

Item.with_all_categories([1,2,3])

两者都会产生这个 SQL

SELECT "items".*
FROM "items"
WHERE "items"."id" IN
  (SELECT DISTINCT "items"."id"
   FROM "items"
   INNER JOIN "categories_items" ON "categories_items"."item_id" = "items"."id"
   INNER JOIN "categories" ON "categories"."id" = "categories_items"."category_id"
   WHERE "categories"."id" IN (1, 2, 3)
   GROUP BY "items"."id" 
   HAVING count(categories.id) = 3)

从技术上讲,您不需要该子查询的 distinct 部分,但我不确定是否有更好的性能。

SQL

原始 SQL 中有几种方法

SELECT *
FROM items
WHERE items.id IN (
  SELECT item_id
  FROM categories_items
  WHERE category_id IN (1,2,3)
  GROUP BY item_id
  HAVING COUNT(category_id) = 3
)

这将在 SQL Server 中工作 - Postgres 中的语法可能略有不同。或者

SELECT *
FROM items
WHERE items.id IN (SELECT item_id FROM categories_items WHERE category_id = 1)
  AND items.id IN (SELECT item_id FROM categories_items WHERE category_id = 2)
  AND items.id IN (SELECT item_id FROM categories_items WHERE category_id = 3)

【讨论】:

    【解决方案2】:

    我不能肯定,但这可能有效

    categories = Category.find(1,2,3)
    items = Item.includes(:categories)
    items.select{|item| (categories-item.categories).blank?}
    

    或者只是

    Item.all.select{|item| (Category.find(1,2,3)-item.categories).blank?}
    

    【讨论】:

      【解决方案3】:

      刚刚用 has_many 尝试了 Alex 的惊人建议:通过设置,它产生了一个令人惊讶的结果:当我查找具有 EXACTLY [6,7,8] 类别的项目时,它还返回匹配所有 6,7,8 类别及更多类别的项目, IE。具有 [6,7,8,9] 类别的项目。

      技术上它是基于代码的正确结果,因为有子句处理 where 子句的查询结果,因此从 Alex 的代码中,有子句的所有可能的计数结果将是 1 或 2 或 3,但可能不是 4 个或更多。

      为了克服这种情况,我添加了一个类别计数器缓存并在 have 子句之前预筛选了类别计数,因此它只返回具有且仅具有 [6,7,8] 类别的项目(没有额外的)。

        def self.with_exact_categories(category_ids)    
          self.
            joins(:categories).
            where('categories.id': category_ids).
            where('items.categories_count = ?', category_ids.length).
            group('items.id').
            having('count(categories.id) = ?', category_ids.length)
        end
      

      对于预筛选类别计数,我不知道如何在 where 子句中使用聚合函数,但仍然很高兴得知计数器缓存在 Rails 4.21 中仍然有效。这是我的模型设置:

      class Item < ActiveRecord::Base
        has_many :categories_items
        has_many :categories, through: :categories_items
      end
      
      class CategoriesItem < ActiveRecord::Base
        belongs_to :category
        belongs_to :item, counter_cache: :categories_count
      end
      
      class Category < ActiveRecord::Base
        has_many :categories_items, dependent: :destroy
        has_many :items, through: :categories_items, dependent: :destroy
      end
      
      class AddCategoriesCountToItems < ActiveRecord::Migration
        def change
          add_column :items, :categories_count, :integer, default: 0
        end
      end
      

      【讨论】:

        【解决方案4】:

        这段代码怎么样

        Item.all.joins(:categories).where(categories: { id: [1, 2, 3] })
        

        SQL 是

        SELECT
            "items" . *
        FROM
            "items" INNER JOIN "categories_items"
                ON "categories_items" . "item_id" = "items" . "id" INNER JOIN "categories"
                ON "categories" . "id" = "categories_items" . "category_id"
        WHERE
            "categories" . "id" IN (
                1
                ,2
                ,3
            )
        

        【讨论】:

        • 不,返回任何类别的项目 - 不是所有类别。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-05-31
        • 2015-09-16
        • 2021-10-17
        • 2021-12-03
        • 2012-08-06
        • 1970-01-01
        • 2011-11-21
        相关资源
        最近更新 更多