I am trying to create a generic product catalog application with Rails and in order to have products of varying types with varying attributes I have abstracted product properties into their own table with a link table in between the product and the property that stores the value.
------------- --------------------
|products | |product_properties| ------------
|-----------| |------------------| |properties|
|name |---|value |---|----------|
|description| |product_id | |name |
|etc... | |property_id | ------------
------------- --------------------
For example a product could have a width property (which will be stored in the property table so it can be reused) whilst the value for the width will be stored in the product_properties table with a record that links the property to the product.
This works fine but I need to implement facet-able search within the products model and have chosen to use ransack. So to find all products that have a width greater than 30 I must do
Product.ransack(product_properties_property_name_eq: 'width', product_properties_value_gt: 30).result
This again works fine but I would prefer to 'ransack' using the property name
Product.ransack(width_gt: 30).result
Are there any ways to dynamically create ransackers (or alternatives) that will allow me to do this? I have tried using method_missing but this confused me to no end. I was thinking of creating scopes on the model using all the name values in the properties table but thought I would ask for some advice first.
UPDATE
I have attempted implementing a series of custom ransackers on the product model
class Product < ActiveRecord::Base
Property.pluck(:name, :id).each do |name, id|
ransacker name, formatter: -> (value) { value.to_s.downcase } do
product_properties = Arel::Table.new(:product_properties)
product_properties[:value]
end
end
end
This is getting me ever closer to the answer I can feel it. What else shoudl I be doing here?
This does the job perfectly. The gotcha here is the Arel::Nodes.build_quoted
. I had originally left this out and I would get no errors/warning back but I would equally get no results either which left me rather stumped. This apparently is only necessary when usingRails 4.2+ (Arel 6.0+).
Property.pluck(:id, :name).each do |id, name|
product_properties = Arel::Table.new(:product_properties)
ransacker name.to_sym, formatter: -> (value) { value.to_s.downcase } do
Arel::Nodes::InfixOperation.new('AND',
Arel::Nodes::InfixOperation.new('=',
product_properties[:property_id], Arel::Nodes.build_quoted(id)
),
product_properties[:value]
)
end
end
To actually use this I then need to explicitly join the product_properties table to the query
Product.joins(:product_properties).ransack(width_gt: 30)
As the ransack documentation states the difficulty some people encounter with using ransackers stems not from Ransack, but from not understanding Arel. This was definitely the case here.