How to solve N + 1 issue in select box? - sql

In my Rails app I have people which can have many projects and vice versa. The two tables are linked by a join table jobs.
class Project < ActiveRecord::Base
belongs_to :user
has_many :people, :through => :jobs
def self.names_as_options
order(:name).map{ |p| [ p.name, p.id, :'data-people_count' => p.people.count ] }
end
end
In one of my forms I have this select box:
<%= f.select :project_id, Project.names_as_options %>
The problem is that the count on people gives me an N + 1 query for each project.
What is the best way to overcome this?
Thanks for any help.

Try use scope whit lambda, is for that, here is one example how this works:
scope :top, lambda { order('views DESC').limit(20) }
in controller just call
Project.top
this is the best way to filter results in Ruby on Rails.

If you use counts, you might better use also counter caches, they are automatically used when needed. http://guides.rubyonrails.org/association_basics.html

You could add a "counter cache" of the people count for each project. Ordinarily you would add a field to the projects table via a migration
add_column :projects, :people_count, :integer, :default => 0
and then declare to use :counter_cache in the Person model
class Person < ActiveRecord::Base
belongs_to :projects, :counter_cache => true
end
This probably won't do what you want as it stands, as you are going through a Job join. So, the Person#projects declaration is just a convenient finder, and not used in any callback. But, you get the idea.
You could add a column as suggested above, and then make use of some callback methods in the Job class.
class Job
def update_project_counter
project.update_people_counter
end
after_create :update_project_counter
after_destroy :update_project_counter
end
class Project
def update_people_counter
self.update_attribute :people_count, people.count
end
end
Or something similar thats appropriate. You should then only need the one query.
class Project < ActiveRecord::Base
def self.names_as_options
order(:name).map do |p|
[p.name, p.id, :'data-people_count' => p.people_count]
end
end
end

Eager loading will solve this issue, use 'includes' as follows.
Example,
class LineItem < ActiveRecord::Base
belongs_to :order, -> { includes :customer }
end
class Order < ActiveRecord::Base
belongs_to :customer
has_many :line_items
end
class Customer < ActiveRecord::Base
has_many :orders
end
Ref: http://guides.rubyonrails.org/association_basics.html

Related

Rails: Scope parent model by attribute of child

I'm having a tough time figuring something out in Rails. It probably has to do with my very limited knowledge of SQL, since I know Rails pretty well. I'm using Rails 5.
I have two models: Applicant and Application.
class Applicant < ApplicationRecord
has_one :application
has_many :skills
accepts_nested_attributes_for :application
accepts_nested_attributes_for :skills,
reject_if: ->(skill) { skill[:name].empty? || skill[:experience].empty? }
validates_with ApplicantValidator
end
class Application < ApplicationRecord
belongs_to :applicant
has_many :notes
VALID_STATUSES = ["in review", "accepted", "declined", "closed"]
validates_length_of :why_interested, minimum: 25
validates :accept_terms, acceptance: true
validates :status, inclusion: { in: VALID_STATUSES }
before_validation :set_status
private
def set_status
self.status ||= "in review"
end
end
I'd like to add a scope, :active, to the Applicant model that returns only applicants who have an application whose status is "in review". However, I can't find a way to access the application within a scope proc.
I've seen other suggestions for cases where there is a has_many relationship with the child, but they didn't work in my case.
I doubt it makes a difference, but I'm using Postgres. The closest I've come to a solution is to add this, but when I run RSpec it says there needs to be a FROM-clause for the applications table. I don't know how to effect that.
scope :active, -> { joins(:application).where('"application"."status" = "in review"') }
scope :in_review_applicants, -> { joins(:application).where('application.status = ?', :in_review) }
I think is something like that..

How to call a scope across a has_many / through relationship

I have the following (on RoR 3.1 and MySQL 5.1):
class Menu < ActiveRecord::Base
has_many :menu_headers
has_many :menu_items, :through => :menu_headers
belongs_to :location
end
class MenuHeader < ActiveRecord::Base
acts_as_tree :parent_id
has_many :menu_items
belongs_to :menu
end
class MenuItem < ActiveRecord::Base
scope :is_enabled, where(:is_enabled => true)
belongs_to :menu_header
end
I'd like to be able to call the scope across the relationship; something like this:
# call the scope :is_enabled here
Menu.find(12).(where menu_items.is_enabled)
but not sure how to do this.
I'd like the behavior for:
Menu.find(12)
to continue to pull menu_items where is_enabled=false
Any ideas on how to do this?
thx
edit #1
added the act_as_tree and location associations as these also need to be working.
Something like this Scope with join on :has_many :through association might work but seems a little bit ugly
This should do the trick:
Menu.find(12).menu_items.is_enabled
It will return all enabled menuitem associated with the menu with id 12.

Problems with validations

