正确的方法来阻止ActiveRecord :: ReadOnlyRecord?

时间:2011-10-05 20:03:35

标签: ruby-on-rails activerecord

我目前正在使用Rails 2.3.9。我知道在没有显式:joins的情况下在查询中指定:select选项会自动生成任何只读的记录。我有一个情况,我想更新记录,虽然我已经阅读了不同的方法,我想知道哪种方式是首选或“正确”的方式。

具体来说,我的情况是我有以下User模型,其active命名范围与subscriptions表执行JOIN:

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 documentation,“它们将具有不属于对应于表的列。“但是,从此调用生成的SQL查询仍然使用SELECT `users`.*,这似乎是安全的,那么Rails首先想要防范的是什么?看来Rails应该防止实际明确指定:select的情况,这与实际行为相反,所以我没有正确理解在{{1}上自动设置只读标志的目的}吗
  • 这看起来像是黑客吗?命名范围的定义应该关注显式设置:joins似乎不合适。如果命名范围与其他命名范围链接,我也害怕副作用。如果我尝试在范围之外指定它(例如,通过执行:readonly => falseUser.active.scoped(:readonly => false)),它似乎无效。

我读过的另一种解决方法是将User.scoped(:readonly => false).active更改为:joins。我理解这种行为更好,但对此有任何不利之处(除了不必要地读取:include表中的所有列)?

最后,我还可以通过调用subscriptions使用记录ID再次检索查询,但我发现这更像是一种解决方法而不是可能的解决方案,因为它会生成额外的SQL查询。

还有其他可能的解决方案吗?在这种情况下,首选解决方案是什么?我已经阅读了previous StackOverflow question about this中给出的答案,但似乎没有给出具体的指导意见。

提前致谢!

4 个答案:

答案 0 :(得分: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))

我希望有所帮助。我很好奇您选择了哪个选项,或者您是否找到了更适合您的方案的其他解决方案。

答案 1 :(得分:3)

我认为最好的解决方案是使用.join,然后单独执行find()

使用的一个重要区别:include是它使用外连接而:join使用内连接!所以使用:include可以解决只读问题,但结果可能是错误的!

答案 2 :(得分:2)

我遇到了同样的问题,并且不习惯使用:readonly => false

结果我做了一个明确的选择,即:select => 'users.*',觉得这似乎不是一个黑客。

您可以考虑执行以下操作:

class User < ActiveRecord::Base
  has_one :subscription

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

答案 3 :(得分:0)

关于你的子问题:所以我没有正确理解自动设置只读标志的目的:加入?

我相信答案是:使用联接查询,您将获得具有User + Subscription表属性的单个记录。如果您尝试更新Subscription表中的某个属性(比如“subscription_num”)而不是User表,则对User表的update语句将无法找到subscription_num并且会崩溃。因此,默认情况下,连接范围是只读的,以防止发生这种情况。

参考: 1)http://blog.ethanvizitei.com/2009/05/joins-and-namedscopes-in-activerecord.html