Co powoduje ten błąd ActiveRecord :: ReadOnlyRecord?

203

Wynika to z poprzedniego pytania, na które udzielono odpowiedzi. Odkryłem, że mogę usunąć złączenie z tego zapytania, więc teraz działa zapytanie

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

To wydaje się działać. Jednak gdy próbuję przenieść te karty DeckCards do innego skojarzenia, pojawia się błąd ActiveRecord :: ReadOnlyRecord.

Oto kod

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

oraz odpowiednie Modele (tableau to karty graczy na stole)

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

Wykonuję podobną akcję zaraz po tym kodzie, dodając DeckCardsdo ręki graczy, a ten kod działa dobrze. Zastanawiałem się, czy potrzebowałem belongs_to :tableauw DeckCard Model, ale działa dobrze w przypadku dodawania do ręki gracza. Mam kolumny tableau_idi hand_idw tabeli DeckCard.

Spojrzałem na ReadOnlyRecord w interfejsie API szyn i niewiele mówi poza opisem.

użytkownik26270
źródło

Odpowiedzi:

283

Szyny 2.3.3 i niższe

Z ActiveRecord CHANGELOG(v1.12.0, 16 października 2005) :

Wprowadź rekordy tylko do odczytu. Jeśli wywołasz object.readonly! następnie oznaczy obiekt jako tylko do odczytu i podniesie ReadOnlyRecord, jeśli wywołasz object.save. object.readonly? informuje, czy obiekt jest tylko do odczytu. Przekazanie: readonly => true do dowolnej metody findera spowoduje oznaczenie zwróconych rekordów jako tylko do odczytu. Opcja: dołącza teraz oznacza: tylko do odczytu, więc jeśli użyjesz tej opcji, zapisanie tego samego rekordu nie powiedzie się. Do obejścia użyj find_by_sql.

Używanie find_by_sqlnie jest tak naprawdę alternatywą, ponieważ zwraca surowe dane wierszy / kolumn, a nie ActiveRecords. Masz dwie opcje:

  1. Zmusza zmienną instancji @readonlydo fałszu w rekordzie (włamanie)
  2. Użyj :include => :cardzamiast:join => :card

Szyny 2.3.4 i nowsze

Większość powyższych nie jest prawdą po 10 września 2012 r .:

  • używanie Record.find_by_sql jest opłacalną opcją
  • :readonly => truejest automatycznie wnioskować jedynie , jeżeli :joinszostała określona bez wyraźnego :select ani wyraźne (lub Finder-zakres-dziedziczone) :readonlyopcja (patrz realizację set_readonly_option!w active_record/base.rbRails 2.3.4, lub realizację to_aw active_record/relation.rbI custom_join_sqlw active_record/relation/query_methods.rbdla Rails 3.0.0)
  • jest jednak :readonly => truezawsze automatycznie wywnioskowane, has_and_belongs_to_manyjeśli tabela łączenia ma więcej niż dwie kolumny kluczy obcych i :joinszostała określona bez wyraźnego :select(tj. :readonlywartości podane przez użytkownika są ignorowane - patrz finding_with_ambiguous_select?w active_record/associations/has_and_belongs_to_many_association.rb.)
  • Podsumowując, chyba że mamy do czynienia ze specjalną tabelą złączeń has_and_belongs_to_many, a następnie @aaronrustadodpowiedź jest odpowiednia w Railsach 2.3.4 i 3.0.0.
  • czy nie skorzystać :includes, jeśli chcesz, aby osiągnąć INNER JOIN( :includesimplikuje LEFT OUTER JOIN, który jest mniej selektywny i mniej wydajny niż INNER JOIN).
Vladr
źródło
the: include pomaga zmniejszyć liczbę wykonanych zapytań, nie wiedziałem o tym; ale próbowałem to naprawić, zmieniając powiązanie Tableau / Deckcards na has_many: through, a teraz otrzymuję komunikat „nie można znaleźć powiązania” msg; Być może będę musiał zadać kolejne pytanie
user26270,
@codeman, tak, the: include zmniejszy liczbę zapytań i wprowadzi dołączoną tabelę do zakresu warunku (rodzaj niejawnego łączenia bez Railsów oznaczającego twoje rekordy jako tylko do odczytu, co robi, gdy tylko wącha cokolwiek SQL -ish w swoim znalezisku, w tym: dołącz /: wybierz klauzule IIRC
vladr
Aby „has_many: a, through =>: b” działało, należy również zadeklarować powiązanie B, np. „Has_many: b; has_many: a,: through =>: b ', mam nadzieję, że to twoja sprawa?
vladr
6
Mogło się to zmienić w ostatnich wydaniach, ale możesz po prostu dodać: readonly => false jako część atrybutów metody find.
Aaron Rustad
1
Ta odpowiedź ma również zastosowanie, jeśli masz powiązanie has_and_belongs_to_many ze zdefiniowanym niestandardowym: tabela_łączenia.
Lee,
172

Lub w Rails 3 możesz użyć metody tylko do odczytu (zamień „...” na twoje warunki):

( Deck.joins(:card) & Card.where('...') ).readonly(false)
Balexand
źródło
1
Hmmm ... Sprawdziłem oba te Railscasty na Asciicastach i żadne z nich nie wspomina o tej readonlyfunkcji.
Purplejacket
45

Mogło się to zmienić w najnowszej wersji Railsów, ale właściwym sposobem rozwiązania tego problemu jest dodanie : readonly => false do opcji find.

Aaron Rustad
źródło
3
Nie sądzę, aby tak było, przynajmniej w wersji 2.3.4
Olly
2
Nadal działa z Rails 3.0.10, oto przykład z mojego własnego kodu pobierającego zakres, który ma: dołącz do Fundraiser.donatable.readonly (false)
Houen
16

wydaje się, że select ('*') rozwiązuje to problem w Rails 3.2:

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

Aby to zweryfikować, pominięcie select („*”) powoduje utworzenie rekordu tylko do odczytu:

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

Nie mogę powiedzieć, że rozumiem uzasadnienie, ale przynajmniej jest to szybkie i czyste obejście.

bronson
źródło
4
To samo w Rails 4. Alternatywnie możesz to zrobić select(quoted_table_name + '.*')
andorov
1
To był genialny bronson. Dziękuję Ci.
Wycieczka
Może to działać, ale jest bardziej skomplikowane niż używaniereadonly(false)
Kelvin
5

Zamiast find_by_sql możesz określić: wybierz w wyszukiwarce i wszystko znów będzie szczęśliwe ...

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


źródło
3

Aby go wyłączyć ...

module DeactivateImplicitReadonly
  def custom_join_sql(*args)
    result = super
    @implicit_readonly = false
    result
  end
end
ActiveRecord::Relation.send :include, DeactivateImplicitReadonly
grosser
źródło
3
Patchowanie małp jest kruche - bardzo łatwe do złamania przez nowe wersje szyn. Zdecydowanie niewskazane, ponieważ istnieją inne rozwiązania.
Kelvin