I have 2 models
class Variant < ActiveRecord::Base
belongs_to :product
with_options :if => :is_active? do |p_active|
p_active.validates :avatar, :presence => true
end
with_options :if => :isnt_diavoleria? do |p_active|
p_active.validates :color, :presence => true
end
def is_active?
self.product.active
end
def isnt_diavoleria?
a = (self.is_active? and self.product.section_id != 5)
a
end
end
class Product < ActiveRecord::Base
has_many :variants, :autosave => true
accepts_nested_attributes_for :variants
end
If i change the attribute section_id or active of a product and save, the validations of the model variant are executed with the old values of section_id and active.
Why?
How can i do the validations with the new values?
The problem is that by default a pair of has_many and belongs_to associations don't know that they are the inverse of each other. So when you
product.section_id = 23
product.save
then inside your validation, the variant goes
self.product
and actually fetches that from the database again, which obviously doesn't have your unsaved change.
You should be able to fix this by adding the :inverse_of flag to your associations, i.e.
class Variant < AR::Base
belongs_to :product, :inverse_of => :variants
end
class Product < AR::Base
has_many :variants, :inverse_of => :products
end
One day rails will have an identity map which should make this sort of stuff less error prone (it is in rails 3.1 but disabled because of subtle associated bugs if i remember correctly)
You probably need to do what #thoferon is suggesting (assuming you aren't doing taking nested attributes for products or something) or make sure all changes to the product are happening through the association object so it is up-to-date.
Maybe you are modifying a product through another Ruby object. The product referenced by the variant is still holding the old values. I don't know if this is what you're doing but it could be the case.
A solution could be to reload the product before validation.
class Variant
before_validation do
self.product.reload
end
end

How do i create an object if it has more than one belongs_to?

I have the following:
class Org < ActiveRecord::Base
has_many :users
has_many :entries
end
class Entry < ActiveRecord::Base
belongs_to :org
belongs_to :user
validates_presence_of :entry_text
end
class User < ActiveRecord::Base
belongs_to :org
has_many :entries
validates_uniqueness_of :user_name
validates_presence_of :user_name, :length => { :minimum => 3 }
end
I can Create Orgs and Users... How do i create an entry if there are two belongs_to? and what is this pattern called?
Double nested resources are tricky. The trick with users usually is to keep it out of your desired entry path.
Your question is kind of broad, but if you specify more information, people would be able to help you better. Also, I would recommend using the gem Devise for your user management system. Since you're using 'users' I would assume you want users from orgs to create entries. The entry created would be a part of org and the user would be the session's current user. Sorry if I am wrong to assume this.
Your routes.rb file can look something like this (assuming rails 3):
resources :orgs do
resources :entries
end
Then the create of your entry controller would look like:
#entry = #org.entries.new(params[:topic])
#entry.user = current_user #or however you are managing the current user's session.
And you'd want to set the org for the entire class by making a method that loads your current org and do a before_filter :loadOrg
def loadOrg
#org = Org.find(params[:id])
end
This is of course assuming your path is something like: /org/(id)/entry/(entry_id)
and not
/org/(id)/user/(user_id)/entry/(entry_id)
which in my opinion is unnecessary and can lead to more problems. You can always create a userpage model that calls all entries by users, but the default route doesn't necessarily have to include users in the path.
I don't see any problem.
#entry = Entry.create(:entry_text => "Hello World!")
Now questions to clarify what do you need:
Can #entry belongs both org and user at the same time? Or it can belongs to only one of them?
Should #entry belongs to at least one of them?
If #entry supposed to belong only one of them, so you should use Polymorphism
http://railscasts.com/episodes/154-polymorphic-association
class Entry < ActiveRecord::Base
belongs_to :textable, :polymorphic => true
validates_presence_of :entry_text
end
class Org < ActiveRecord::Base
has_many :users
has_many :entries, :as => :textable
end
class User < ActiveRecord::Base
belongs_to :org
has_many :entries, :as => :textable
validates_uniqueness_of :user_name
validates_presence_of :user_name, :length => { :minimum => 3 }
end

counter_cache has_many_through sql optimisation, reduce number of sql queries

How I can optimise my SQL queries, to ignore situations like this:
Meeting.find(5).users.size => SELECT COUNT(*) FROM ... WHERE ...
User.find(123).meetings.size => SELECT COUNT(*) FROm ... WHERE ...
I have no idea how to use counter_cache here.
Here is my model relation:
class Meeting < ActiveRecord::Base
has_many :meeting_users
has_many :users, :through => meeting_users
end
class User < ActiveRecord::Base
has_many :meeting_users
has_many :meetings, :through => meeting_users
end
class Meeting_user < ActiveRecord::Base
belongs_to :meeting
belongs_to :user
end
What are the most optimal solutions ?
And how implement counter_cache here ?
Starting from Rails3.0.5 and in newer versions, you are now able to set counter cache to the "linker" model, in your case it will be:
class MeetingUser < ActiveRecord::Base
belongs_to :meeting, :counter_cache => :users_count
belongs_to :user, :counter_cache => :meetings_count
end
It's important to explicitly specify count column names, otherwise the columns used will default to meeting_users_count.
As far as I know you can't use counter_cache with through associations, that's why you should manually increment it.
For example (untested):
class MeetingUser < ActiveRecord::Base
...
after_create { |record|
Meeting.increment_counter(:users_count, record.meeting.id)
}
after_destroy { |record|
Meeting.decrement_counter(:users_count, record.meeting.id)
}
end