Правильный способ предотвращения ActiveRecord:: ReadOnlyRecord?

В настоящее время я использую Rails 2.3.9. Я понимаю, что указание опции :joins в запросе без явного :select автоматически делает любые записи, которые возвращаются только для чтения. У меня есть ситуация, когда я хотел бы обновить записи, и пока я читал о различных способах подхода к ней, мне было интересно, какой путь является предпочтительным или "правильным".

В частности, моя ситуация заключается в том, что у меня есть следующая модель User с областью имен с именем active, которая выполняет JOIN с таблицей subscriptions:

class User < ActiveRecord::Base
  has_one :subscription

  named_scope :active, :conditions => { :subscriptions => { :status => 'active' } }, :joins => :subscription
end

Когда я вызываю User.active.all, возвращаемые пользователем записи доступны только для чтения, поэтому, если, например, я вызываю update_attributes! для пользователя, ActiveRecord::ReadOnlyRecord будет поднят.

С помощью чтения различных источников кажется популярным способом обойти это, добавив :readonly => false к запросу. Однако мне было интересно следующее:

  • Является ли это безопасным? Я понимаю, почему Rails устанавливает его только для чтения, потому что, согласно Документация Rails, "они будут иметь атрибуты, которые не соответствуют столбцам таблиц". Однако SQL-запрос, который генерируется из этого вызова, использует SELECT `users`.* в любом случае, что кажется безопасным, , так что Rails пытается защитить в первую очередь? Похоже, что Rails должен быть защищая от случая, когда :select действительно явно указано, что является обратным действию, , так я не правильно понимаю цель автоматической установки флага только для чтения на :joins?
  • Это похоже на взлом? Не похоже, чтобы определение именованной области должно заботиться о явной настройке :readonly => false. Я также боюсь побочных эффектов, если названная область скопирована с другими названными областями. Если я попытаюсь указать его вне области действия (например, выполнив User.active.scoped(:readonly => false) или User.scoped(:readonly => false).active), он не работает.

Другим способом, который я прочитал, чтобы обойти это, является изменение :joins на :include. Я понимаю поведение этого лучше, но существуют ли какие-либо недостатки (кроме ненужного чтения всех столбцов в таблице subscriptions)?

Наконец, я мог бы снова получить запрос с помощью идентификаторов записей, вызвав User.find_all_by_id(User.active.map(&:id)), но я считаю, что это скорее обходное решение, чем возможное решение, поскольку оно генерирует дополнительный SQL-запрос.

Есть ли другие возможные решения? Каким будет предпочтительное решение в этой ситуации? Я прочитал ответ, указанный в qaru.site/info/32676/..., но он не кажется дать конкретное руководство относительно того, что будет считаться правильным.

Спасибо заранее!

Ответ 1

Я считаю, что в этом случае было бы общепринято и приемлемо использовать :include вместо :join. Я думаю, что :join используется только в редких специализированных обстоятельствах, тогда как :include довольно распространен.

Если вы не собираетесь обновлять всех активных пользователей, то, вероятно, разумно добавить дополнительную именованную область или найти условие для дальнейшего сужения пользователей, которые вы загружаете, чтобы вы не загружали дополнительных пользователей и подписки без необходимости. Например...

User.active.some_further_limiting_scope(:with_an_argument)
  #or
User.active.find(:all, :conditions => {:etc => 'etc'})

Если вы решите, что хотите по-прежнему использовать :join, и только собираетесь обновить небольшой процент загруженных пользователей, то, вероятно, лучше всего перезагрузить пользователя, которого вы хотите обновить, прежде чем это сделать. Такие, как...

readonly_users = User.active
# insert some other code that picks out a particular user to update
User.find(readonly_users[@index].id).update_attributes(:etc => 'etc')

Если вам действительно нужно загрузить всех активных пользователей, и вы хотите придерживаться :join, и, скорее всего, вы будете обновлять большинство или всех пользователей, тогда ваша идея перезагрузить их с помощью массива ID вероятно, ваш лучший выбор.

#no need to do find_all_by_id in this case. A simple find() is sufficient.
writable_users_without_subscriptions = User.find(Users.active.map(&:id))

Надеюсь, это поможет. Мне любопытно, с каким вариантом вы идете, или если вы нашли другое решение более подходящим для вашего сценария.

Ответ 2

Я думаю, лучшее решение - использовать .join, как вы уже сделали, и сделать отдельный find()

Одно существенное отличие использования: include заключается в том, что он использует внешнее соединение, а: join использует внутреннее соединение! Поэтому использование: include может решить проблему только для чтения, но результат может быть неправильным!

Ответ 3

Я столкнулся с этой проблемой и не был удобен с помощью :readonly => false

В результате я сделал явный выбор, а именно :select => 'users.*', и почувствовал, что это похоже на хук.

Вы можете подумать о следующем:

class User < ActiveRecord::Base
  has_one :subscription

  named_scope :active, :select => 'users.*', :conditions => { :subscriptions => { :status => 'active' } }, :joins => :subscription
end

Ответ 4

Что касается вашего подзапроса: , я не правильно понимаю цель автоматической установки флага только для чтения: joins?

Я считаю, что ответ таков: с запросом на объединение вы возвращаете одну запись с атрибутами таблицы User + Subscription. Если вы попытались обновить один из атрибутов (скажем, "subscription_num" ) в таблице "Абонент" вместо таблицы "Пользователь", выражение об обновлении в таблице "Пользователь" не сможет найти subscription_num и произойдет сбой. Таким образом, объединительные области по умолчанию доступны только для чтения, чтобы это не происходило.

Ссылка: 1) http://blog.ethanvizitei.com/2009/05/joins-and-namedscopes-in-activerecord.html