How to prevent using a named_scope from creating n+1 queries - sql

What is the best way to prevent ActiveRecord from making an unnecessary query when referencing a list of models that already been loaded?
class Checklist
has_many :checklist_items
scope :active, where(active: true)
end
class ChecklistItem
belongs_to :checklist
scope :active, where(active: true)
end
#checklists = Checklist.active.includes(:checklist_items).where(checklist_items: {active: true})
# Works fine, does NOT make extra SQL query
#checklists[0].checklist_items
# Makes extra query. How do I prevent this?
#checklists[0].checklist_items.active
What is the best work around for preventing this extra query when using a named_scope?

As soon as you use where, you'll trigger another query. If you think you have all your records in memory, treat the resulting active record relation as an array, not SQL. Use array methods like select instead of where. Perhaps:
#checklists[0].checklist_items.select {|ci| ci.active == true}

Related

Rails ActiveRecord query

My question is twofold... Primarily, I am trying to figure out how to ask > or < when filtering this query. You can see at the end I have .where(:created_at > 2.months.ago) and that is improper syntax, but I'm not sure the correct way to call something similar.
Secondly, this is a bit of a long string and is going to get longer as the are more conditions I have to factor in. Is there a cleaner way of building this, or is a long string of conditions like this pretty standard?
class PhotosController < ApplicationController
def showcase
#photos = Photo.order(params[:sort] || 'random()').search(params[:search]).paginate(:per_page => 12, :page => params[:page]).where(:created_at > 2.months.ago)
end
Thanks.
Unfortunately you've hit a sore point in the ActiveRecord querying api. There is no standard, out of the box way to do this. You can do date ranges very easily, but < and > have no easy path. However Arel, the underlying SQL engine, can do this very easily. You could write a simple scope to handle it thusly:
scope :created_after, lambda {|date| where arel_table[:created_at].gt(date) }
And you could refactor this easily to take a column, or gt versus lt, etc.
Other people have solved this problem already, however, and you could take advantage of their work. One example is MetaWhere, which adds a bunch of syntactic sugar to your queries. For example, using it you might write:
Article.where(:title.matches => 'Hello%', :created_at.gt => 3.days.ago)
On #2, scopes do tend to get long. You might look into the gem has_scope, which helps to alleviate this by defining scopes on the controller in an analogous way to how they are defined on the model. An example from the site:
# The model
# Note it's using old Rails 2 named_scope, but Rails 3 scope works just as well.
class Graduation < ActiveRecord::Base
named_scope :featured, :conditions => { :featured => true }
named_scope :by_degree, proc {|degree| { :conditions => { :degree => degree } } }
end
# The controller
class GraduationsController < ApplicationController
has_scope :featured, :type => :boolean
has_scope :by_degree
def index
#graduations = apply_scopes(Graduation).all
end
end
You can do where(["created_at > ?", 2.months.ago]) for your first question.
For your second question there are several solutions :
You can use scopes to embed the conditions in them and then combine them.
You can break the line in multiple lines.
You can keep it like this if you have a large screen and you don't work with any other people.

ARel mimic includes with find_by_sql

I've got a fairly complex sql query that I'm pretty sure I can't accomplish with ARel (Rails 3.0.10)
Check out the link, but it has a few joins and a where exists clause, and that I'm pretty sure is too complex for ARel.
My problem however is that, before this query was so complex, with ARel I could use includes to add other models that I needed to avoid n+1 issues. Now that I'm using find_by_sql, includes don't work. I still want to be able to fetch these records and attach them to my model instances, the way includes does, but I'm not quite sure how to achieve this.
Can someone point me in the right direction?
I haven't tried joining them in the same query yet. I'm just not sure how they would be mapped to objects (ie. if ActiveRecord would properly map them to the proper class)
I know that when using includes ActiveRecord actually makes a second query, then somehow attaches those rows to the corresponding instances from the original query. Can someone instruct me on how I might do this? Or do I need to join in the same query?
Let's pretend that the SQL really can't be reduced to Arel. Not everything can, and we happen to really really want to keep our custom find_by_sql but we also want to use includes.
Then preload_associations is your friend:
(Updated for Rails 3.1)
class Person
def self.custom_query
friends_and_family = find_by_sql("SELECT * FROM people")
# Rails 3.0 and lower use this:
# preload_associations(friends_and_family, [:car, :kids])
# Rails 3.1 and higher use this:
ActiveRecord::Associations::Preloader.new(friends_and_family, [:car, :kids]).run
friends_and_family
end
end
Note that the 3.1 method is much better, b/c you can apply the eager-loading at any time. Thus you can fetch the objects in your controller, and then just before rendering, you can check the format and eager-load more associations. That's what happens for me - html doens't need the eager loading, but the .json does.
That help?
I am pretty sure that you can do even the most complex queries with Arel. Maybe you are being over-skeptical about it.
Check these:
Rails 3: Arel for NOT EXISTS?
How to do "where exists" in Arel
#pedrorolo thanks for the heads up on that not exists arel query, helped me achieve what I needed. Here's the final solution (they key is the final .exists on the GroupChallenge query:
class GroupChallenge < ActiveRecord::Base
belongs_to :group
belongs_to :challenge
def self.challenges_for_contact(contact_id, group_id=nil)
group_challenges = GroupChallenge.arel_table
group_contacts = GroupContact.arel_table
challenges = Challenge.arel_table
groups = Group.arel_table
query = group_challenges.project(1).
join(group_contacts).on(group_contacts[:group_id].eq(group_challenges[:group_id])).
where(group_challenges[:challenge_id].eq(challenges[:id])).
where(group_challenges[:restrict_participants].eq(true)).
where(group_contacts[:contact_id].eq(contact_id))
query = query.join(groups).on(groups[:id].eq(group_challenges[:group_id])).where(groups[:id].eq(group_id)) if group_id
query
end
end
class Challenge < ActiveRecord::Base
def self.open_for_participant(contact_id, group_id = nil)
open.
joins("LEFT OUTER JOIN challenge_participants as cp ON challenges.id = cp.challenge_id AND cp.contact_id = #{contact_id.to_i}").
where(['cp.accepted != ? or cp.accepted IS NULL', false]).
where(GroupChallenge.challenges_for_contact(contact_id, group_id).exists.or(table[:open_to_all].eq(true)))
end
end

