default_scope breaks (update|delete|destroy)_all in some cases - ruby-on-rails

I believe this is a bug in Rails 3. I am hoping someone here can steer me in the correct direction. The code posted below, is purely for illustration of this problem. Hopefully this does not confuse the issue.
Given I have a Post model, and a Comment model. Post has_many Comments, and Comment belongs_to Post.
With a default_scope set on the Post model, defining joins() and where() relations. In this case where() is dependent on joins().
Normally Posts wouldn't be dependent on Comments. Again, I just want to give a simple example. This could be any case when where() is dependent on joins().
class Post < ActiveRecord::Base
has_many :comments, :dependent => :destroy
default_scope joins(:comments).where("comments.id < 999")
end
class Comment < ActiveRecord::Base
belongs_to :post, :counter_cache => true
end
Running the following command:
Post.update_all(:title => Time.now)
Produces the following query, and ultimately throws ActiveRecord::StatementInvalid:
UPDATE `posts` SET `title` = '2010-10-15 15:59:27' WHERE (comments.id < 999)
Again, update_all, delete_all, destroy_all behave the same way. I discovered this behaviour when my application complained when trying to update the counter_cache. Which eventually drills down into update_all.

I had this problem also, but we really needed to be able to use update_all with complex conditions in the default_scope (for example, without the default scope eager-loading is impossible, and pasting a named scope literally everywhere is no fun at all). I have opened a pull request here with my fix:
https://github.com/rails/rails/pull/8449
For delete_all I've raised an error if there's a join condition to make it more obvious what you have to do (instead of just tossing the join condition and running the delete_all on everything, you get an error).
Not sure what the rails guys are going to do with my pull request, but thought it was relevant to this discussion. (Also, if you need this bug fixed, you could try out my branch and post a comment on the pull request.)

I ran into this as well.
If you have
class Topic < ActiveRecord::Base
default_scope :conditions => "forums.preferences > 1", :include => [:forum]
end
and you do a
Topic.update_all(...)
it’ll fail with
Mysql::Error: Unknown column 'forums.preferences' in 'where clause'
The work around for this is:
Topic.send(:with_exclusive_scope) { Topic.update_all(...) }
You can monkey patch this using this code (and requiring it in environment.rb or else where)
module ActiveRecordMixins
class ActiveRecord::Base
def self.update_all!(*args)
self.send(:with_exclusive_scope) { self.update_all(*args) }
end
def self.delete_all!(*args)
self.send(:with_exclusive_scope) { self.delete_all(*args) }
end
end
end
end
Then just you update_all! or delete_all! when it has a default scope.

You can also do this on the class level, without creating new methods, like so:
def self.update_all(*args)
self.send(:with_exclusive_scope) { super(*args) }
end
def self.delete_all(*args)
self.send(:with_exclusive_scope) { super(*args) }
end

I don't think I'd call it a bug. The behavior seems logical enough to me, although not immediately obvious. But I worked out a SQL solution that seems to be working well. Using your example, it would be:
class Post < ActiveRecord::Base
has_many :comments, :dependent => :destroy
default_scope do
with_scope :find => {:readonly => false} do
joins("INNER JOIN comments ON comments.post_id = posts.id AND comments.id < 999")
end
end
end
In reality I'm using reflection to make it more robust, but the above gets the idea cross. Moving the WHERE logic into the JOIN ensures that it won't be applied in inappropriate places. The :readonly option is to counteract Rails's default behavior of making joins'd objects readonly.
Also, I know that some people deride the use of default_scope. But for multi-tenant apps, it's a perfect fit.

Related

Condition for association Rails 4

