【问题标题】:What is best strategy to handle exceptions & errors in Rails?在 Rails 中处理异常和错误的最佳策略是什么?
【发布时间】:2010-04-09 14:40:31
【问题描述】:

我想知道人们是否会分享他们处理异常和错误的最佳实践/策略。现在我不是在问什么时候抛出异常(这里已经彻底回答了:SO: When to throw an Exception)。而且我没有在我的应用程序流程中使用它——但总是有合法的例外情况发生。例如,最流行的是 ActiveRecord::RecordNotFound。处理它的最佳方法是什么? DRY 方式?

现在我正在我的控制器中进行大量检查,所以如果Post.find(5) 返回 Nil - 我会检查并抛出一个 flash 消息。然而,虽然这非常精细 - 从某种意义上说,我需要在每个控制器中检查类似的异常,但它们中的大多数本质上是相同的,并且与未找到记录或未找到相关记录有关 - 例如因为Post.find(5) 未找到,或者如果您尝试显示与不存在的帖子相关的 cmets,则会引发异常(类似于Post.find(5).comments[0].created_at

我知道您可以在 ApplicationController 中执行类似的操作,并稍后在特定控制器/方法中覆盖它以获得更精细的支持,但是这样做是否合适?

class ApplicationController < ActionController::Base
    rescue_from ActiveRecord::RecordInvalid do |exception|
        render :action => (exception.record.new_record? ? :new : :edit)
    end
end

如果找不到Post.find(5),这也可以工作,但Post.find(5).comments[0].created_at 呢?我的意思是,如果帖子存在但没有cmets,我不能抛出一个完整的异常,对吧?

总结到目前为止,我使用 if/else/unless 或 case/when 进行了大量手动检查(我承认偶尔开始/救援)并检查 nil?还是空的?等,但似乎必须有更好的方法。

回复:

@米兰: 嗨米兰 感谢您的回复-我同意您所说的,并且我认为我误用了例外一词。我的意思是,我现在做了很多事情,比如:

if Post.exists?(params[:post_id])
    @p = Post.find(params[:post_id])
else
    flash[:error] = " Can't find Blog Post"
end

我做了很多这种“异常处理”,我尽量避免使用开始/救援。但在我看来,这是一个足够普遍的结果/验证/情况,应该有一种 DRYer 方法来做到这一点,不是吗? 你会怎么做这样的检查?

在这种情况下如何处理呢? 假设您想在视图中显示评论创建日期:

Last comment for this post at : <%= @post.comments[0].created_at %>

而且这篇文章没有任何 cmets。 你可以这样做

Last comment for this post at : <%= @post.comments.last.created_at unless @post.comments.empty? %>

您可以检查控制器。等等。有几种方法可以做到这一点。但是处理这个问题的“最佳”方法是什么?

【问题讨论】:

    标签: ruby-on-rails exception exception-handling


    【解决方案1】:

    大量手动检查异常的事实表明您没有正确使用它们。事实上,你的例子没有一个是例外的。

    至于不存在的帖子 - 您应该期望您的 API 用户(例如,通过浏览器使用您的网络的用户)请求不存在的帖子。

    您的第二个示例(Post.find(5).cmets[0].created_at) 也不例外。有些帖子只是没有 cmets,而您事先就知道了。那么为什么要抛出异常呢?

    ActiveRecord::RecordInvalid 示例也是如此。没有理由通过异常来处理这种情况。用户在表单中输入一些无效数据是很平常的事情,没有什么特别之处。

    在某些情况下对这些情况使用异常机制可能非常方便,但由于上述原因,它是不正确的。

    话虽如此,但这并不意味着您不能 DRY 封装这些情况的代码。你很有可能至少在某种程度上可以做到这一点,因为这些情况很常见。

    那么,例外情况如何?嗯,第一条规则确实是:尽可能少地使用它们。

    如果您真的需要使用它们,通常有两种例外情况(如我所见):

    1. 不会破坏用户在您的应用程序内的一般工作流程的异常(想象一下您的个人资料图片缩略图生成例程中的异常),您可以对用户隐藏它们,或者您只是通知他有关问题及其必要时的后果

    2. 完全阻止用户使用应用程序的异常。这些是最后的手段,应该通过 Web 应用程序中的 500 内部服务器错误来处理。

    我倾向于只为后者使用 ApplicationController 中的 rescue_from 方法,因为第一种类型有更合适的位置,而 ApplicationController 作为控制器类的最顶层似乎是回退到的正确位置在这种情况下(尽管现在某种 Rack 中间件可能更适合放置这种东西)。

    -- 编辑--

    建设性部分:

    至于第一件事,我的建议是开始使用 find_by_id 而不是 find,因为它不会抛出异常,但如果不成功则返回 nil。你的代码看起来像这样:

    unless @p = Post.find_by_id(params[:id])
      flash[:error] = "Can't find Blog Post"
    end
    

    不那么健谈了。

    干燥这种情况的另一个常见习惯用法是使用控制器 before_filters 来设置经常使用的变量(如本例中的@p)。之后,您的控制器可能如下所示

    controller PostsController
      before_filter :set_post, :only => [:create, :show, :destroy, :update]
    
      def show
          flash[:error] = "Can't find Blog Post" unless @p
      end 
    
    private
    
      def set_post
        @p = Post.find_by_id(params[:id]) 
      end
    
    end
    

    至于第二种情况(不存在最后一条评论),这个问题的一个明显解决方案是将整个事情移到一个助手中:

    # This is just your way of finding out the time of the last comment moved into a 
    # helper. I'm not saying it's the best one ;)
    def last_comment_datetime(post)
      comments = post.comments
      if comments.empty?
        "No comments, yet."
      else
        "Last comment for this post at: #{comments.last.created_at}"
      end
    end
    

    那么,在你看来,你只需调用

    <%= last_comment_datetime(post) %>
    

    通过这种方式,边缘情况(没有任何 cmets 的帖子)将在其自己的位置处理,并且不会使视图混乱。

    我知道,这些都没有建议在 Rails 中处理错误的任何模式,但也许通过诸如此类的一些重构,您会发现对某种异常/错误处理策略的大量需求就消失了。

    【讨论】:

    • 请 - 在答案中查看我上面的回复 - 我无法在 cmets 中发布正确缩进的代码 :-)
    • 嗨,尼克,抱歉听起来如此粗鲁。我只是没有足够的时间在我的答案中添加任何建设性的部分。现在,它就在那里。希望整个答案现在更有帮助。
    • 没问题 - 没有冒犯,我的问题也不是很清楚 - 直到我清理它。你给了我一些很好的建议——我等一下,也许有人有更多的建议,然后我会将其标记为已回答。谢谢你的详细回答。 :-)
    • 另外关于异常处理的主题(与我问这个问题时的意思的错误处理相比)我做了更多的谷歌搜索,发现了这一点,它谈到了异常通知插件,它也很优雅处理 RecordNotFound 等:whynotwiki.com/Rails_/_Exception_handling(还有 ruby​​ 工具箱在此主题上列出了一些其他项目:ruby-toolbox.com/categories/exception_notification.html
    【解决方案2】:

    例外是针对特殊情况。糟糕的用户输入通常并不例外。如果有的话,这很常见。当你确实有特殊情况时,你想给自己尽可能多的信息。根据我的经验,最好的方法是根据调试经验认真改进异常处理。当你遇到异常时,你应该做的第一件事就是为它编写一个单元测试。您应该做的第二件事是确定是否有更多信息可以添加到异常中。在这种情况下,更多信息通常采用以下形式:在堆栈更高的位置捕获异常并处理它或抛出一个新的、信息量更大的异常,该异常具有附加上下文的好处。我个人的规则是,我不喜欢从堆栈的三层以上捕获异常。如果异常必须传播得更远,您需要更早地捕获它。

    至于在 UI 中暴露错误,if/case 语句是完全可以的,只要你不要嵌套太深。那就是这种代码难以维护的时候。如果出现问题,您可以将其抽象化。

    例如:

    def flash_assert(conditional, message)
      return true if conditional
      flash[:error] = message
      return false
    end
    
    flash_assert(Post.exists?(params[:post_id]), "Can't find Blog Post") or return
    

    【讨论】:

      猜你喜欢
      • 2010-10-09
      • 1970-01-01
      • 2015-12-13
      • 2011-04-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-11-15
      相关资源
      最近更新 更多