Quelle est la cause de cette erreur ActiveRecord :: ReadOnlyRecord?

203

Cela fait suite à cette question préalable, à laquelle il a été répondu. J'ai découvert que je pouvais supprimer une jointure de cette requête, alors maintenant la requête de travail est

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

Cela semble fonctionner. Cependant, lorsque j'essaie de déplacer ces DeckCards dans une autre association, j'obtiens l'erreur ActiveRecord :: ReadOnlyRecord.

Voici le code

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

et les modèles pertinents (le tableau représente les cartes des joueurs sur la table)

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

Je fais une action similaire juste après ce code, ajoutant DeckCardsà la main des joueurs, et ce code fonctionne très bien. Je me demandais si j'avais besoin belongs_to :tableaudu modèle DeckCard, mais cela fonctionne bien pour l'ajout à la main du joueur. J'ai des colonnes tableau_idet hand_iddans la table DeckCard.

J'ai recherché ReadOnlyRecord dans l'api des rails, et cela ne dit pas grand-chose au-delà de la description.

user26270
la source

Réponses:

283

Rails 2.3.3 et inférieurs

Extrait d' ActiveRecord CHANGELOG(v1.12.0, 16 octobre 2005) :

Introduisez des enregistrements en lecture seule. Si vous appelez object.readonly! il marquera alors l'objet en lecture seule et augmentera ReadOnlyRecord si vous appelez object.save. object.readonly? signale si l'objet est en lecture seule. Passing: readonly => true pour toute méthode de recherche marquera les enregistrements retournés comme étant en lecture seule. L'option: joins implique désormais: en lecture seule, donc si vous utilisez cette option, l'enregistrement du même enregistrement échouera désormais. Utilisez find_by_sql pour contourner ce problème.

L'utilisation find_by_sqln'est pas vraiment une alternative car elle renvoie des données brutes de ligne / colonne, non ActiveRecords. Vous avez deux options:

  1. Forcer la variable d'instance @readonlyà false dans l'enregistrement (hack)
  2. Utiliser :include => :cardau lieu de:join => :card

Rails 2.3.4 et supérieur

La plupart de ce qui précède n'est plus vrai, après le 10 septembre 2012:

  • l'utilisation Record.find_by_sql est une option viable
  • :readonly => truen'est automatiquement déduit que s'il a :joinsété spécifié sans option explicite :select ni explicite (ou héritée par le chercheur) :readonly(voir l'implémentation de set_readonly_option!in active_record/base.rbpour Rails 2.3.4, ou l'implémentation de to_ain active_record/relation.rbet de custom_join_sqlin active_record/relation/query_methods.rbpour Rails 3.0.0)
  • cependant, :readonly => trueest toujours automatiquement déduit has_and_belongs_to_manysi la table de jointure a plus de deux colonnes de clés étrangères et a :joinsété spécifiée sans explicite :select(c'est-à-dire que les :readonlyvaleurs fournies par l'utilisateur sont ignorées - voir finding_with_ambiguous_select?dans active_record/associations/has_and_belongs_to_many_association.rb).
  • en conclusion, sauf s'il s'agit d'une table de jointure spéciale et has_and_belongs_to_many, alors @aaronrustadla réponse s'applique très bien dans Rails 2.3.4 et 3.0.0.
  • ne pas utiliser :includessi vous souhaitez réaliser un INNER JOIN( :includesimplique un LEFT OUTER JOIN, qui est moins sélectif et moins efficace que INNER JOIN.)
vladr
la source
l'inclusion est utile pour réduire le nombre de requêtes effectuées, je n'en savais rien; mais j'ai essayé de le corriger en changeant l'association Tableau / Deckcards en has_many: through, et maintenant j'obtiens un msg 'could not find association'; Je
devrais
@codeman, oui, le: include réduira le nombre de requêtes et mettra la table incluse dans votre portée de condition (une sorte de jointure implicite sans que Rails marque vos enregistrements en lecture seule, ce qu'il fait dès qu'il renifle quoi que ce soit SQL) -ish dans votre recherche, y compris: join /: select clauses IIRC
vladr
Pour que 'has_many: a, through =>: b' fonctionne, l'association B doit également être déclarée, par exemple 'has_many: b; has_many: a,: through =>: b ', j'espère que c'est votre cas?
vladr
6
Cela a peut-être changé dans les versions récentes, mais vous pouvez simplement ajouter: readonly => false dans le cadre des attributs de la méthode find.
Aaron Rustad
1
Cette réponse est également applicable si vous avez une association has_and_belongs_to_many avec un custom: join_table spécifié.
Lee
172

Ou dans Rails 3, vous pouvez utiliser la méthode en lecture seule (remplacez "..." par vos conditions):

( Deck.joins(:card) & Card.where('...') ).readonly(false)
balexand
la source
1
Hmmm ... J'ai recherché ces deux Railscasts sur Asciicasts, et aucun ne mentionne la readonlyfonction.
Purplejacket
45

Cela a peut-être changé dans la version récente de Rails, mais la façon appropriée de résoudre ce problème consiste à ajouter : readonly => false aux options de recherche.

Aaron Rustad
la source
3
Je ne crois pas que ce soit le cas, avec 2.3.4 au moins
Olly
2
Cela fonctionne toujours avec Rails 3.0.10, voici un exemple de mon propre code récupérant une portée qui a: join Fundraiser.donatable.readonly (false)
Houen
16

select ('*') semble résoudre ce problème dans Rails 3.2:

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

Juste pour vérifier, l'omission de select ('*') produit un enregistrement en lecture seule:

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

Je ne peux pas dire que je comprends la justification, mais au moins c'est une solution rapide et propre.

bronson
la source
4
Même chose dans Rails 4. Sinon, vous pouvez le faire select(quoted_table_name + '.*')
andorov
1
C'était un brillant bronson. Je vous remercie.
Voyage le
Cela peut fonctionner, mais est plus compliqué que l'utilisationreadonly(false)
Kelvin
5

Au lieu de find_by_sql, vous pouvez spécifier a: sélectionnez sur le viseur et tout est à nouveau heureux ...

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


la source
3

Pour le désactiver ...

module DeactivateImplicitReadonly
  def custom_join_sql(*args)
    result = super
    @implicit_readonly = false
    result
  end
end
ActiveRecord::Relation.send :include, DeactivateImplicitReadonly
plus grossier
la source
3
Le patch de singe est fragile - très facilement cassé par les nouvelles versions de rails. Certainement déconseillé étant donné qu'il existe d'autres solutions.
Kelvin