Jak „zatwierdzić” przy niszczeniu w szynach

81

Czy w przypadku niszczenia spokojnego zasobu chcę zagwarantować kilka rzeczy, zanim pozwolę kontynuować operację niszczenia? Zasadniczo chcę mieć możliwość zatrzymania operacji niszczenia, jeśli zauważę, że spowodowałoby to nieprawidłowe ustawienie bazy danych? Nie ma żadnych wywołań zwrotnych walidacji operacji niszczenia, więc jak można „sprawdzić”, czy operacja zniszczenia powinna zostać zaakceptowana?

Stephen Cagle
źródło

Odpowiedzi:

70

Możesz zgłosić wyjątek, który następnie złapiesz. Railsy opakowują usunięcia w transakcję, co pomaga.

Na przykład:

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

Alternatywnie możesz użyć wywołania zwrotnego before_destroy. To wywołanie zwrotne jest zwykle używane do niszczenia rekordów zależnych, ale zamiast tego można zgłosić wyjątek lub dodać błąd.

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.destroyzwróci teraz wartość false i myBooking.errorszostanie wypełniony po powrocie.

Airsource Ltd
źródło
3
Zauważ, że tam, gdzie teraz jest napisane "... ok, śmiało i niszcz", musisz wstawić "super", więc oryginalna metoda zniszczenia jest faktycznie wywoływana.
Alexander Malfait,
3
errors.add_to_base jest przestarzałe w Railsach 3. Zamiast tego powinieneś zrobić errors.add (: base, "message").
Ryan,
9
Railsy nie sprawdzają poprawności przed zniszczeniem, więc before_destroy musiałby zwrócić wartość false, aby anulować niszczenie. Samo dodawanie błędów jest bezużyteczne.
graywh
24
W przypadku Rails 5 znak falsena końcu before_destroyjest bezużyteczny. Od teraz powinieneś używać throw(:abort)(@see: weblog.rubyonrails.org/2015/1/10/This-week-in-Rails/… ).
romainsalles
1
Twój przykład obrony przed osieroconymi rekordami można rozwiązać znacznie łatwiej za pomocąhas_many :booking_payments, dependent: :restrict_with_error
thisismydesign
48

tylko uwaga:

Do szyn 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
workdreamer
źródło
2
Problem z tym podejściem polega na tym, że wywołanie zwrotne before_destroy wydaje się być wywoływane po zniszczeniu wszystkich booking_payments.
sunkencity
4
Powiązany bilet: github.com/rails/rails/issues/3458 @sunkencity możesz zadeklarować before_destroy przed deklaracją asocjacji, aby tymczasowo tego uniknąć.
lulalala
1
Twój przykład obrony przed osieroconymi rekordami można rozwiązać znacznie łatwiej za pomocąhas_many :booking_payments, dependent: :restrict_with_error
thisismydesign
Zgodnie z przewodnikiem po railsach wywołania zwrotne before_destroy mogą i powinny być umieszczane przed skojarzeniami z zależnym_destroy; wywołuje to wywołanie zwrotne przed wywołaniem powiązanych zniszczeń: guide.rubyonrails.org/…
grouchomc
20

Oto co zrobiłem z Railsami 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
Raphael Monteiro
źródło
3
To fajny artykuł, który wyjaśnia to zachowanie w Railsach 5: blog.bigbinary.com/2016/02/13/…
Yaro Holodiuk
1
Twój przykład obrony przed osieroconymi rekordami można rozwiązać znacznie łatwiej za pomocąhas_many :qrcodes, dependent: :restrict_with_error
thisismydesign
6

Skojarzenia ActiveRecord has_many i has_one pozwalają na zależną opcję, która zapewni, że powiązane wiersze tabeli zostaną usunięte podczas usuwania, ale zwykle ma to na celu utrzymanie bazy danych w czystości, a nie zapobieganie jej niepoprawności.

