Что вызывает эту ошибку ActiveRecord :: ReadOnlyRecord?

203

Это следует за этим предыдущим вопросом, на который был дан ответ. Я обнаружил, что могу удалить соединение из этого запроса, поэтому теперь рабочий запрос

start_cards = DeckCard.find :all, :joins => [:card], :conditions => ["deck_cards.deck_id = ? and cards.start_card = ?", @game.deck.id, true]  

Это похоже на работу. Однако, когда я пытаюсь переместить эти карты DeckCards в другую ассоциацию, я получаю ошибку ActiveRecord :: ReadOnlyRecord.

Вот код

for player in @game.players 
  player.tableau = Tableau.new
  start_card = start_cards.pop 
  start_card.draw_pile = false
  player.tableau.deck_cards << start_card  # the error occurs on this line
end

и соответствующие модели (таблица - это карты игроков на столе)

class Player < ActiveRecord::Base
  belongs_to :game
  belongs_to :user
  has_one :hand
  has_one :tableau
end

class Tableau < ActiveRecord::Base
  belongs_to :player
  has_many :deck_cards
end  

class DeckCard < ActiveRecord::Base
  belongs_to :card
  belongs_to :deck  
end

Я делаю аналогичное действие сразу после этого кода, добавляя DeckCardsв руку игроков, и этот код работает нормально. Мне было интересно, нужна belongs_to :tableauли мне модель DeckCard, но она отлично работает для добавления в руку игрока. У меня есть tableau_idи hand_idстолбцов в таблице DeckCard.

Я посмотрел ReadOnlyRecord в rails api, и это не говорит о многом за пределами описания.

user26270
источник

Ответы:

283

Рельсы 2.3.3 и ниже

Из ActiveRecord CHANGELOG(v1.12.0, 16 октября 2005 г.) :

Введите записи только для чтения. Если вы вызываете object.readonly! тогда он помечает объект как доступный только для чтения и вызывает ReadOnlyRecord, если вы вызываете object.save. object.readonly? сообщает, доступен ли объект только для чтения. Передача: readonly => true для любого метода поиска помечает возвращаемые записи как доступные только для чтения. Опция: joins теперь подразумевает: readonly, поэтому, если вы используете эту опцию, сохранение той же записи теперь не удастся. Используйте find_by_sql, чтобы обойти.

Использование на find_by_sqlсамом деле не является альтернативой, так как возвращает необработанные данные строк / столбцов ActiveRecords. У вас есть два варианта:

  1. Принудительно установить для переменной экземпляра @readonlyзначение false в записи (взломать)
  2. Используйте :include => :cardвместо:join => :card

Рельсы 2.3.4 и выше

Большинство из вышеперечисленного больше не действует после 10 сентября 2012 года:

  • использование Record.find_by_sql является жизнеспособным вариантом
  • :readonly => trueвыводится автоматически только в том случае, если он :joinsбыл указан без явного :select или явного (или унаследованного от finder-scope) :readonlyпараметра (см. реализацию set_readonly_option!in active_record/base.rbдля Rails 2.3.4 или реализацию to_ain active_record/relation.rband custom_join_sqlin active_record/relation/query_methods.rbдля Rails 3.0.0)
  • Однако, :readonly => trueвсегда автоматически выводится в has_and_belongs_to_manyслучае , если присоединиться к таблице имеет более двух внешних ключей столбцов и :joinsбыл определен без явного :select(т.е. предоставленные пользователем :readonlyзначения игнорируются - см finding_with_ambiguous_select?в active_record/associations/has_and_belongs_to_many_association.rb.)
  • В заключении, если имеет дело с особым присоединитесь столом и has_and_belongs_to_many, то @aaronrustadответ «s применяется только штраф в Rails 2.3.4 и 3.0.0.
  • ничего не использовать , :includesесли вы хотите достичь INNER JOIN( :includesподразумевает LEFT OUTER JOIN, что менее избирательны и менее эффективно , чем INNER JOIN.)
vladr
источник
: include помогает уменьшить количество выполненных запросов, я не знал об этом; но я попытался исправить это, изменив ассоциацию Tableau / Deckcards на has_many: through, и теперь я получаю сообщение «не удалось найти ассоциацию»; Возможно, мне придется отправить еще один вопрос для этого
user26270
@codeman, да,: include сократит количество запросов и перенесет включенную таблицу в область ваших условий (своего рода неявное соединение без Rails, помечающее ваши записи как доступные только для чтения, что и происходит, как только что-то прослушивает SQL). в вашей находке, в том числе: присоединиться /: выбрать пункты IIRC
vladr
Чтобы 'has_many: a, through =>: b' работал, также должна быть объявлена ​​ассоциация B, например, has_many: b; has_many: a,: through =>: b ', я надеюсь, что это ваш случай?
vladr
6
Это могло измениться в последних выпусках, но вы можете просто добавить: readonly => false как часть атрибутов метода find.
Аарон Рустад
1
Этот ответ также применим, если у вас есть ассоциация has_and_belongs_to_many с указанным custom: join_table.
Ли
172

Или в Rails 3 вы можете использовать метод readonly (замените «...» на ваши условия):

( Deck.joins(:card) & Card.where('...') ).readonly(false)
balexand
источник
1
Хммм ... Я посмотрел оба этих Railscasts на Asciicasts, и ни один не упоминает readonlyфункцию.
Purplejacket
45

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

Аарон Рустад
источник
3
Я не верю, что это так, по крайней мере, с 2.3.4
Olly
2
Он все еще работает с Rails 3.0.10, вот пример из моего собственного кода, извлекающего область видимости, которая имеет: join Fundraiser.donatable.readonly (false)
Houen
16

select ('*'), кажется, исправляет это в Rails 3.2:

> Contact.select('*').joins(:slugs).where('slugs.slug' => 'the-slug').first.readonly?
=> false

Просто чтобы проверить, опущение select ('*') действительно производит запись только для чтения:

> Contact.joins(:slugs).where('slugs.slug' => 'the-slug').first.readonly?
=> true

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

Bronson
источник
4
То же самое в Rails 4. В качестве альтернативы вы можете сделать select(quoted_table_name + '.*')
andorov
1
Это был блестящий бронсон. Спасибо.
Поездка
Это может сработать, но сложнее, чем использоватьreadonly(false)
Кельвин
5

Вместо find_by_sql, вы можете указать: select на искателе, и все снова будет счастливым ...

start_cards = DeckCard.find :all, :select => 'deck_cards.*', :joins => [:card], :conditions => ["deck_cards.deck_id = ? and cards.start_card = ?", @game.deck.id, true]


источник
3

Чтобы деактивировать это ...

module DeactivateImplicitReadonly
  def custom_join_sql(*args)
    result = super
    @implicit_readonly = false
    result
  end
end
ActiveRecord::Relation.send :include, DeactivateImplicitReadonly
грубее
источник
3
Обезьяны-патчи хрупки - очень легко ломаются новыми версиями рельсов. Определенно не рекомендуется, если есть другие решения.
Кельвин