Как мне "проверить" на уничтожение в рельсах

81

При уничтожении успокаивающего ресурса я хочу гарантировать несколько вещей, прежде чем разрешу продолжить операцию уничтожения? В принципе, мне нужна возможность остановить операцию уничтожения, если я замечу, что это приведет к тому, что база данных перейдет в недопустимое состояние? Для операции уничтожения нет обратных вызовов проверки, так как же можно «проверить», следует ли принять операцию уничтожения?

Стивен Кейгл
источник

Ответы:

70

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

Например:

class Booking < ActiveRecord::Base
  has_many   :booking_payments
  ....
  def destroy
    raise "Cannot delete booking with payments" unless booking_payments.count == 0
    # ... ok, go ahead and destroy
    super
  end
end

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

def before_destroy
  return true if booking_payments.count == 0
  errors.add :base, "Cannot delete booking with payments"
  # or errors.add_to_base in Rails 2
  false
  # Rails 5
  throw(:abort)
end

myBooking.destroyтеперь вернет false и myBooking.errorsбудет заполнен при возврате.

Airsource Ltd
источник
3
Обратите внимание, что там, где теперь написано «... ладно, давай и уничтожь», вам нужно поставить «super», так что на самом деле вызывается оригинальный метод уничтожения.
Александр Мальфаит
3
errors.add_to_base устарела в Rails 3. Вместо этого вы должны сделать errors.add (: base, "message").
Ryan
9
Rails не проверяет перед уничтожением, поэтому before_destroy должен вернуть false, чтобы отменить уничтожение. Просто добавлять ошибки бесполезно.
graywh
24
В Rails 5 символ falseв конце before_destroyбесполезен. С этого момента вы должны использовать throw(:abort)(@see: weblog.rubyonrails.org/2015/1/10/This-week-in-Rails/… ).
romainsalles 06
1
Ваш пример защиты от потерянных записей может быть решен намного проще с помощьюhas_many :booking_payments, dependent: :restrict_with_error
thisismydesign
48

просто примечание:

Для рельсов 3

class Booking < ActiveRecord::Base

before_destroy :booking_with_payments?

private

def booking_with_payments?
        errors.add(:base, "Cannot delete booking with payments") unless booking_payments.count == 0

        errors.blank? #return false, to not destroy the element, otherwise, it will delete.
end
мечтатель
источник
2
Проблема с этим подходом заключается в том, что обратный вызов before_destroy, кажется, вызывается после того, как все booking_payments были уничтожены.
sunkencity
4
Связанный билет: github.com/rails/rails/issues/3458 @sunkencity вы можете объявить before_destroy перед объявлением ассоциации, чтобы временно избежать этого.
lulalala
1
Ваш пример защиты от потерянных записей может быть решен намного проще с помощьюhas_many :booking_payments, dependent: :restrict_with_error
thisismydesign
Согласно руководству rails, обратные вызовы before_destroy могут и должны быть размещены перед ассоциациями с зависимым_destroy; это вызывает обратный вызов до того, как будут вызваны связанные уничтожения: guides.rubyonrails.org/…
grouchomc
20

Так я поступил с Rails 5:

before_destroy do
  cannot_delete_with_qrcodes
  throw(:abort) if errors.present?
end

def cannot_delete_with_qrcodes
  errors.add(:base, 'Cannot delete shop with qrcodes') if qrcodes.any?
end
Рафаэль Монтейро
источник
3
Это хорошая статья, объясняющая такое поведение в Rails 5: blog.bigbinary.com/2016/02/13/…
Яро Холодюк,
1
Ваш пример защиты от потерянных записей может быть решен намного проще с помощьюhas_many :qrcodes, dependent: :restrict_with_error
thisismydesign
6

Связи ActiveRecord has_many и has_one позволяют использовать зависимую опцию, которая будет обеспечивать удаление связанных строк таблицы при удалении, но обычно это делается для поддержания вашей базы данных в чистоте, а не для предотвращения ее недействительности.

идти минимально
источник
1
Другой способ позаботиться о подчеркивании, если они являются частью имени функции или аналогичного, - заключить их в обратные кавычки. Это будет отображаться в виде кода like_so.
Ричард Джонс
Спасибо. Ваш ответ привел меня к еще одному поиску типов зависимых опций, на которые был дан ответ здесь: stackoverflow.com/a/25962390/3681793
bonafernando
Есть также dependentварианты, которые не позволяют удалить объект, если он создаст потерянные записи (это более актуально для вопроса). Egdependent: :restrict_with_error
thisismydesign
5

