Rails: apply where conditions to both model and associations - ruby-on-rails

in my app I have the models Item, Category and Categorization defined as above:
class Item < ActiveRecord::Base
attr_accessible :name, :description, :property_valuations, :barcode
has_many :categorizations
has_many :categories, :through => :categorizations
end
class Category < ActiveRecord::Base
attr_accessible :name, :description, :parent, :children, :items, :parent_id
has_many :children, :class_name => "Category", :foreign_key => "parent_id", :dependent => :nullify
belongs_to :parent, :class_name => "Category"
has_many :categorizations
has_many :items, :through => :categorizations
def all_children(children_array = [])
children = Category.where(:parent_id => self.id).includes(:items)
children_array += children.all
children.each do |child|
children_array = child.all_children(children_array)
end
children_array
end
end
class Categorization < ActiveRecord::Base
attr_accessible :category, :item
belongs_to :category
belongs_to :item
end
I'm search the category tree recursively for items associated to categories. However, besides filtering categories by parent_id, I want to apply a filter items name. How can I do this (I was expecting something like Category.where(:parent_id => self.id).includes(:items).where(:name => 'something...'))?
Thanks in advance!

You can do
Category.where(:parent_id => self.id).includes(:items).where("items.name like ?", '%cheap%')
That will get the categories, and the items that matched the criteria.
i.e.:
categories = Category.where(:parent_id => 2).includes(:items).where("items.name like ?", 'AA%')
categories.first.items.size # -> 2
categories.first.id # -> 1
Category.find(1).items.size # -> 3
Category.find(1).items.where("name like ?", 'AA%').count # -> 2
If you just want to get the items of the subcategories of a parent category:
parent_category = Category.find(2)
Item.joins(:categories).where(categories: {parent_id: parent_category.id}).where("items.name like ?", 'AA%')

Recursive querying (to avoid the manual recursion of your all_children function) might be supported by your database, eg postgres via the with command, but rails doesn't have a wrapper that accesses that. If that recursion happens a lot, it might be worth doing some reading here: http://gmarik.info/blog/2012/10/14/recursive-data-structures-with-rails -- and it does look there are some promising gems addressing the issue, but I haven't used any of them personally.

Related

creating associations rails during CRUD

I have a model with associations. How to create/update the associations as CRUD operations are performed on the model.
That is, when I run
#v1_seller = V1::Seller.new(seller_params)
#v1_seller.save
It should save the associations.
Should I create after_create hooks and pass the params (but then I will have to do the same in update)? Or am I missing something? I feel that it should be done automatically in rails.
currently I am doing it explicitly:
#v1_seller = V1::Seller.new(seller_params)
if #v1_seller.save
#v1_seller.assign_categories(params)
my seller model:
class V1::Seller < ActiveRecord::Base
has_many :categories, :class_name => 'V1::Category', dependent: :delete_all
has_many :category_names, :class_name => 'V1::CategoryName', through: :categories
# right now I am manually calling this after a create/update operation in my controller
def assign_categories(params)
params.require(:seller).require(:categories)
params.require(:seller).permit(:categories => []).permit(:name, :brands => [])
self.categories.clear
params[:seller][:categories].each do |c|
if c[:brands].nil? || c[:brands].empty?
next # skip the category if it has no brands associated with it
end
category_name = c[:name]
category = V1::Category.new
category.category_name = V1::CategoryName.find_by(name: category_name)
category.seller = self
category.save
c[:brands].each do |b|
begin
category.brand_names << V1::BrandName.find_by(name: b)
rescue ActiveRecord::RecordInvalid
# skip it. May happen if brand is already added to the particular category
end
end
end
end
end
And V1::Cateogry model:
class V1::Category < ActiveRecord::Base
belongs_to :category_name, :class_name => 'V1::CategoryName', inverse_of: :category
belongs_to :seller, :class_name => 'V1::Seller', inverse_of: :category
has_many :brands, :class_name => 'V1::Brand', dependent: :delete_all, inverse_of: :category
has_many :brand_names, :class_name => 'V1::BrandName', through: :brands, inverse_of: :category
validates :seller, :uniqueness => {:scope => [:category_name, :seller]}
end
Seem like you need nested attributes.
Checkout the docs here: http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html

Making sum on join tables

I am creating a Rails 3.2 web app.
In this app I got four tables. Project, Task, Article and Item.
What I want to do is to get all the task values (prices from article) summed up
in a single call.
This is what I tried and it works, but is it the best way of doing it?
#project.tasks.where("status = ?", "completed").joins(:articles).sum(:price)
Task table
class Task < ActiveRecord::Base
has_many :articles
has_many :items, :through => :articles
end
Article Join Table
class Article < ActiveRecord::Base
belongs_to :task
belongs_to :item
attr_accessible :account_id, :amount, :price, :item_id, :task_id
end
Item table
class Item < ActiveRecord::Base
has_many :articles
has_many :tasks, :through => :articles
end
to sum it up it looks ok the way you did it, but also you can prettify your code:
project.rb
has_many :completed_tasks, class: 'Task', :conditions => {:status => 'completed'}
controller
#project.completed_tasks.joins(:articles).sum(:price)

