¿Qué está causando este error ActiveRecord :: ReadOnlyRecord?

203

Esto sigue a esta pregunta anterior, que fue respondida. De hecho, descubrí que podía eliminar una combinación de esa consulta, por lo que ahora la consulta de trabajo es

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

Esto parece funcionar. Sin embargo, cuando intento mover estas DeckCards a otra asociación, aparece el error ActiveRecord :: ReadOnlyRecord.

Aquí está el código

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

y los modelos relevantes (el cuadro son las cartas de los jugadores en la mesa)

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

Estoy haciendo una acción similar justo después de este código, agregando DeckCardsa la mano de los jugadores, y ese código funciona bien. Me preguntaba si lo necesitaba belongs_to :tableauen el modelo DeckCard, pero funciona bien para agregarlo a la mano del jugador. Tengo una tableau_idy hand_idcolumnas en la tabla DeckCard.

Busqué ReadOnlyRecord en la API de rieles, y no dice mucho más allá de la descripción.

usuario26270
fuente

Respuestas:

283

Carriles 2.3.3 y más bajos

Desde ActiveRecord CHANGELOG(v1.12.0, 16 de octubre de 2005) :

Introducir registros de solo lectura. Si llamas a object.readonly! luego marcará el objeto como de solo lectura y elevará ReadOnlyRecord si llama a object.save. object.readonly? informa si el objeto es de solo lectura. Paso: readonly => true para cualquier método de búsqueda marcará los registros devueltos como de solo lectura. La opción: une ahora implica: solo lectura, por lo que si usa esta opción, guardar el mismo registro ahora fallará. Use find_by_sql para evitarlo.

El uso find_by_sqlno es realmente una alternativa, ya que devuelve datos sin procesar de fila / columna, no ActiveRecords. Tienes dos opciones:

  1. Forzar la variable de instancia @readonlya falso en el registro (piratear)
  2. Usar en :include => :cardlugar de:join => :card

Carriles 2.3.4 y superiores

La mayoría de lo anterior ya no es cierto, después del 10 de septiembre de 2012:

  • usar Record.find_by_sql es una opción viable
  • :readonly => truese infiere automáticamente solo si :joinsse especificó sin una opción explícita :select ni explícita (o buscador-alcance-heredado) :readonly(vea la implementación de set_readonly_option!in active_record/base.rbpara Rails 2.3.4, o la implementación de to_ain active_record/relation.rby de custom_join_sqlin active_record/relation/query_methods.rbpara Rails 3.0.0)
  • sin embargo, :readonly => truesiempre se infiere automáticamente has_and_belongs_to_manysi la tabla de unión tiene más de las dos columnas de claves foráneas y :joinsse especificó sin un valor explícito :select(es decir, los :readonlyvalores proporcionados por el usuario se ignoran, ver finding_with_ambiguous_select?en active_record/associations/has_and_belongs_to_many_association.rb).
  • en conclusión, a menos que se trate de una tabla de unión especial y has_and_belongs_to_many, entonces @aaronrustad, la respuesta se aplica muy bien en Rails 2.3.4 y 3.0.0.
  • no lo use :includessi desea lograr un INNER JOIN( :includesimplica un LEFT OUTER JOIN, que es menos selectivo y menos eficiente que INNER JOIN).
vladr
fuente
the: include es útil para reducir el número de consultas realizadas, no sabía sobre eso; pero intenté solucionarlo cambiando la asociación de Tableau / Deckcards a has_many: through, y ahora recibo un mensaje "no se pudo encontrar la asociación"; Puede que tenga que publicar otra pregunta para eso
user26270
@codeman, sí, el: include reducirá el número de consultas y traerá la tabla incluida al alcance de su condición (una especie de unión implícita sin Rails que marca sus registros como de solo lectura, lo que hace tan pronto como detecta cualquier cosa SQL) -ish en su búsqueda, que incluye: join /: select cláusulas IIRC
vladr
Para que 'has_many: a, through =>: b' funcione, la asociación B también debe declararse, por ejemplo, 'has_many: b; has_many: a,: through =>: b ', espero que este sea tu caso?
vladr
66
Esto podría haber cambiado en versiones recientes, pero simplemente puede agregar: readonly => false como parte de los atributos del método find.
Aaron Rustad
1
Esta respuesta también es aplicable si tiene una asociación has_and_belongs_to_many con una costumbre: join_table especificado.
Lee
172

O en Rails 3 puede usar el método de solo lectura (reemplace "..." con sus condiciones):

( Deck.joins(:card) & Card.where('...') ).readonly(false)
balexand
fuente
1
Hmmm ... busqué ambos Railscasts en Asciicasts, y ninguno menciona la readonlyfunción.
Purplejacket
45

Esto podría haber cambiado en la versión reciente de Rails, pero la forma adecuada de resolver este problema es agregar : readonly => false a las opciones de búsqueda.

Aaron Rustad
fuente
3
No creo que este sea el caso, con 2.3.4 al menos
Olly
2
Todavía funciona con Rails 3.0.10, aquí hay un ejemplo de mi propio código que busca un ámbito que tiene un: join Fundraiser.donatable.readonly (false)
Houen
16

select ('*') parece corregir esto en Rails 3.2:

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

Solo para verificar, omitir select ('*') produce un registro de solo lectura:

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

No puedo decir que entiendo la lógica, pero al menos es una solución rápida y limpia.

Bronson
fuente
44
Lo mismo en Rails 4. Alternativamente, puedes hacerlo select(quoted_table_name + '.*')
andorov
1
Eso fue brillante Bronson. Gracias.
Viaje el
Esto puede funcionar, pero es más complicado que usarloreadonly(false)
Kelvin
5

En lugar de find_by_sql, puede especificar a: select en el buscador y todo vuelve a ser feliz ...

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


fuente
3

Para desactivarlo ...

module DeactivateImplicitReadonly
  def custom_join_sql(*args)
    result = super
    @implicit_readonly = false
    result
  end
end
ActiveRecord::Relation.send :include, DeactivateImplicitReadonly
más asqueroso
fuente
3
El parcheado de monos es frágil: las nuevas versiones de rieles lo rompen muy fácilmente. Definitivamente desaconsejable dado que hay otras soluciones.
Kelvin