default_scope breaks (update|delete|destroy)_all в некоторых случаях
Я считаю, что это ошибка в Rails 3. Я надеюсь, что кто-то здесь может направить меня в правильном направлении. Код, размещенный ниже, предназначен исключительно для иллюстрации этой проблемы. Надеюсь, это не смущает проблему.
Учитывая, что у меня есть модель Post и модель Comment. Post has_many Комментарии и комментарий принадлежит_ к сообщению.
С установленным по умолчанию параметром default_scope для модели Post, определяющим отношения joins() и where(). В этом случае, где () зависит от joins().
Обычно сообщения не будут зависеть от комментариев. Опять же, я просто хочу привести простой пример. Это может быть любой случай, когда where() зависит от 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
Выполнение следующей команды:
Post.update_all(:title => Time.now)
Создает следующий запрос и в конечном итоге выдает ActiveRecord::StatementInvalid:
UPDATE `posts` SET `title` = '2010-10-15 15:59:27' WHERE (comments.id < 999)
Опять же, update_all, delete_all, destroy_all ведут себя одинаково. Я обнаружил такое поведение, когда мое приложение жаловалось при попытке обновить counter_cache. Который в конечном итоге приводит к update_all.
4 ответа
У меня была и эта проблема, но нам действительно нужно было иметь возможность использовать update_all
со сложными условиями в default_scope
(например, без стандартной области видимости невозможна загрузка, а вставка именованной области буквально повсюду не доставляет никакого удовольствия). Я открыл запрос на удаление с моим исправлением:
https://github.com/rails/rails/pull/8449
Для delete_all я поднял ошибку, если есть условие соединения, чтобы сделать более очевидным, что вы должны сделать (вместо того, чтобы просто бросить условие соединения и запустить delete_all для всего, вы получите ошибку).
Не уверен, что ребята с рельсов сделают с моим запросом на тягу, но подумал, что это актуально для этого обсуждения. (Кроме того, если вам нужно исправить эту ошибку, вы можете попробовать мою ветку и оставить комментарий к запросу на извлечение.)
Если у вас есть
class Topic < ActiveRecord::Base
default_scope :conditions => "forums.preferences > 1", :include => [:forum]
end
и вы делаете
Topic.update_all(...)
это потерпит неудачу с
Mysql::Error: Unknown column 'forums.preferences' in 'where clause'
Обходной путь для этого:
Topic.send(:with_exclusive_scope) { Topic.update_all(...) }
Вы можете исправить это, используя этот код (и требуя его в environment.rb или где-нибудь еще)
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
конец
Тогда только вы update_all! или delete_all! когда у него есть область по умолчанию.
Вы также можете сделать это на уровне класса, не создавая новые методы, например:
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
Я не думаю, что я бы назвал это ошибкой. Поведение кажется мне достаточно логичным, хотя и не сразу очевидным. Но я разработал решение SQL, которое, кажется, работает хорошо. Используя ваш пример, это будет:
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
На самом деле я использую рефлексию, чтобы сделать ее более надежной, но вышесказанное делает идею идеальной. Перемещение логики WHERE в JOIN гарантирует, что она не будет применяться в неподходящих местах. :readonly
вариант заключается в противодействии стандартному поведению Rails по созданию joins
объекты только для чтения.
Кроме того, я знаю, что некоторые люди высмеивают использование default_scope
, Но для мультитенантных приложений это идеально подходит.