209

This follows this prior question, which was answered. I actually discovered I could remove a join from that query, so now the working query is

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

This appears to work. However, when I try to move these DeckCards into another association, I get the ActiveRecord::ReadOnlyRecord error.

Here's the 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

and the relevant Models (tableau are the players cards on the 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

I am doing a similar action just after this code, adding DeckCards to the players hand, and that code is working fine. I wondered if I needed belongs_to :tableau in the DeckCard Model, but it works fine for the adding to player's hand. I do have a tableau_id and hand_id columns in the DeckCard table.

I looked up ReadOnlyRecord in the rails api, and it doesn't say much beyond the description.

Community
  • 1
  • 1
user26270
  • 6,904
  • 13
  • 62
  • 94

6 Answers6

287

Rails 2.3.3 and lower

From the ActiveRecord CHANGELOG(v1.12.0, October 16th, 2005):

Introduce read-only records. If you call object.readonly! then it will mark the object as read-only and raise ReadOnlyRecord if you call object.save. object.readonly? reports whether the object is read-only. Passing :readonly => true to any finder method will mark returned records as read-only. The :joins option now implies :readonly, so if you use this option, saving the same record will now fail. Use find_by_sql to work around.

Using find_by_sql is not really an alternative as it returns raw row/column data, not ActiveRecords. You have two options:

  1. Force the instance variable @readonly to false in the record (hack)
  2. Use :include => :card instead of :join => :card

Rails 2.3.4 and above

Most of the above no longer holds true, after September 10 2012:

  • using Record.find_by_sql is a viable option
  • :readonly => true is automatically inferred only if :joins was specified without an explicit :select nor an explicit (or finder-scope-inherited) :readonly option (see the implementation of set_readonly_option! in active_record/base.rb for Rails 2.3.4, or the implementation of to_a in active_record/relation.rb and of custom_join_sql in active_record/relation/query_methods.rb for Rails 3.0.0)
  • however, :readonly => true is always automatically inferred in has_and_belongs_to_many if the join table has more than the two foreign keys columns and :joins was specified without an explicit :select (i.e. user-supplied :readonly values are ignored -- see finding_with_ambiguous_select? in active_record/associations/has_and_belongs_to_many_association.rb.)
  • in conclusion, unless dealing with a special join table and has_and_belongs_to_many, then @aaronrustad's answer applies just fine in Rails 2.3.4 and 3.0.0.
  • do not use :includes if you want to achieve an INNER JOIN (:includes implies a LEFT OUTER JOIN, which is less selective and less efficient than INNER JOIN.)
morhook
  • 685
  • 7
  • 19
vladr
  • 65,483
  • 18
  • 129
  • 130
  • the :include is helpful in reducing the # of queries done, I didn't know about that; but I tried to fix it by changing the Tableau/Deckcards association to a has_many: through, and now I'm getting a 'could not find association' msg; I may have to post another question for that – user26270 Mar 12 '09 at 19:02
  • @codeman, yes, the :include will reduce the number of queries *and* will bring the included table into your condition scope (a sort of implicit join without Rails marking your records as read-only, which it does as soon as it sniffs anything SQL-ish in your find, including :join/:select clauses IIRC – vladr Mar 12 '09 at 19:27
  • For 'has_many :a, through => :b' to work, the B association must be declared as well, e.g. 'has_many :b; has_many :a, :through => :b', I hope this is your case? – vladr Mar 12 '09 at 19:29
  • yes, I didn't have the 'has_many :b' association; once I got that it all worked great - Thanks! – user26270 Mar 12 '09 at 20:14
  • 6
    This might have changed in recent releases, but you can simply add :readonly => false as part of the find method attributes. – Aaron Rustad Jun 07 '09 at 00:31
  • 1
    This answer is also applicable if you have a has_and_belongs_to_many association with a custom :join_table specified. – Lee Jun 20 '10 at 17:18
  • So helpful - I wish I could mod up more than once! You just saved me an hour or so of going through rails source code and commit messages – nfm Mar 09 '11 at 03:23
171

Or in Rails 3 you can use the readonly method (replace "..." with your conditions):

( Deck.joins(:card) & Card.where('...') ).readonly(false)
balexand
  • 9,549
  • 7
  • 41
  • 36
45

This might have changed in recent release of Rails, but the appropriate way to solve this problem is to add :readonly => false to the find options.

Aaron Rustad
  • 2,016
  • 17
  • 25
  • 3
    I don't believe this is the case, with 2.3.4 at least – Olly Jan 15 '10 at 09:55
  • 2
    It still works with Rails 3.0.10, here's an example from my own code fetching a scope that has a :join Fundraiser.donatable.readonly(false) – Houen Sep 20 '11 at 11:12
16

select('*') seems to fix this in Rails 3.2:

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

Just to verify, omitting select('*') does produce a readonly record:

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

Can't say I understand the rationale but at least it's a quick and clean workaround.

bronson
  • 5,612
  • 3
  • 31
  • 18
5

Instead of find_by_sql, you can specify a :select on the finder and everything's happy again...

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

3

To deactivate it...

module DeactivateImplicitReadonly
  def custom_join_sql(*args)
    result = super
    @implicit_readonly = false
    result
  end
end
ActiveRecord::Relation.send :include, DeactivateImplicitReadonly
grosser
  • 14,707
  • 7
  • 57
  • 61
  • 3
    Monkey-patching is fragile - very easily broken by new rails versions. Definitely inadvisable given there are other solutions. – Kelvin Jun 22 '15 at 22:35