Як я можу "перевірити" на знищення в рейки


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буде заповнено при поверненні.


3
Зверніть увагу, що там, де зараз написано "... гаразд, продовжуйте і знищуйте", вам потрібно поставити "супер", тому оригінальний метод знищення насправді називається.
Олександр Малфайт

3
error.add_to_base застаріло в Rails 3. Натомість вам слід зробити error.add (: base, "message").
Райан

9
Rails не перевіряється перед знищенням, тому before_destroy потрібно буде повернути false, щоб скасувати знищення. Просто додавати помилки марно.
greywh

24
З Rails 5, falseкінець кінця before_destroyмарний. Відтепер ви повинні використовуватиthrow(:abort) (@see: weblog.rubyonrails.org/2015/1/10/This-week-in-Rails/… ).
romainsalles

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_pay були знищені.
sunkencity

4
Пов’язаний квиток: github.com/rails/rails/issues/3458 @sunkencity ви можете оголосити before_destroy перед декларацією асоціації, щоб тимчасово уникнути цього.
lulalala

1
Ваш приклад захисту від осиротілих записів можна вирішити набагато простіше за допомогоюhas_many :booking_payments, dependent: :restrict_with_error
thisismydesign

Відповідно до напрямних рейок before_destroy зворотних викликів можна і потрібно розміщувати їх перед асоціаціями з depend_destroy; це викликає зворотний виклик до того, як викликаються пов’язані знищення
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.
Richard Jones

Дякую. Ваш відповідь привів мене в інший пошук про типах залежного варіанту , який відповідав тут: stackoverflow.com/a/25962390/3681793
bonafernando

Існують також dependentваріанти, які не дозволяють видалити сутність, якщо вона створює осиротілі записи (це більше стосується питання). Наприкладdependent: :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

Стан справ за напрямком 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

Оскільки Rails 5 throw(:abort)потрібно скасувати виконання: 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

У мене є ці класи або моделі

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, перегляньте цю проблему
секретар

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.