什么导致了ActiveRecord::ReadOnlyRecord错误?

210

这个问题是基于之前得到回答的问题。实际上我发现可以将查询中的一个join去掉,所以现在这个有效的查询语句是:

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

这似乎能够工作。但是,当我尝试将这些DeckCards移动到另一个关联时,就会出现ActiveRecord::ReadOnlyRecord错误。

以下是代码:

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

并且相关的模型(tableau是桌面上的玩家牌)

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

在这段代码之后,我正在执行类似的操作,将DeckCards添加到玩家手牌中,这段代码运行良好。我想知道在DeckCard模型中是否需要belongs_to:tableau,但是对于添加到玩家手牌中来说这很好用。我的DeckCard表中确实有tableau_idhand_id列。

我在Rails API中查找了ReadOnlyRecord,除了描述之外,没有太多信息。

6个回答

288

Rails 2.3.3及以下版本

ActiveRecord CHANGELOG(v1.12.0,2005年10月16日) 中得知:

引入只读记录。如果调用 object.readonly!,则会将对象标记为只读并在调用 object.save 时引发 ReadOnlyRecord。object.readonly? 报告对象是否为只读。将 :readonly => true 传递给任何查找器方法都将标记返回的记录为只读。 :joins 选项现在意味着 :readonly,因此如果使用此选项,则保存相同的记录将失败。 使用 find_by_sql 来解决。

使用 find_by_sql 不是一个真正的替代方案,因为它返回原始行/列数据,而不是 ActiveRecords。你有两个选择:

  1. 强制将记录中的实例变量 @readonly 设置为 false (hack)
  2. 使用 :include => :card 替代 :join => :card

Rails 2.3.4及以上版本

大部分内容在2012年9月10日后不再适用:

  • 使用Record.find_by_sql是可行的选项。
  • :readonly => true仅在指定了:joins但没有明确指定:select或明确指定(或继承finder-scope):readonly选项时才会自动推断(请参见Rails 2.3.4中active_record/base.rb中的set_readonly_option!的实现,或Rails 3.0.0中active_record/relation.rb中的to_aactive_record/relation/query_methods.rb中的custom_join_sql的实现)。
  • 然而,如果连接表具有两个以上外键列并且没有明确指定:select(即用户提供的:readonly值被忽略),则:readonly => true始终会自动推断在has_and_belongs_to_many中的:joins中(请参见active_record/associations/has_and_belongs_to_many_association.rb中的finding_with_ambiguous_select?)。)
  • 总之,除非涉及特殊的连接表和has_and_belongs_to_many,否则@aaronrustad的答案在Rails 2.3.4和3.0.0中都适用。
  • 如果要实现INNER JOIN,请不要使用:includes:includes意味着LEFT OUTER JOIN,它比INNER JOIN选择性更低,效率更低。)

:include 在减少查询次数方面非常有帮助,我之前不知道这个功能;但是我试图通过将 Tableau/Deckcards 关联更改为 has_many: through 来解决它,现在却收到了“找不到关联”的消息;我可能需要再发一个问题来解决这个问题。 - user26270
@codeman,是的,:include将减少查询数量,并将所包含的表带入您的条件范围内(一种隐式连接方式,而Rails会在您的查找中嗅探到任何SQL-ish内容时标记您的记录为只读,包括:join / :select子句IIRC)。 - vladr
6
这可能在最近的版本中有所更改,但是您可以将:readonly => false作为查找方法属性的一部分简单地添加进去,以此来取消只读属性。 - Aaron Rustad
1
如果您使用自定义的:join_table与has_and_belongs_to_many关联,那么这个答案也适用。 - Lee
非常有帮助 - 我希望我能够多次点赞!你刚刚为我节省了一个小时左右的时间,因为我不用再查看Rails源代码和提交信息了。 - nfm
显示剩余2条评论

171

在Rails 3中,您可以使用readonly方法(将“ ...”替换为您的条件):

( Deck.joins(:card) & Card.where('...') ).readonly(false)

1
嗯...我查了一下Asciicasts上的这两个Railscasts,都没有提到readonly函数。 - Purplejacket

45

这在Rails的最近版本中可能已经改变,但解决此问题的适当方法是在查找选项中添加:readonly => false


3
我不相信这是事实,至少在2.3.4版本中。 - Olly
2
它仍然与Rails 3.0.10兼容,以下是我自己代码的示例,获取了一个具有:join的作用域 Fundraiser.donatable.readonly(false) - Houen

16

在Rails 3.2中,select('*')似乎可以解决这个问题:

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

只是为了验证,省略 select('*') 将产生一个只读记录:

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

无法说我理解其原理,但至少这是一种快速且干净的解决方法。

4
在Rails 4中是相同的事情。或者你可以使用 select(quoted_table_name + '.*') - andorov
1
那真是太棒了,Bronson。谢谢你。 - Trip
这可能有效,但比使用 readonly(false) 更复杂。 - Kelvin

5

不要使用find_by_sql,你可以在查找器上指定一个 :select,这样一切都会恢复正常...

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


3
为了停用它,您需要执行以下步骤:
module DeactivateImplicitReadonly
  def custom_join_sql(*args)
    result = super
    @implicit_readonly = false
    result
  end
end
ActiveRecord::Relation.send :include, DeactivateImplicitReadonly

3
猴子补丁很脆弱 - 在新版本的Rails中非常容易被打破。考虑到还有其他解决方案,这绝对不可取。 - Kelvin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接