Find Products matching ALL Categories (Rails 3.1)
You can do this with a having clause:
@ids = [1,5,8]
query = Product.select('products.id,products.name').joins(:categories) \
.where(:categories => {:id => @ids}) \
.group('products.id, products.name') \
.having("count(category_products.category_id) = #{@ids.length}")
How to find items with *all* matching categories
ActiveRecord
For ActiveRecord, you could put a method like this in your Item class:
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
Then you can filter your queries like so:
category_ids = [1,2,3]
Item.where(id: Item.with_all_categories(category_ids))
You could also make use of scopes to make it a little more friendly:
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])
Both will produce this 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)
You don't technically need the distinct
part of that subquery, but I'm not sure whether with or without would be better for performance.
SQL
There's a couple approaches in raw 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
)
That will work in SQL Server - the syntax might be slightly different in Postgres. Or
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)
Dynamic categories criteria with matching products rails 3
In my opinion, it's better not to define additional tables to handle this due lots of performance issues. My preference to handle such things is to use a serialized column in the products table. Ability to search directly in the database is reduced with this approach, but then you wouldn't want to do that anyway. To handle search, you have to add some sort of indexed searching mechanism. Like acts_as_ferret or even Solr or ElasticSearch.
If you are using postgres check out https://github.com/softa/activerecord-postgres-hstore
For Mysql, use the rails's built in 'store'
http://api.rubyonrails.org/classes/ActiveRecord/Store.html
class Product < ActiveRecord::Base
has_and_belongs_to_many :categories
store :settings
end
To set criteria for each category do something similar to this:
class Category < ActiveRecord::Base
has_and_belongs_to_many :products
def criteria
@criteria_list ||= self[:criteria].split('|')
@criteria_list
end
def criteria=(names)
self[:criteria] = names.join('|')
end
end
Everytime a product is added to a category, check if all of the criteria in that category is available in the product's properties hash keys. If not, add it with a default value if needed.
You can also setup accessors for the properties hash store using a proc that dynamically gets the accessor names from the all the criteria field of the categories of the product? (not sure about this, cause I haven't done this before)
You can also look into using STI (Single table Inheritance) using a type field in your products table. (It's well documented) This approach is slightly better 'cause when products move from one category to another, the properties won't change.
class Gadget < Product
store_accessor :manufacturer, :model
end
class Phone < Gadget
store_accessor :os, :touch_screen, :is_smart
end
Hope this helps
How can I filter products by multiple categories with ActiveRecord?
Based on @muistooshort's comment above, I found this SO post with the solution I needed:
Selecting posts with multiple tags
Updating my query like so gave the products I wanted, those in ALL of the specified categories (lines wrapped for readability):
Product.joins(:categories).where(:categories => { :id => category_ids })
.having('count(categories.name) = ?', category_ids.size)
.group('products.id')
Filtering by categories with rails
You can achieve this with group
and having
:
scope :filter, -> (category_ids) {
return unless category_ids.present?
includes(:categories).where(categories: { id: category_ids })
.group(:id).having("count(*) = ? ", category_ids.size)
}
ActiveRecord find categories which contain at least one item
please notice, what the other guys answererd is NOT performant!
the most performant solution:
better to work with a counter_cache and save the items_count in the model!
scope :with_items, where("items_count > 0")
has_and_belongs_to_many :categories, :after_add=>:update_count, :after_remove=>:update_count
def update_count(category)
category.items_count = category.items.count
category.save
end
for normal "belongs_to" relation you just write
belongs_to :parent, :counter_cache=>true
and in the parent_model you have an field items_count (items is the pluralized has_many class name)
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
in a has_and_belongs_to_many relation you have to write it as your own as above
Related Topics
How to Convert a Scientific Notation String to Decimal Notation
How to Set Ca-Bundle Path for Openssl in Ruby
How to Split String into Array as Integers
Using Activerecord Find_In_Batches Method for Deleting Large Data
Is It Necessary to Close Stringio in Ruby
Remove Double Quotes from String
Error "Undefinded Method "Load_Defaults" " When Trying to Deploy App on Heroku
Ruby - Does Array a Contain All Elements of Array B
Access Ruby Hash Using Dotted Path Key String
/Usr/Bin/Env Ruby_Noexec_Wrapper Fails with No File or Directory
Rails: How to Check If a Column Has a Value
In Ruby or Rails, Why Is "Include" Sometimes Inside the Class and Sometimes Outside the Class
Ruby Conditional-Assignment and Private Methods
Rails Devise - Current_User Is Nil