Ruby on Rails: filter associations based on several relations and conditions

In my application I the have the models Category, Item, Property and PropertyValuation. The idea is that a category contains items, and an item has several properties. PropertyValuation purpose is to store the property value for the specific items. The models are defined as above:
class Category < ActiveRecord::Base
attr_accessible :name, :description, :parent, :children, :items, :parent_id
has_many :children, :class_name => "Category", :foreign_key => "parent_id", :dependent => :nullify
belongs_to :parent, :class_name => "Category"
has_many :categorizations
has_many :items, :through => :categorizations
end
class Item < ActiveRecord::Base
attr_accessible :name, :description, :property_valuations, :barcode
has_many :property_valuations, :dependent => :destroy
has_many :properties, :through => :property_valuations
has_many :categorizations
has_many :categories, :through => :categorizations
end
class Property < ActiveRecord::Base
attr_accessible :name, :description, :value_type, :unit, :unit_id
has_many :property_valuations, :dependent => :destroy
has_many :items, :through => :property_valuations
has_many :property_ranges, :dependent => :destroy
belongs_to :unit
end
class PropertyValuation < ActiveRecord::Base
attr_accessible :property, :item, :value, :categorization
belongs_to :property
belongs_to :item
end
Now my question, I've successfully managed to filter categories items by name by doing this:
#category.items.where("lower(items.name) like ?", "%#{params[:keywords].downcase}%")
But now I also want to filter those items depending on the associated property value. I receive the property ids and the values for each property (exact value, minimum value or maximum value), and the idea is to build the query dynamically. Given my models, how can I do this for example: I want the items whose name contains "foo", and where property with id=1 has value 2, property with id=2 has value<10, and property with id=8 has value>2 and value<5.
You can drive the search off the PropertyValuation model and join it to the product and category models
valuations = PropertyValuation.joins(:item)
.where(value: 2, property_id: 1)
.where('lower(items.name) LIKE ?', "%#{params[keywords].downcase}%")
items = valuations.map(&:item)
There a gems that make this thing a long easier, one is Ransack https://github.com/ernie/ransack
Item.search(name_contains: params[:keywords],
product_valuations_value_equals: 2,
product_valuations_property_id: 1)

Eager loading nested association and scope

I'm a beginner and it's hard to explain my problem:
My models:
class Skill
has_many :categories
has_many :positions, :through => :categories
end
class Category
belongs_to :skill
has_many :positions
end
class Position
belongs_to :category
has_one :skill, :through => :category
end
I can successfully eager load everything, like this:
#skills = Skill.includes(:positions)
However sometimes I want to apply a scope on the Positions:
class Position
...
scope :active, where(:hidden => false)
end
I wish I could do:
#skills = Skill.includes(:positions.active)
Instead, I apply the scope in the views, but the eager loading doesn't work anymore:
<%= skill.positions.acitve ... %>
Is it possible to have both eager loading and scope?
You could use another association:
class Skill
has_many :categories
has_many :positions, :through => :categories
has_many :active_positions, :through => :categories
end
class Category
belongs_to :skill
has_many :positions
has_many :active_positions, :class_name => "Position", :conditions => {:hidden => false}
end
class Position
belongs_to :category
has_one :skill, :through => :category
end
And then
#skills = Skill.includes(:active_positions)
But then you'll get two associations. If you ever use skill.positions, all the skill's positions will be loaded from the database. You should only use skill.active_positions.
Try this:
#skills = Skill.includes(:positions).where('position.active = TRUE')

Retrieving unique associated models from an array of another model

What is the recommended approach for finding multiple, unique associated models for a subset of another model? As an example, for a subset of users, determine unique artist models they have favorited.
One approach is to grab the users from the database, then iterate them all quering for favorites and building a unique array, but this seems rather inefficient and slow.
class User < ActiveRecord::Base
has_many :favorites
end
class Artist < ActiveRecord::Base
has_many :favorites
end
class Favorite < ActiveRecord::Base
belongs_to :user
belongs_to :artist
end
#users = User.find_by_age(26)
# then determine unique favorited artists for this subset of users.
The has_many association has a option called uniq for this requirement:
class User < ActiveRecord::Base
has_many :favorites
has_many :artists, :through => :favorites, :uniq => true
end
class Artist < ActiveRecord::Base
has_many :favorites
has_many :users, :through => :favorites, :uniq => true
end
class Favorite < ActiveRecord::Base
belongs_to :user
belongs_to :artist
end
Usage:
# if you are expecting an array of users, then use find_all instead of find_
#users = User.find_all_by_age(26, :include => :artists)
#users.each do |user|
user.artists # unique artists
end
Edit 1
I have updated the answer based on user's comment.
Solution 1- :group
Artist.all(:joins => :users, :group => :id,
:conditions => ["users.age = ?", 26])
Solution 2- SELECT DISTINCT
Artist.all(:joins => :users, :select => "DISTINCT artists.*",
:conditions => ["users.age = ?", 26]))

Resources