There's a way to condition something to an associative table of ActiveRecord?
I retrieve segments this way:
#segments = Segment.all
But, a Segment has_many products. See:
models/product.rb:
class Product < ActiveRecord::Base
belongs_to :segment, dependent: :destroy
end
models/segment.rb:
class Segment < ActiveRecord::Base
has_many :products
end
The problem is: I just want to retrieve products whose its status is equals to 1. I can condition something like this using where on Segment model, but how can I achieve this for products?
What I already tried
I found a solution. Take a look:
#segments = Segment.find(:all, include: :products, conditions: {products: {status: 1}})
It worked, but I think the code can be better.
Why I think the code can be better
Well, why should I use include: :products if the association is already live within the models? We're associating things through the model and I'm sure that is something near to enough.
Ideas?
Segment.joins(:products).where("products.status = 1")
You can also use includes instead of joins. But rails will convert it into a join internally since you are using the products table attribute in the query
A few tips, that might help you.
For easy naming purposes, I am considering the status==1 as being active. Of course I have no idea what it means in your specific case.
class Product
ACTIVE=1
def self.active
where(status: ACTIVE)
end
end
Now you write something like:
segment.products.active
and this will return only the active products for the given segment.
The solution you found, which will retrieve all segments with (active) products, could be written differently as follows:
Segment.includes(:products).where(products: {status: 1})
Now, why so elaborate: this actually translates to a sql query, so you have to be a little more explicit about it.
If you only ever want those with a status of 1
class Segment < ActiveRecord::Base
has_many :products, :conditions => { :status => 1 }
end
In rails 3 or
class Segment < ActiveRecord::Base
has_many :products, -> { where status: 1 }
end
In rails 4
Obviously can use status: true if it's a boolean
Then
#segments = Segment.includes(:products)
The association has_many :products makes it possible to use include: :products in your scope. Therefore you shouldn't doubt in your solution. It is right, and it is just the same as solutions presented in the other answers but by other syntacsis.
This should do the job - and it's compatibile with AREL syntax:
#segments = Segment.joins(:products).where(products: {status: 1})
It's quite different that solution with include (or includes, as it would be Rails 3/4), because it generates query with INNER JOIN, while includes generates LEFT OUTER JOIN. Also, includes is usually used for eager loading associated records, not for queries with JOIN.

Is this eager loading doing what I think it's meant to be doing?

