before_destroy callback не останавливает запись от удаления

Я пытаюсь предотвратить уничтожение записи, если есть дети.

class Submission < ActiveRecord::Base

has_many :quotations, :dependent => :destroy

 before_destroy :check_for_payments


  def quoted?
    quotations.any?
  end


  def has_payments?
   true if quotations.detect {|q| q.payment}
  end


  private

  def check_for_payments
    if quoted? && has_payments?
      errors[:base] << "cannot delete submission that has already been paid"
      false
    end
  end

end

class Quotation < ActiveRecord::Base

    #associations
    belongs_to :submission
        has_one :payment_notification   
        has_one :payment

         before_destroy :check_for_payments

private 

def check_for_payments
  if payment_notification || payment
    errors[:base] << "cannot delete quotation while payment exist"
    return false
  end
end
end

Когда я проверяю этот код, before_destroy: check_for_payments не позволяет удалить запись Quotation.

Однако: check_for_payments в обратном вызове Submission before_destroy не прекращает удаление Представление.

Как я могу приостановить подачу с момента уничтожения платежей?

Ответ 1

Я бы попробовал код ниже, где у меня есть:

  1. использовал has_many: через ассоциацию для платежей
  2. избегали ненужного извлечения записей котировок и платежей с помощью any? без блока, который приводит к использованию кеша счетчика ассоциаций, если он определен, или размер ассоциации, если он уже загружен, и при отсутствии SQL COUNT, если это необходимо.
  3. избегать перечисления котировок
  4. избегали проверки правдоподобия/наличия прокси-сервера q.payment напрямую, который не работает для has_xxx. Если вы хотите проверить наличие, используйте q.payment.present?

Попробуйте следующее и посмотрите, как вы идете:

class Submission < ActiveRecord::Base

  has_many :quotations,
    inverse_of: :submission,
    dependent: :destroy

  has_many :payments,
    through: :quotations

  before_destroy :check_for_payments

private

  def check_for_payments
    if payments.any?
      errors[:base] << "cannot delete submission that has already been paid"
      return false
    end
  end
end

Ответ 2

В Rails 5 вы должны throw :abort иначе это не сработает. (даже возвращая false)

Что-то вроде этого должно работать:

class Something < ApplicationRecord

  before_destroy :can_destroy?

  private

  def can_destroy?
    if model.something?
      errors[:base] << "Can't be destroy because of something"
      throw :abort
  end
end

Ответ 3

http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html порядок обратных вызовов (я изменил формулировку на этот конкретный пример)

Иногда код требует, чтобы обратные вызовы выполнялись в определенном порядке. Например, перед тем, как котировки будут уничтожены с помощью опции +dependent: destroy+, должен быть выполнен обратный вызов before_destroy (check_for_payments в этом случае).

В этом случае проблема заключается в том, что при выполнении обратного вызова before_destroy котировка недоступна, так как обратный вызов destroy сначала выполняется. Чтобы избежать этого, вы можете использовать опцию prepend для обратного вызова before_destroy.

before_destroy :check_for_payments, prepend: true

Я сделал новое приложение с теми же моделями, что и выше, а затем с помощью теста на отправку. Это довольно уродливо, я просто изучаю...

class Submission < ActiveRecord::Base

  has_many :quotations, :dependent => :destroy

  before_destroy :check_for_payments, prepend: true

  def quoted?
    quotations.any?
  end

  def has_payments?
    true if quotations.detect {|q| q.payment }
  end

  private

    def check_for_payments
      if quoted? && has_payments?
        errors[:base] << "error message"
        false
      end
    end

end

class Quotation < ActiveRecord::Base

  belongs_to :submission
  has_one :payment_notification   
  has_one :payment

  before_destroy :check_for_payments

  private 

    def check_for_payments
      if payment_notification || payment
        errors[:base] << "cannot delete quotation while payment exist"
        return false
      end
    end
end

require 'test_helper'

class SubmissionTest < ActiveSupport::TestCase


  test "can't destroy" do

    sub = Submission.new
    sub.save

    quote = Quotation.new
    quote.submission_id = sub.id
    quote.save

    pay = Payment.new
    pay.quotation_id = quote.id
    pay.save

    refute sub.destroy, "destroyed record"
  end
end

Он прошел !. Надеюсь, это поможет.

Ответ 4

Насколько я знаю, когда уничтожается вызов объекта класса (Submission), имеющего связь с dependent => :destroy, и если какой-либо обратный вызов в связанной модели не работает, в вашем случае " Quotation, то объект класса " Submission " все равно будут удалены.

Поэтому, чтобы предотвратить это поведение, мы должны использовать методы, о которых я могу сейчас думать:

1) Вместо возвращения ложна в Quotation#check_for_payments, вы могли бы вызвать исключение и обработать его изящно в Submission модели, которая будет делать полный ROLLBACK, и не позволить какой - либо записи будут уничтожены.

2) Вы можете проверить, является ли какие - либо quotations для Submission, например, иметь payment/payment_notification в Submission#check_for_payments сам метод, который предотвратил бы исключить Submission объекта.

Ответ 5

Убедитесь, что quoted? и has_payments? не возвращает false.

Для отладки попробуйте это

def check_for_payments
    raise "Debugging #{quoted?} #{has_payments?}" # Make sure both are true
    if quoted? && has_payments?
      errors[:base] << "cannot delete submission that has already been paid"
      false
    end
  end

Ответ 6

В Rails 5 вы также можете:

def destroy
  quoted? && has_payments? ? self : super
end

submission.destroy # => submission
submission.destroyed? # => true/false