default_scope breaks (update|delete|destroy)_all in some cases

扶醉桌前 提交于 2019-12-04 02:40:41

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!