【问题标题】:Receiving "Attempted to update a stale object: User" exception when updating current_user before logging out在注销之前更新 current_user 时收到“尝试更新陈旧对象:用户”异常
【发布时间】:2014-10-18 18:51:16
【问题描述】:

我在我的用户模型上启用了乐观锁定,以处理我的代码库各个部分中可能出现的冲突。但是,我遇到了意想不到的冲突,我不知道如何处理它,因为我不知道是什么原因造成的。

我正在使用 Devise gem 进行身份验证,并且我正在使用 before_logout 方法来重置安全令牌...

class SessionsController < Devise::SessionsController
  after_filter :after_login, :only => :create
  before_filter :before_logout, :only => :destroy

  def after_login
    # logic to set the security token
  end

  def before_logout
    current_user.update(security_token: nil) # <--"Attempted to update a stale object: User"
  end
end

我是 Rails 的新手(才几周),所以我真的不知道在我的代码中还有什么地方可以查看,也不知道为什么这段代码会抛出异常。非常感谢任何建议或想法。

提前感谢您的智慧!

【问题讨论】:

    标签: ruby-on-rails ruby activerecord devise


    【解决方案1】:

    乐观锁定意味着您希望多个线程能够编辑同一用户记录。

    您的应用中发生的情况是对同一用户记录进行了两次(或更多)编辑,而您的应用正确地检测到了这一点,并引发了相应的错误。

    您通常会处理错误:

    • 例如,您可以打印一条消息,例如“抱歉,其他人编辑了此内容,因此您的更改未保存”。

    • 例如,您可以编写任何方式来协调编辑,例如检测一个编辑更改了此人的姓名,而另一次编辑更改了此人的电话号码,然后您可以编写一种合并这些编辑的方法二合一存档。

    一般来说,您也可以使用user.reload 来获取用户的新版本。

    我建议您在学习 Rails 时首先尝试关闭乐观锁定。乐观锁定是对速度的一个很好的优化,但不需要它来让典型的 Rails & Devise 应用程序成功运行。

    您询问了 Devise 可能具体做什么:

    • 当更新用户记录时,典型的 Rails ActiveRecord 设置会使用当前时间戳更新表字段 updated_at

    • 典型的 Devise 模块设置(例如 trackable)会使用最近的登录和/或注销时间戳更新用户记录。

    查看正在发生的事情的一种简单方法是查看lock_version 字段:

    • 当 Rails 加载记录(例如用户)时,Rails 会增加锁定版本号。

    • 当您的应用程序保存用户时,Rails ActiveRecord 会将用户对象 lock_version 编号与数据库 lock_version 编号进行比较。

    • 如果数字匹配,则保存成功;如果数字不匹配,那么 Rails 会引发过时对象异常。

    示例代码:

    def before_logout
      logger.debug current_user.lock_version
      current_user.reload      
      logger.debug current_user.lock_version
      current_user.update(security_token: nil) 
    end
    

    Devise 及其一些不同的模块将字段添加到user 表中。这对于典型的 Web 应用程序来说既快速又方便,但这些添加的字段往往会破坏缓存、锁定、持久性、序列化等。

    (顺便说一句,您可能需要考虑评估 Devise 与其他安全解决方案。我特别喜欢的一个称为 Sorcery;它提供了一个包含多种方法的工具包,您可以按照自己想要的方式进行组合。在根据我的个人经验,Sorcery 是一种很好的方法,可以准确地了解您自己的代码中的每一步都在做什么,并且您可以以您想要的方式控制缓存和锁定。)

    【讨论】:

    • 谢谢,乔尔。我很清楚乐观锁定是什么,否则我不会使用它。我不同意它主要用于速度优化并且不需要。正如您所说,它用于线程保护,我在代码的各个部分中使用它来处理两个线程正在修改同一个对象的情况,所以我不想关闭它。我的问题是什么可能导致 Devise 注销时抛出过时的对象异常,或者我可以通过哪些方法来跟踪它。它不是间歇性的,它发生在每次注销时。再次感谢。
    • (为您添加了更多信息)
    【解决方案2】:

    我也遇到过类似的问题。该错误是由于您尝试更新的对象的 lock_version 不同造成的。您可以在执行更新之前尝试在对象上调用 reload。这将确保对象是最新的 lock_version。

    【讨论】:

      【解决方案3】:

      您需要为迁移文件中的lock_version 列设置一个默认值(0)

      【讨论】:

        【解决方案4】:

        陈旧对象异常的良好行为实际上取决于您的应用程序。

        一个好的解决方案通常是重试工作:

        def before_logout
          begin
            current_user.update(security_token: nil)
          rescue ActiveRecord::StaleObjectError
            current_user.reload
            retry
          end
        end
        

        当然,这可能会在代码中的其他地方隐藏“错误”更新,但它可能会解决您的问题,直到您找到“错误”更新

        【讨论】:

          【解决方案5】:

          在这种情况下我会使用update_attribute,因为您不关心验证等。

          class SessionsController < Devise::SessionsController
            after_filter :after_login, :only => :create
            before_filter :before_logout, :only => :destroy
          
            def after_login
              # logic to set the security token
            end
          
            def before_logout
              current_user.update_attribute :security_token, nil
            end
          end
          

          http://apidock.com/rails/ActiveRecord/Persistence/update_attribute

          【讨论】:

            【解决方案6】:

            我创建了一个非常简单的模块来处理这个问题,可以重新加载以解决问题。请注意,这是一个竞争条件,您需要对保存到数据库的内容负责。

            module StaleObjHandler
              def retry
                @staled_retries ||= 5
                yield
              rescue ActiveRecord::StaleObjectError => e
                if @staled_retries.zero?
                  Rails.logger.error ' Staled object retried 5 times, raising error.'
                  raise e
                end
            
                Rails.logger.error " Staled Object Error for #{e.record} retrying..."
                e.record.reload
                @staled_retries -= 1
                retry
              end
              module_function :retry
            end
            

            那么你就可以像这样使用它了

            StaleObjHandler.retry { account.update_attribute :balance, account.balance - total }
            

            默认会尝试5次,如果失败则重新加载。

            【讨论】:

              【解决方案7】:

              看起来设计会话控制器已经销毁了会话,因此您的 current_user 要么被销毁,要么被标记为销毁。我有一段时间没有使用设计了,但是您应该检查事件的顺序是否按照您认为它们发生的顺序发生。

              尝试做 current_user.destroyed?在您的 before_logout 方法中。看起来 before_logout 是为了处理而不是处理 current_user

              【讨论】:

                猜你喜欢
                • 2016-08-11
                • 2015-04-16
                • 1970-01-01
                • 2015-11-29
                • 1970-01-01
                • 1970-01-01
                • 2015-04-24
                • 1970-01-01
                • 2018-03-23
                相关资源
                最近更新 更多