Вы можете заключить действие уничтожения в оператор «if» в контроллере:

def destroy # in controller context
  if (model.valid_destroy?)
    model.destroy # if in model context, use `super`
  end
end

Где valid_destroy? - это метод вашего класса модели, который возвращает истину, если выполняются условия для уничтожения записи.

Наличие такого метода также позволит вам предотвратить отображение опции удаления для пользователя, что улучшит взаимодействие с пользователем, поскольку пользователь не сможет выполнить незаконную операцию.

Тоби Хеде
источник
1
хороший улов, но я предполагал, что этот метод находится в контроллере, полагаясь на модель. Если бы это было в модели, это определенно вызвало бы проблемы
Тоби Хеде
хе-хе, извините за это ... Я понимаю, что вы имеете в виду, я только что увидел "метод в вашем классе модели" и быстро подумал "ах, ах", но вы правы - уничтожьте на контроллере, это будет работать нормально. :)
jenjenut233
все хорошо, на самом деле лучше быть предельно ясным, чем усложнять жизнь какого-нибудь плохого новичка из-за плохой ясности
Тоби Хеде
1
Я тоже думал о том, чтобы сделать это в контроллере, но он действительно принадлежит модели, поэтому объекты нельзя уничтожить с консоли или любого другого контроллера, которому может потребоваться уничтожить эти объекты. Держите это СУХОЕ. :)
Джошуа Пинтер
При этом вы все равно можете использовать свой ifоператор в destroyдействии вашего контроллера, за исключением того if model.valid_destroy?, что вместо вызова просто вызовите if model.destroyи позвольте модели определять, было ли уничтожение успешным и т. Д.
Джошуа Пинтер
5

Положение дел в Rails 6:

Это работает:

before_destroy :ensure_something, prepend: true do
  throw(:abort) if errors.present?
end

private

def ensure_something
  errors.add(:field, "This isn't a good idea..") if something_bad
end

validate :validate_test, on: :destroyне работает: https://github.com/rails/rails/issues/32376

Поскольку throw(:abort)для отмены выполнения требуется Rails 5 : https://makandracards.com/makandra/20301-cancelling-the-activerecord-callback-chain

prepend: trueтребуется, чтобы dependent: :destroyон не запускался до выполнения проверки: https://github.com/rails/rails/issues/3458

Вы можете выудить это вместе из других ответов и комментариев, но я не нашел ни одного из них полным.

В качестве дополнительной заметки многие использовали has_manyотношение в качестве примера, в котором они хотят убедиться, что не удаляют какие-либо записи, если это приведет к созданию потерянных записей. Решить эту проблему гораздо проще:

has_many :entities, dependent: :restrict_with_error

это
источник
Небольшое улучшение: before_destroy :handle_destroy, prepend: true; before_destroy { throw(:abort) if errors.present? }позволит пропускать ошибки из других проверок before_destroy вместо немедленного завершения процесса уничтожения
Пол Одеон
4

В итоге я использовал код отсюда, чтобы создать переопределение can_destroy на activerecord: https://gist.github.com/andhapp/1761098

class ActiveRecord::Base
  def can_destroy?
    self.class.reflect_on_all_associations.all? do |assoc|
      assoc.options[:dependent] != :restrict || (assoc.macro == :has_one && self.send(assoc.name).nil?) || (assoc.macro == :has_many && self.send(assoc.name).empty?)
    end
  end
end

Это имеет дополнительное преимущество, позволяющее упростить скрытие / отображение кнопки удаления в пользовательском интерфейсе.

Хьюго Форте
источник
2

Вы также можете использовать обратный вызов before_destroy, чтобы вызвать исключение.

Маттиас Винкельманн
источник
2

У меня есть эти классы или модели

class Enterprise < AR::Base
   has_many :products
   before_destroy :enterprise_with_products?

   private

   def empresas_with_portafolios?
      self.portafolios.empty?  
   end
end

class Product < AR::Base
   belongs_to :enterprises
end

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

Матео Видал
источник
1

Используйте проверку контекста ActiveRecord в Rails 5.

class ApplicationRecord < ActiveRecord::Base
  before_destroy do
    throw :abort if invalid?(:destroy)
  end
end
class Ticket < ApplicationRecord
  validate :validate_expires_on, on: :destroy

  def validate_expires_on
    errors.add :expires_on if expires_on > Time.now
  end
end
меч
источник
Вы не можете подтвердить on: :destroy, посмотрите эту проблему
секретарша