如何正确地避免ActiveRecord::ReadOnlyRecord?

30
我目前正在使用Rails 2.3.9版本。我知道在查询中指定:joins选项但没有明确的:select会自动使返回的任何记录只读。我有一个需要更新记录的情况,虽然我已经阅读了不同的方法来解决它,但我想知道哪种方式是首选或"正确"的方式。
具体而言,我的情况是我有以下User模型,其中active命名范围执行与subscriptions表的连接:
class User < ActiveRecord::Base
  has_one :subscription

  named_scope :active, :conditions => { :subscriptions => { :status => 'active' } }, :joins => :subscription
end

当我调用User.active.all时,返回的用户记录都是只读的,因此,如果我在用户上调用update_attributes!,将会引发ActiveRecord::ReadOnlyRecord异常。通过阅读各种来源,似乎一个流行的解决方法是在查询中添加:readonly => false。但是,我想知道以下内容:
  • 这是安全的吗? 我理解Rails最初将其设置为只读的原因是因为根据Rails文档,“它们将具有不对应于表列的属性。” 但是,从此调用生成的SQL查询仍然使用SELECT `users`.*,这似乎是安全的,所以Rails试图在第一次防范什么? 看起来Rails应该防范的是实际上明确指定:select时的情况,这与实际行为相反,所以我是否没有正确理解自动设置:joins的只读标志的目的?
  • 这看起来像一个hack吗? 命名范围的定义关心显式设置:readonly => false似乎不合适。 我也担心命名范围与其他命名范围链接时会产生副作用。 如果我尝试在范围之外指定它(例如通过执行User.active.scoped(:readonly => false)User.scoped(:readonly => false).active),它似乎不起作用。

我读到的另一种解决方法是将:joins更改为:include。 我更好地理解了这种行为,但是是否有任何缺点(除了不必要地读取subscriptions表中的所有列)?

最后,我也可以通过调用User.find_all_by_id(User.active.map(&:id))再次使用记录ID来检索查询,但我认为这更像是一种解决方法而不是可能的解决方案,因为它会生成额外的SQL查询。 还有其他可能的解决方案吗?在这种情况下,什么是首选解决方案?我已经阅读了关于此的上一个StackOverflow问题中给出的答案,但似乎没有给出具体的指导来说明什么是正确的。
提前感谢!

你能把你的问题缩小到1或2个吗? - charlysisto
@charlysisto:我为所有的子问题道歉,但我想知道的主要问题是是否有关于正确解决 ActiveRecord::ReadOnlyRecord 问题的具体指导。其他问题主要是为了让我理解为什么一个特定的解决方案比另一个更好。 - Claw
4个回答

14

我认为在这种情况下使用:include而不是:join会更加习惯和可接受。我认为:join只用于罕见的专业情况,而:include则相对常见。

如果你不会更新所有活跃用户,那么最好添加一个额外的命名作用域或查找条件来进一步缩小你加载的用户范围,以避免不必要地加载额外的用户和订阅。例如...

User.active.some_further_limiting_scope(:with_an_argument)
  #or
User.active.find(:all, :conditions => {:etc => 'etc'})
如果你决定仍然要使用:join,并且只会更新一小部分已加载的用户,则最好在执行操作之前重新加载你想要更新的用户。例如...
readonly_users = User.active
# insert some other code that picks out a particular user to update
User.find(readonly_users[@index].id).update_attributes(:etc => 'etc')

如果你确实需要加载所有活跃用户,并且想要坚持使用:join,并且你很可能会更新大部分或所有用户,那么重新加载它们的想法,使用ID数组可能是最好的选择。

#no need to do find_all_by_id in this case. A simple find() is sufficient.
writable_users_without_subscriptions = User.find(Users.active.map(&:id))

我希望这可以帮到你。我很好奇你选择了哪个选项,或者你是否找到了另一个更适合你情况的解决方案。


谢谢您的回复!我已经给您点了赞(我看到其他人为这个问题设置了悬赏,您已经得到了),但自从发布这个问题以来,我还没有真正看到满意的答案。我觉得我仍然缺乏足够的理解每种方法的原因。 - Claw
2
根据我所尝试的内容,我确实需要加载每个活跃用户,并且迄今为止还没有听到使用 :join 的好理由,特别是因为我的用例更符合使用 :join 的情况(如 此 Railscast 中所解释的)。因此,我目前选择使用重新获取记录 ID 的选项来再次查询数据,但我仍然认为这只是一个临时解决方法。 - Claw

3

我认为最好的解决方案是使用已有的.join并进行单独的find()操作。

:include和:join的一个重要区别在于,前者使用外连接(outer join),而后者使用内连接(inner join)!因此,使用:include可能会解决只读问题,但结果可能是错误的!


2

我遇到了同样的问题,但不太想使用:readonly => false这种方式。

因此,我使用了显式选择,即:select => 'users.*',感觉这种方式更加规范。

你可以考虑采用以下方法:

class User < ActiveRecord::Base
  has_one :subscription

  named_scope :active, :select => 'users.*', :conditions => { :subscriptions => { :status => 'active' } }, :joins => :subscription
end

0
关于您的子问题:那么我是否没有正确理解在 :joins 上自动设置只读标志的目的? 我认为答案是:使用 joins 查询,您将获得一个包含 User + Subscription 表属性的单个记录。如果您尝试更新 Subscription 表中的一个属性(比如 "subscription_num"),而不是 User 表,则更新语句无法找到 subscription_num 并会崩溃。因此,默认情况下,连接作用域是只读的,以防止发生这种情况。
参考: 1) http://blog.ethanvizitei.com/2009/05/joins-and-namedscopes-in-activerecord.html

1
生成的 SQL 查询仅选择用户属性(SELECT `users`.*),而订阅表属性无法从返回的记录中访问。这似乎与您的源代码所述相矛盾;也许行为已经发生了变化? - Claw

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