I've just changed an action in competitors_controller.rb from...
def audit
#review = Review.find(params[:review_id])
#competitor = #review.competitors.find(params[:id])
respond_with(#review, #competitor)
end
...to...
def audit
#review = Review.find(params[:review_id])
#competitor = Competitor.find(params[:id], :include => {:surveys => {:question => [:condition, :precondition]}})
respond_with(#review, #competitor)
end
...as the page was timing out on loading.
The underlying associations are:
class Competitor < ActiveRecord::Base
has_many :surveys
end
class Survey < ActiveRecord::Base
belongs_to :competitor
belongs_to :question
delegate :dependencies, :precondition, :condition, :to => :question
end
class Question < ActiveRecord::Base
has_many :dependancies, :class_name => "Question", :foreign_key => "precondition_id"
belongs_to :precondition, :class_name => "Question"
has_many :surveys, :dependent => :delete_all
end
Basically, the audit.html.haml page loads:
#competitor.surveys.{sorting, etc}.each do |s|
s.foo, s.bar
s.{sorting, etc}.dependant_surveys.each do |s2|
s2.foo, s2.bar
s2.{sorting, etc}.dependant_surveys.each do |s3|
s3.foo, s3.bar, etc etc
If I nest it too far, the page doesn't load before it times out.
What I need to know is whether the eager loading I have inserted into competitor_controllers.rb theoretically speeds up each of the following two methods, which are called so often when building audit.html.haml?
class Survey < ActiveRecord::Base
def dependant_surveys
self.review.surveys.select{|survey| self.dependencies.include?(survey.question)}
end
end
class Question < ActiveRecord::Base
def dependencies
Question.all.select{|question| question.precondition == self}
end
end
(I say "theoretically" as I know the question could also be answered with benchmarking. But before I get that far I want to check I have the theory right.)
You do a lot of processing in ruby, and there's no need for that. You should move all operations like
Question.all.select{|question| question.precondition == self}
#competitor.surveys.{sorting, etc}
to database.
If I understand properly, first line is meant to select all records that have a precondition set to given question. Remember that Question.all returns an array so you perform select in array, and you could do it in db with simple scope scope :has_precondition, -> {|q| where(precondition_id: q.id} or so.
Given you always sort models in same way, you may consider creating a default scope with order clause. Doing it in ruby is very counter-efficient.
The eager loading looks like it should work, but much more importantly these are things that you could and should be doing using SQL. Loading and iterating through all the ActiveRecord models in your DB has the potential to be incredibly time consuming (as you've found), whereas working out how to do it in SQL will allow you to load all the models directly from a single query. It can take a while to get a handle on this, but it's well worth it!
As far as I know you can only include associations for eager loading. I don't think what you have will work at all. You haven't shown us where Question.condition is defined, or Survey.review. Also, the associations that you are eager loading won't be used by your methods dependent_surveys and dependencies as they're performing their own queries.
AFAICT your eager loading won't make a jot of difference, or it will probably slow it down. I think you'll have to refactor your dependent_surveys as an association and eager load that. I haven't got the slightest clue what that method is trying to do, on a high level, so I'm not even going to attempt to refactor it.

Including an association if it exists in a rails query

Update: This may be something that just isn't doable. See this
TLDR: How do you conditionally load an association (say, only load the association for the current user) while also including records that don't have that association at all?
Rails 3.1, here's roughly the model I'm working with.
class User
has_many :subscriptions
has_many :collections, :through => :subscriptions
end
class Collection
has_many :things
end
class Thing
has_many :user_thing_states, :dependent => :destroy
belongs_to :collection
end
class Subscription
belongs_to :user
belongs_to :collection
end
class UserThingState
belongs_to :user
belongs_to :thing
end
There exist many collections which have many things. Users subscribe to many collections and thereby they subscribe to many things. Users have a state with respect to things, but not necessarily, and are still subscribed to things even if they don't happen to have a state for them. When a user subscribes to a collection and its associated things, a state is not generated for every single thing (which could be in the hundreds). Instead, states are generated when a user first interacts with a given thing. Now, the problem: I want to select all of the user's subscribed things while loading the user's state for each thing where the state exists.
Conceptually this isn't that hard. For reference, the SQL that would get me the data needed for this is:
SELECT things.*, user_thing_states.* FROM things
# Next line gets me all things subscribed to
INNER JOIN subscriptions as subs ON things.collection_id = subs.collection_id AND subs.user_id = :user_id
# Next line pulls in the state data for the user
LEFT JOIN user_thing_states as uts ON things.id = uts.thing_id AND uqs.user_id = :user_id
I just don't know how to piece it together in rails. What happens in the Thing class? Thing.includes(:user_thing_states) would load all states for all users and that looks like the only tool. I need something like this but am not sure how (or if it's possible):
class Thing
has_many :user_thing_states
delegates :some_state_property, :to => :state, :allow_nil => true
def state
# There should be only one user_thing_state if the include is correct, state method to access it.
self.user_thing_states.first
end
end
I need something like:
Thing.includes(:user_question_states, **where 'user_question_state.user_id => :user_id**).by_collections(user.collections)
Then I can do
things = User.things_subscribed_to
things.first.some_state_property # the property of the state loaded for the current user.
You don't need to do anything.
class User
has_many :user_thing_states
has_many :things, :through => :user_thing_states
end
# All Users w/ Things eager loaded through States association
User.all.includes(:things)
# Lookup specific user, Load all States w/ Things (if they exist for that user)
user = User.find_by_login 'bob'
user.user_thing_states.all(:include => :things)
Using includes() for this already loads up the associated object if they exist.
There's no need to do any filtering or add extra behavior for the Users who don't have an associated object.
Just ran into this issue ourselves, and my coworker pointed out that Rails 6 seems to include support for this now: https://github.com/rails/rails/pull/32655
*Nope, didn't solve it :( Here's a treatment of the specific issue I seem to have hit.
Think I've got it, easier than expected:
class Thing
has_many :user_thing_states
delegates :some_state_property, :to => :state, :allow_nil => true
scope :with_user_state, lambda { |user|
includes(:user_thing_states).where('user_thing_states.user_id = :user_id
OR user_thing_states.user_id IS NULL',
{:user_id => user.id}) }
def state
self.user_thing_states.first
end
end
So:
Thing.with_user_state(current_user).all
Will load all Things and each thing will have only one user_question_state accessible via state, and won't exclude Things with no state.
Answering my own question twice... bit awkward but anyway.
Rails doesn't seem to let you specify additional conditions for an includes() statement. If it did, my previous answer would work - you could put an additional condition on the includes() statement that would let the where conditions work correctly. To solve this we'd need to get includes() to use something like the following SQL (Getting the 'AND' condition is the problem):
LEFT JOIN user_thing_states as uts ON things.id = uts.thing_id AND uqs.user_id = :user_id
I'm resorting to this for now which is a bit awful.
class User
...
def subscribed_things
self.subscribed_things_with_state + self.subscribed_things_with_no_state
end
def subscribed_things_with_state
self.things.includes(:user_thing_states).by_subscribed_collections(self).all
end
def subscribed_things_with_no_state
Thing.with_no_state().by_subscribed_collections(self).all
end
end

Querying a polymorphic association

I have a polymorphic association like this -
class Image < ActiveRecord::Base
has_one :approval, :as => :approvable
end
class Page < ActiveRecord::Base
has_one :approval, :as => :approvable
end
class Site < ActiveRecord::Base
has_one :approval, :as => :approvable
end
class Approval < ActiveRecord::Base
belongs_to :approvable, :polymorphic => true
end
I need to find approvals where approval.apporvable.deleted = false
I have tried something like this -
#approvals = Approval.find(:all,
:include => [:approvable],
:conditions => [":approvable.deleted = ?", false ])
This gives "Can not eagerly load the polymorphic association :approvable" error
How can the condition be given correctly so that I get a result set with approvals who's approvable item is not deleted ?
Thanks for any help in advance
This is not possible, since all "approvables" reside in different tables. Instead you will have to fetch all approvals, and then use the normal array methods.
#approvals = Approval.all.select { |approval| !approval.approvable.deleted? }
What your asking, in terms of SQL, is projecting data from different tables for different rows in the resultset. It is not possible to my knowledge.
So you'll have to be content with:
#approvals = Approval.all.reject{|a| a.approvable.deleted? }
# I assume you have a deleted? method in all the approvables
I would recommend either of the answers already presented here (they are the same thing) but I would also recommend putting that deleted flag into the Approval model if you really care to do it all in a single query.
With a polymorphic relationship rails can use eager fetching on the polys, but you can't join to them because yet again, the relationships are not known so the query is actually multiple queried intersected.
So in the end if you REALLY need to, drop into sql and intersect all the possible joins you can do to all the types of approvables in a single query, but you will have to do lots of joining manually. (manually meaning not using rails' built-in mechanisms...)
thanks for your answers
I was pretty sure that this couldn't be done. I wanted some more confirmation
besides that I was hoping for some other soln than looping thru the result set
to avoid performance related issues later
Although for the time being both reject/select are fine but in the long run I
will have to do those sql joins manually.
Thanks again for your help!!
M

What to do when find doesn't find any records in Ruby on Rails

I am trying to accomplish the atypical library learning a language application. I have been able to create Books, People, and BookCheckOuts. I've got the relationships working pretty well but am having issues with wrapping my head around how to handle books that have never been checked out.
I've created two properties on my book class CheckedOut (returns a boolean) and LastCheckedOutTo (returns a person). I am pretty much at peace with CheckedOut and feel confident that I am using the right RoR mechanism for determining if a book is presently checked out and returning a boolean in either case. I am not as confident about LastCheckedOutTo as my implementation seems like a kludge.
Am I going about this correctly? Is there a better way?
Book Class in its Entirety
class Book < ActiveRecord::Base
has_many :book_check_outs
has_many :people, :through => :book_check_outs
def checked_out
if (book_check_outs.find(:first, :conditions => "return_date is null"))
true
else
false
end
end
def last_checked_out_to
if (book_check_outs.count > 0)
book_check_outs.find(:first,
:order => "out_date desc").person
else
Person.new()
end
end
end
Perhaps:
class Book < ActiveRecord::Base
has_many :book_loans
has_many :borrowers, :class_name => 'Person', :through => :book_loans
def loaned?
book_loans.exists?(:return_date => nil)
end
# I would be reluctant to return a new Person object
# just because it was not checked out by anyone, instead you could return nil
# OR exception out.
def current_borrower
book_loans.first(:order => "out_date desc").person
end
end
# you can use a helper to keep your presentation clean
module BookHelper
def borrower_name(book)
if borrower = book.borrower
borrower.name
else
"not checked out"
end
end
end
There are actually lots of ways to do this. Here are some ideas:
You can add order and another has_many since you really care about the return date:
has_many :book_check_outs, :order => "out_date asc"
has_many :current_book_check_outs, :conditions=>'return_date is null'
Then you get:
def checked_out?
current_book_check_outs.any?
end
def last_checked_out_to
if (book_check_outs.count > 0)
book_check_outs.last.person
else
Person.new()
end
end
But I'm a little confused about how I'd use last_checked_out_to. I think I'd prefer it to return nil if it has no last person.
You should check out named scopes, as those help build these dynamic queries modularly. They would work quite well here.
Although you're not using persons (people?) in this code, I'd rework the terminology a little bit so it reads better. book.persons doesn't really seem right for what it's telling us. What do librarians call them? book.checker_outters or something?
Regarding def checked_out i'd
rename it to checked_out? since
there is unwritten (or maybe
written) ruby convention that any
method returning true or falls
end up with question-mark.
The second method is pretty much ok,
but it won't go well for heavy-dute
websites. I'd suggest denormalizing
this part and adding
last_checked_out_to_id attribute to
books table and updating it after
each checkout process. Other way
would be
book_check_outs.last.person for
existing person and
book_check_outs.people.build for new one.
It may well be overkill for this particular example but alternatively you might want to explore the option of implementing a state machine i.e. aasm.

Resources