【问题标题】:Rails: How to prevent the deletion of records?Rails:如何防止删除记录?
【发布时间】:2016-09-01 20:31:19
【问题描述】:

我有一个模型国家,因此有一个表格国家。国家表作为 iso 国家和货币代码的集合,不应该减少那里的内容(在我用种子数据填充它之后)。因为 Country 是 ActiveRecord::Base 的子类,所以它继承了类方法如 destroy、delete_all 等删除记录。我正在寻找一种解决方案来防止在模型级别删除记录。

办公室。我知道我可以利用面向对象的方法通过覆盖这些方法来解决这个问题(并在它们调用时引发例如错误),但这假设我必须知道基类的所有继承方法。如果有人可以提供更优雅的解决方案,我会很高兴。

【问题讨论】:

标签: ruby-on-rails ruby-on-rails-4


【解决方案1】:

从 Mark Swardstrom 的回答中汲取灵感,我提出以下适用于 Rails > 5.0 的建议:

在您的模型中:

before_destroy :stop_destroy

def stop_destroy
  errors.add(:base, :undestroyable)
  throw :abort
end

以下内容将使所有对model.destroy 的调用返回false,并且您的模型不会被删除。

您仍然可以争辩说,调用model.delete 将起作用,并删除您的记录,但由于这些是较低级别的调用,这对我来说非常有意义。

如果需要,您也可以直接从数据库中删除记录,但上述解决方案会阻止从应用程序级别删除记录,这是检查的正确位置。

Rubocop 检查您对 delete 或 delete_all 的调用并发出警告,因此您可以 100% 确定如果您调用 model.delete 是因为您真的想要它。

我的解决方案适用于您需要 throw :abort 而不是返回 false 的最新 Rails 版本。

【讨论】:

    【解决方案2】:

    有一个 before_destroy 回调,也许你可以利用它。

    before_destroy :stop_destroy
    
    def stop_destroy
      self.errors[:base] << "Countries cannot be deleted"
      return false
    end
    

    【讨论】:

    • 使用'delete'时不会触发。仅在销毁时。
    • 同意这根本不是一个完整的答案。
    • 尝试 before_destroy :stop_destroy, prepend: true
    • 这确实是一个非常好的答案。我将提供一个新的、更新的答案,并提供更多详细信息。
    【解决方案3】:

    在 Rails 6 中,这就是我防止记录被删除的方法。 引发的异常将回滚 ActiveRecord 正在使用的事务,从而防止记录被删除

        class MenuItem < ApplicationRecord
    
          after_destroy :ensure_home_page_remains
    
          class Error < StandardError
          end
    
    
          protected #or private whatever you need
    
          #Raise an error that you trap in your controller to prevent your record being deleted.
            def ensure_home_page_remains
              if menu_text == "Home"
              raise Error.new "Can't delete home page"
            end
        end
    

    因此,ensure_home_page_remains 方法会引发MenItem::Error 导致事务回滚,您可以将其捕获在控制器中并采取您认为必要的任何适当操作,通常只是在重定向到某个地方后向用户显示错误消息.例如

          # DELETE /menu_items/1
          # DELETE /menu_items/1.json
          def destroy
            @menu_item.destroy
            respond_to do |format|
              format.html { redirect_to admin_menu_items_url, notice: 'Menu item was successfully destroyed.' }
              format.json { head :no_content }
            end
          end
    
          #Note, the rescue block is outside the destroy method
          rescue_from 'MenuItem::Error' do |exception|
            redirect_to menu_items_url, notice: exception.message
          end
    private
    #etc...
    

    【讨论】:

      【解决方案4】:

      Rails 4+(包括 Rails 6)

      在我们的例子中,我们希望防止对象在有任何关联记录时被销毁。这将是意外行为,因此我们希望引发异常并提供有用的错误消息。

      我们使用带有自定义消息的原生 ActiveRecord::RecordNotDestroyed 类,如下所述。

      class MyClass < ApplicationRecord
        has_many :associated_records
        before_destroy :check_if_associated_records
      
        private 
      
        def check_if_associated_records
          # set a guard clause to check whether the record is safe to destroy
          return unless associated_records.exists?
          raise ActiveRecord::RecordNotDestroyed, 'Cannot destroy because...'
        end
      end
      

      destroydestroy! 的行为

      my_class = MyClass.first.destroy
      # => false
      
      my_class = MyClass.first.destroy!
      # => ActiveRecord::RecordNotDestroyed (Cannot destroy because...)
      

      注意:如果您有 belongs_tohas_one 而不是 has_many 关联,您的方法将如下所示:

      def check_if_associated_records
        # set a guard clause to check whether the record is safe to destroy
        return unless associated_record
        raise ActiveRecord::RecordNotDestroyed, 'Cannot destroy because...'
      end
      

      【讨论】: