default_scope breaks (update|delete|destroy)_all i

2020-07-06 06:43发布

问题:

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.

回答1:

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.)



回答2:

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.



回答3:

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


回答4:

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.