Rails 3: how to write DRYer scopes

I'm finding myself writing very similar code in two places, once to define a (virtual) boolean attribute on a model, and once to define a scope to find records that match that condition. In essence,
scope :something, where(some_complex_conditions)
def something?
some_complex_conditions
end
A simple example: I'm modelling a club membership; a Member pays a Fee, which is valid only in a certain year.
class Member < ActiveRecord::Base
has_many :payments
has_many :fees, :through => :payments
scope :current, joins(:fees).merge(Fee.current)
def current?
fees.current.exists?
end
end
class Fee < ActiveRecord::Base
has_many :payments
has_many :members, :through => :payments
scope :current, where(:year => Time.now.year)
def current?
year == Time.now.year
end
end
Is there a DRYer way to write a scopes that make use of virtual attributes (or, alternatively, to determine whether a model is matched by the conditions of a scope)?
I'm pretty new to Rails so please do point out if I'm doing something stupid!
This in not an answer to the question, but your code has a bug (in case you use something similar in production): Time.now.year will return the year the server was started. You want to run this scope in a lambda to have it behave as expected.
scope :current, lambda { where(:year => Time.now.year) }
No, there's no better way to do what you're trying to do (other than to take note of Geraud's comment). In your scope you're defining a class-level filter which will generate SQL to be used in restricting the results your finders return, in the attribute you're defining an instance-level test to be run on a specific instance of this class.
Yes, the code is similar, but it's performing different functions in different contexts.
Yes, you can use one or more parameters with a lambda in your scopes. Suppose that you have a set of items, and you want to get back those that are either 'Boot' or 'Helmet' :
scope :item_type, lambda { |item_type|
where("game_items.item_type = ?", item_type )
}
You can now do game_item.item_type('Boot') to get only the boots or game_item.item_type('Helmet') to get only the helmets. The same applies in your case. You can just have a parameter in your scope, in order to check one or more conditions on the same scope, in a DRYer way.

rails 3: difference between write_attribute and update_attribute

I did not know about write_attribute until today...
it seems like update_attribute, although not calling validation is still calling the :before_save callbacks, whereas write_attribute doesn't.
Is that the difference between these two methods?
update_attribute actually makes a physical call to the DB. You get a full execution of an UPDATE statement. It's like update_attributes but just for a single column.
While write_attribute writes the attribute for assignment to the model for AR based columns. If you were to overwrite a DB based attribute.
def first_name=(val)
write_attribute :first_name, val
end
# some_model.first_name => 'whatever val is'
def first_name=(val)
#first_name = val
end
# some_model.first_name => nil
I have not looked into write_attribute extensively, but I gather Activerecord based models handle assignments to db based columns slightly differently than your run of the mill accessor.
write_attribute is used when you want to overwrite the default accessors for a method. It is essentially syntactic sugar for self[:attribute]=(value).
Have a look at the ActiveRecord::Base documentationunder the heading "Overwriting default accessors".
If you tried to rewrite the example in the documentation using update_attribute, I'd imagine it would end up in a loop.

Do Rails 3 Active Record dynamic find_or_create finder methods have some undocumented inconsistencies?

Apologies for the long title, but this is bothering me. I'm new to Rails, so this is my first project. Rails 3.0.3.
In my model, a User may or may not have read many Entries; this is tracked in a model called ReadEntries. This many-to-one relationship is properly defined in the code, I think.
User.rb:
has_many :read_entries
Entry.rb:
has_many :read_entries
ReadEntry.rb:
belongs_to :entry
belongs_to :user
This table has to be populated at some point. If I try to do this:
user.read_entries.find_or_create_by_entry_id(entry.id, :read => false)
I get the error Unknown key(s): read. Leave out trying to set :read, and it works.
However, if I create the same row with this, it works:
ReadEntry.find_or_create_by_entry_id_and_user_id(entry.id, user.id, :read => false)
Logically, these methods should be identical, right? Thanks.
I've also had weird experiences with find_or_create. I would love it if it worked, but it seems inconsistent.
I'm currently having the same issue as you, and I think it may be due to calling find_or_create on an association as opposed to the model directly. Here's my example:
permission_assignments.find_or_create_by_role_id(:role_id => role_id, :is_allowed => false)
This works to create the assignment, except the "is_allowed" field gets set to it's default of "true". This code works for me (in the Permission model, hence the self reference)
PermissionAssignment.find_or_create_by_permission_id_and_role_id(:permission_id => self.id, :role_id => role_id, :is_allowed => false)
It's more verbose, unfortunately, but it works. The only problem that I still notice is that the object that is returned has no id assigned (the record does get created in the database, however, but if I wanted to update any more attributes I wouldn't be able to without the id). Don't know if that's a separate issue or not.
Rails 3.0.4 here with Postgres 8.4
You cannot pass in other fields like that as Rails will assume they are options for the find. Instead, you will need to make your method call longer:
user.read_entries.find_or_create_by_entry_id_and_read(entry.id, false)
Or alternatively use a shorter, custom syntax for that.
For your final example, my thoughts are that Rails will take the second argument and use that as options. Other than that, I am not sure.