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.