idź minimalnie
źródło
1
Innym sposobem na zajęcie się podkreśleniami, jeśli są one częścią nazwy funkcji lub podobną, jest umieszczenie ich w znakach odwrotnych. Które następnie będą wyświetlane jako kodu like_so.
Richard Jones
Dziękuję Ci. Twoja odpowiedź doprowadziła mnie do kolejnego wyszukiwania na temat typów opcji zależnych , na które
znalazłem
Istnieją również dependentopcje, które nie pozwalają na usunięcie podmiotu, jeśli spowodowałoby to utworzenie osieroconych rekordów (jest to bardziej istotne w przypadku pytania). Np.dependent: :restrict_with_error
thisismydesign
5

Akcję zniszczenia można umieścić w instrukcji „if” w kontrolerze:

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

Gdzie valid_destroy? jest metodą w klasie modelu, która zwraca wartość true, jeśli spełnione są warunki zniszczenia rekordu.

Posiadanie takiej metody pozwoli również zapobiec wyświetlaniu użytkownikowi opcji usuwania - co poprawi komfort użytkowania, ponieważ użytkownik nie będzie mógł wykonać nielegalnej operacji.

Toby Hede
źródło
1
dobry chwyt, ale zakładałem, że ta metoda jest w kontrolerze, odwołując się do modelu. Gdyby to było w modelu, na pewno spowodowałoby problemy
Toby Hede
hehe, przepraszam ... Rozumiem, co masz na myśli, właśnie zobaczyłem "metodę na twojej klasie modelki" i szybko pomyślałem "uh oh", ale masz rację - zniszcz na kontrolerze, to zadziała. :)
jenjenut233
wszystko dobrze, w rzeczywistości lepiej być bardzo klarownym, niż utrudniać życie niektórym biednym początkującym przez słabą przejrzystość
Toby Hede
1
Myślałem o zrobieniu tego również w kontrolerze, ale tak naprawdę to należy do modelu, aby obiekty nie mogły zostać zniszczone z konsoli lub innego kontrolera, który może potrzebować zniszczyć te obiekty. Utrzymuj to SUCHE. :)
Joshua Pinter
Biorąc to pod uwagę, nadal możesz użyć swojego ifoświadczenia w destroyakcji kontrolera, z wyjątkiem zamiast wywoływaniaif model.valid_destroy? , wystarczy zadzwonić if model.destroyi niech model klamki czy zniszczyć był udany, itd.
Joshua Pinter
5

Stan na Rails 6:

To działa:

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: :destroynie działa: https://github.com/rails/rails/issues/32376

Ponieważ Rails 5 throw(:abort)jest zobowiązany do anulowania wykonania: https://makandracards.com/makandra/20301-cancelling-the-activerecord-callback-chain

prepend: truejest wymagany, aby dependent: :destroynie działał przed wykonaniem walidacji: https://github.com/rails/rails/issues/3458

Możesz wyłowić to razem z innymi odpowiedziami i komentarzami, ale żadna z nich nie jest kompletna.

Na marginesie, wielu użyło has_manyrelacji jako przykładu, w którym chcą upewnić się, że nie usuwają żadnych rekordów, jeśli spowodowałoby to utworzenie osieroconych rekordów. Można to rozwiązać znacznie łatwiej:

has_many :entities, dependent: :restrict_with_error

to mój projekt
źródło
Małe ulepszenie: before_destroy :handle_destroy, prepend: true; before_destroy { throw(:abort) if errors.present? }pozwoli na przejście błędów z innych walidacji before_destroy zamiast natychmiastowego kończenia procesu niszczenia
Paul Odeon,
4

Skończyło się na tym, że użyłem kodu stąd, aby utworzyć zastąpienie can_destroy na 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

Ma to dodatkową zaletę polegającą na tym, że ukrywanie / wyświetlanie przycisku usuwania w interfejsie użytkownika jest trywialne

Hugo Forte
źródło
2

Możesz również użyć wywołania zwrotnego before_destroy, aby zgłosić wyjątek.

Matthias Winkelmann
źródło
2

Mam te klasy lub modele

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

Teraz, gdy usuwasz przedsiębiorstwo, ten proces sprawdza, czy istnieją produkty powiązane z przedsiębiorstwami. Uwaga: Musisz wpisać to na początku klasy, aby najpierw je zweryfikować.

Mateo Vidal
źródło
1

Użyj walidacji kontekstu ActiveRecord w Railsach 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
miecznik
źródło
Nie możesz zweryfikować on: :destroy, zobacz ten numer
thesecretmaster