O que está causando esse erro do ActiveRecord :: ReadOnlyRecord?

203

Isto segue esta questão prévia, que foi respondido. Na verdade, descobri que era possível remover uma associação dessa consulta. Agora, a consulta de trabalho é

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

Isso parece funcionar. No entanto, quando tento mover esses DeckCards para outra associação, recebo o erro ActiveRecord :: ReadOnlyRecord.

Aqui está o 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

e os modelos relevantes (quadro são as cartas dos jogadores na 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

Estou fazendo uma ação semelhante logo após esse código, aumentando DeckCardsa mão do jogador, e esse código está funcionando bem. Gostaria de saber se eu precisava belongs_to :tableauno modelo DeckCard, mas funciona bem para adicionar à mão do jogador. Eu tenho as colunas a tableau_ide hand_idna tabela DeckCard.

Procurei ReadOnlyRecord na API do Rails, e isso não diz muito além da descrição.

user26270
fonte

Respostas:

283

Trilhos 2.3.3 e inferior

No ActiveRecord CHANGELOG(v1.12.0, 16 de outubro de 2005) :

Introduzir registros somente leitura. Se você chamar object.readonly! em seguida, marcará o objeto como somente leitura e aumentará ReadOnlyRecord se você chamar object.save. object.readonly? relata se o objeto é somente leitura. Passando: readonly => true para qualquer método localizador marcará os registros retornados como somente leitura. A opção: joins agora implica: somente leitura, portanto, se você usar esta opção, o salvamento do mesmo registro falhará. Use find_by_sql para contornar.

Usar find_by_sqlnão é realmente uma alternativa, pois retorna dados brutos de linha / coluna, não ActiveRecords. Você tem duas opções:

  1. Forçar a variável de instância @readonlya false no registro (hack)
  2. Use em :include => :cardvez de:join => :card

Trilhos 2.3.4 e superior

A maioria dos itens acima não se aplica mais a partir de 10 de setembro de 2012:

  • usando Record.find_by_sql é uma opção viável
  • :readonly => trueé inferido automaticamente apenas se tiver :joinssido especificado sem uma opção explícita :select nem explícita (ou herdada pelo escopo do localizador) :readonly(consulte a implementação do set_readonly_option!in active_record/base.rbfor Rails 2.3.4 ou a implementação do to_ain active_record/relation.rbe of custom_join_sqlinactive_record/relation/query_methods.rb para o Rails 3.0.0)
  • no entanto, :readonly => trueé sempre inferido automaticamente has_and_belongs_to_manyse a juntar-se a tabela tem mais do que as duas colunas chaves estrangeiras e :joinsfoi especificado sem um compromisso explícito :select(ou seja, fornecidos pelo usuário :readonlyvalores são ignorados - veja finding_with_ambiguous_select?emactive_record/associations/has_and_belongs_to_many_association.rb .)
  • em conclusão, a menos que lide com uma tabela de junção especial e has_and_belongs_to_many, em seguida,@aaronrustad a resposta se aplica muito bem no Rails 2.3.4 e 3.0.0.
  • se não usar :includesse você quer conseguir um INNER JOIN( :includesimplica um LEFT OUTER JOIN, que é menos seletiva e menos eficiente do que INNER JOIN.)
vladr
fonte
o: include é útil para reduzir o número de consultas feitas, eu não sabia disso; mas tentei corrigi-lo alterando a associação Tableau / Deckcards para has_many: through e agora estou recebendo uma mensagem 'não foi possível encontrar associação'; Talvez eu tenha que colocar outra pergunta para que
user26270
@codeman, sim, o: include reduzirá o número de consultas e trará a tabela incluída para o escopo de sua condição (uma espécie de associação implícita sem o Rails marcar seus registros como somente leitura, o que ocorre assim que cheira qualquer coisa SQL -ish em sua busca, incluindo: join /: select cláusulas
IIRC
Para 'has_many: a, através de>> b' funcionar, a associação B também deve ser declarada, por exemplo, 'has_many: b; has_many: a,: a =>: b ', espero que este seja o seu caso?
vladr
6
Isso pode ter sido alterado em versões recentes, mas você pode simplesmente adicionar: readonly => false como parte dos atributos do método find.
Aaron Rustad
1
Esta resposta também é aplicável se você tiver uma associação has_and_belongs_to_many com um custom: join_table especificado.
Lee
172

Ou no Rails 3 você pode usar o método readonly (substitua "..." pelas suas condições):

( Deck.joins(:card) & Card.where('...') ).readonly(false)
balexand
fonte
1
Hummm ... eu procurei os dois Railscasts no Asciicasts, e nenhum deles menciona a readonlyfunção.
Purplejacket
45

Isso pode ter mudado na versão recente do Rails, mas a maneira apropriada de resolver esse problema é adicionar : readonly => false às opções de localização.

Aaron Rustad
fonte
3
Eu não acredito que seja esse o caso, com pelo menos 2.3.4
Olly
2
Ele ainda trabalha com Rails 3.0.10, aqui está um exemplo do meu próprio código buscar um escopo que tem um: junte Fundraiser.donatable.readonly (false)
Houen
16

select ('*') parece corrigir isso no Rails 3.2:

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

Apenas para verificar, omitir select ('*') produz um registro somente leitura:

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

Não posso dizer que entendi a lógica, mas pelo menos é uma solução rápida e limpa.

Bronson
fonte
4
A mesma coisa no Rails 4. Como alternativa, você pode fazer select(quoted_table_name + '.*')
andorov 16/10
1
Isso foi brilhante Bronson. Obrigado.
Viagem
Isso pode funcionar, mas é mais complicado do que usarreadonly(false)
Kelvin
5

Em vez de find_by_sql, você pode especificar a: select no localizador e tudo ficará feliz novamente ...

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


fonte
3

Para desativá-lo ...

module DeactivateImplicitReadonly
  def custom_join_sql(*args)
    result = super
    @implicit_readonly = false
    result
  end
end
ActiveRecord::Relation.send :include, DeactivateImplicitReadonly
mais grosseiro
fonte
3
A correção de macacos é frágil - facilmente quebrada pelas novas versões de trilhos. Definitivamente desaconselhável, dado que existem outras soluções.
Kelvin