How do I effectively preload conditional associations in Rails? - ruby-on-rails

I have the following two models:
class Company < ActiveRecord::Base
has_many :employees # A company can have 1000's of employees.
end
class Employee < ActiveRecord::Base
belongs_to :company
end
One of my use cases looks like this
In the controller:
def my_action
#companies = Company.where(country: 'GB').preload(:employees)
#title = params[:title] # E.g. 'CTO'
end
In the view:
- #companies.each do |c|
= c.employees.where(title: #title).first.last_name
The problem with the above is that it will create an N+1 in the view
Now I could change the view to:
# In the view
- #companies.each do |c|
= c.employees.select{|e| e.title == #title}.first.last_name
This will solve the N+1 but it will still load out potentially 1000's of employee records even though I don't need them (I only need the ones with the right title).
I could solve that by:
class Company < ActiveRecord::Base
has_many :employees
has_many :ctos, ->{ where(title: 'CTO') }
end
# In the controller
def my_action
#companies = Company.where(country: 'GB').preload(:ctos)
end
# In the view
- #companies.each do |c|
= c.ctos.first.last_name
The problem with this however is that it would require me to add associations for every single possible type on Company.
So the question is, how can I solve this?

companies = Company.where(country: 'GB')
title = params[:title]
#company_employee = Employee.where(company: companies, title: title).group_by(&:company)
#company_employee will be hash with structure { company => [employees] }

Related

How to access a property from belongs_to model from a scope

I have 2 models in my app: Person and Review each person has many reviews and each review belongs to a person. I have an attribute called grade inside of my review model and I'd like to display the average grade of each person so I wrote this scope:
scope :average_grade, -> { self.first.review.average(:grade) }
Is there a better way for doing that? Besides the 2 queries that this method needs, I also have to run another 2 queries to get the proper Person object in my controller:
def show
#average = Person.includes(:review).where(id: params[:id]).average_grade
#person = Person.includes(:review).find(params[:id])
end
How could I avoid all of those queries?
Your scope is an instance method rather than scope, since it does not return ActiveRecord::Relation object.
I suggest you to do following:
# person.rb:
def average_grade
review.average(:grade)
end
# controller:
def show
#person = Person.find(params[:id])
#average = #person.average_grade
end
# person.rb
class Person < ActiveRecord::Base
has_many :reviews
end
# review.rb
class Review < ActiveRecord::Base
belongs_to :person
scope :by_person, ->(person) { where(person_id: person) }
end
# persons_controller
class PersonsController < ApplicationController
helper_method :person
private
def person
return #person if defined? #person
#person = Person.find(params[:id])
end
end
# show.html.haml
- present(person) do |person_presenter|
%p= person_presenter.average_grade
# person_presenter.rb
class PersonPresenter < BasePresenter
present :person
def average_grade
Review.by_person(person).average(:grade)
end
end
More on presenters you could find here Railscasts PRO #287 Presenters from Scratch

Rails has_many relationship find name based on foreign key

I have two models:
class country < ActiveRecord::Base
has_many :companies
end
class company < ActiveRecord::Base
belongs_to :country
end
In my view index for company I can show which country the company belongs to by showing the following:
<%= company.country_id %>
This will show me the id number it's associated with, but I can't seem to work out how to resolve this back to the country name which is country.name, everything I seem to try crashes rails, I don't think I'm approaching the problem the correct way?
<%= company.country.try(:name) %>
really ought to do what you want.
Edited as comment suggested.
I wouldn't recommend using #try though. It is much better when you tell Rails to give you the filtered collection, that's what controllers are for.
In this case you'd have
class CompaniesController < ApplicationController
def index
#companies = Company.where('country_id IS NOT NULL') #if using the relational db
# or #companies = Company.all.select { |company| company.country.present? }
end
end
and in your view
<% #companies.each do |company| %>
<%= company.country.name %>
<% end %>
Update:
Even better aproach is to create a named scope in your model for such things.
class Company < ActiveRecord::Base
#...
scope :with_country, ->() { where('country_id IS NOT NULL') }
end
Now you can change your controller action
def index
#companies = Company.with_country
end
This will make your code much more consistent and readable.

Nested Resource - Controller to filter by current_user also

Thanks to some help in another question, I now have a nested resource setup, and it is nearly working the way I need it to. This is a follow-up question about the controller.
Expenses have both a user, and a project that they belong to.
I would like to visit /projects/5/expenses, and see a list of all the expenses for that project, (which IS working), but also have it sensitive to the user that is currently signed in, so they only see their own expenses.
first the models:
class Expense < ActiveRecord::Base
attr_accessible :amount, :project_id, :user_id
belongs_to :project
belongs_to :user
end
each of the other models has "has_many :expenses", to complete the relationship.
so my route looks like:
resources :projects do
resources :expenses
end
And
class ExpensesController < ApplicationController
def index
#user = current_user
#project = Project.find(params[:project_id])
#expense_list = #project.expenses.all
end
How can I filter my #expense_list further by only showing the current_user's expenses?
You need a an additional condition to query the expenses based on the user that they belong to.
I would suggest that you create a scope in your Expense model
scope :for_user, lambda{ |user|
where( :user_id => user.id )
}
And you can do this in the controller:
#expense_list = #project.expenses.for_user(current_user).all

How do I count records that satisfy a condition in a associated model?

Okay, what I've got is two models...
Jiraissue:
class Jiraissue < ActiveRecord::Base
# JIRA uses a singular table name for this model
set_table_name 'jiraissue'
has_one :severity
end
Severity:
class Severity < ActiveRecord::Base
belongs_to :jiraissue
end
What I'm trying to do is get a count of all Jiraissues for which jiraissue.severity = "S1"
Now it turns out that the jiraissue table has a column for priority so I can pull this trick in the model...
Jiraissue:
class Jiraissue < ActiveRecord::Base
# JIRA uses a singular table name for this model
set_table_name 'jiraissue'
has_one :severity
def self.count_priority(priority)
where("PRIORITY = ?",priority).count()
end
end
And then in the view do something like...
<%= (1..4).map {
|priority| Jiraissue.biit.bugs.recent.count_priority(priority)
}.inspect %>
How do I do something similar for Jiraissue to get a count_severity method?
This just doesn't work (nor would I expect it to)...
def self.count_severity(severity)
where("severity = ?",severity).count()
end
But I'm totally confused.
Jiraissue.joins(:severities).where(:severities => {:severity => "S1"}).count
model
def self.count_priority(priority)
where("PRIORITY = ?",priority).size
end
controller
def index
#jiraissues = Jiraissue.count_priority('S1')
end
Doesn't it work?

get name of object following 2 time a relationship (2x has_many)

I have an author, who writes products, which are placed in groups.
What I want to do is to list the groups that an author is involved into.
So, I have first to follow the link from author to products, which works with :
#author = Author.find(params[:id])
#products = #author.products
which gives lot of products :-) Then I have to find (all) the groups that are linked to all of the products (because a group can contain a lot of products, I have already a has_many link with group_id column in the product table)
But when I try to write something as
#groups = #author.products.groups
I get the error : undefined method `groups' for # Class:0x000000032a2198
Why ?
Here is the model, where only the "through" clauses seem not to work ?
class Author < ActiveRecord::Base
has_and_belongs_to_many :products
has_many :groups, :through => :products
end
class Product < ActiveRecord::Base
belongs_to :group
has_and_belongs_to_many :authors
end
class Group < ActiveRecord::Base
has_many :products, :dependent => :destroy
has_many :authors, :through => :products
end
Thanks !
Edit : I found an ugly way to do what I want. But is there no better RoR way ??
def show
#author = Author.find(params[:id])
#products = #author.products
#groups = []
#products.each do |product|
group = Group.find(product.group_id)
unless #groups.include?(group)
#groups << group
end
end
end
As far as I know there's no way to do it in a Rails way. First, here's how to make what you have more "Railsy":
def show
#author = Author.find(params[:id])
#products = #author.products
#groups = []
#products.each do |product|
unless #groups.include?(product.group)
#groups << group
end
end
end
But a shorter way would be:
#products.map(&:group).uniq
Quick explanation of what it's doing. First it's using the symbol to proc shorthand that was introduced in Rails 1.1 (http://blog.hasmanythrough.com/2006/3/7/symbol-to-proc-shorthand). That builds an array of groups. Then b/c, in your question, you are checking for uniqueness (using include?) I call .uniq which weeds out any duplicates.
I also found another way to do this :
#user = User.find(params[:id])
#groupcollect = []
#user.products.each { |e| #groupcollect << e.group_id} # collect group_id for all products
#mygroups = Group.find(#groupcollect.uniq) # gets the groups for all group_id, unique
:-)

Resources