Wie "validiere" ich bei der Zerstörung in Schienen?

81

Bei der Zerstörung einer erholsamen Ressource möchte ich einige Dinge garantieren, bevor ich zulasse, dass eine Zerstörungsoperation fortgesetzt wird. Grundsätzlich möchte ich die Möglichkeit haben, den Zerstörungsvorgang zu stoppen, wenn ich feststelle, dass die Datenbank dadurch in einen ungültigen Zustand versetzt wird. Es gibt keine Validierungsrückrufe für eine Zerstörungsoperation. Wie "validiert" man also, ob eine Zerstörungsoperation akzeptiert werden soll?

Stephen Cagle
quelle
Siehe auch
Deepak N

Antworten:

70

Sie können eine Ausnahme auslösen, die Sie dann abfangen. Rails-Wraps werden in einer Transaktion gelöscht, was hilfreich ist.

Zum Beispiel:

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

Alternativ können Sie den Rückruf before_destroy verwenden. Dieser Rückruf wird normalerweise verwendet, um abhängige Datensätze zu zerstören. Sie können jedoch eine Ausnahme auslösen oder stattdessen einen Fehler hinzufügen.

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.destroywird nun false zurückgeben und myBooking.errorswird bei der Rückgabe ausgefüllt.

Airsource Ltd.
quelle
3
Beachten Sie, dass dort, wo jetzt "... ok, mach weiter und zerstöre" steht, "super" gesetzt werden muss, damit die ursprüngliche Zerstörungsmethode tatsächlich aufgerufen wird.
Alexander Malfait
3
Errors.add_to_base ist in Rails 3 veraltet. Stattdessen sollten Sie Errors.add (: base, "message") ausführen.
Ryan
9
Rails wird vor der Zerstörung nicht validiert, daher müsste before_destroy false zurückgeben, damit die Zerstörung abgebrochen wird. Nur das Hinzufügen von Fehlern ist nutzlos.
Graywh
24
Bei Rails 5 ist das falseam Ende des before_destroynutzlos. Von nun an sollten Sie verwenden throw(:abort)(@see: weblog.rubyonrails.org/2015/1/10/This-week-in-Rails/… ).
Romainsalles
1
Ihr Beispiel für die Verteidigung gegen verwaiste Rekorde kann viel einfacher gelöst werden überhas_many :booking_payments, dependent: :restrict_with_error
thisismydesign
48

nur eine Notiz:

Für Schienen 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
Arbeitsträumer
quelle
2
Ein Problem bei diesem Ansatz besteht darin, dass der Rückruf vor_Zerstörung aufgerufen zu werden scheint, nachdem alle Buchungszahlungen zerstört wurden.
Sunkencity
4
Zugehöriges Ticket: github.com/rails/rails/issues/3458 @sunkencity Sie können before_destroy vor der Assoziationserklärung deklarieren, um dies vorübergehend zu vermeiden.
Lulalala
1
Ihr Beispiel für die Verteidigung gegen verwaiste Rekorde kann viel einfacher gelöst werden überhas_many :booking_payments, dependent: :restrict_with_error
thisismydesign
Gemäß der Rails-Anleitung können und sollten Rückrufe vor Assoziationen mit abhängigen_Destroy platziert werden. Dies löst den Rückruf aus, bevor die zugehörigen Zerstörungen
grouchomc
20

Das habe ich mit Rails 5 gemacht:

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
quelle
3
Dies ist ein schöner Artikel, der dieses Verhalten in Rails 5 erklärt: blog.bigbinary.com/2016/02/13/…
Yaro Holodiuk
1
Ihr Beispiel für die Verteidigung gegen verwaiste Rekorde kann viel einfacher gelöst werden überhas_many :qrcodes, dependent: :restrict_with_error
thisismydesign
6

Die ActiveRecord-Zuordnungen has_many und has_one ermöglichen eine abhängige Option, mit der sichergestellt wird, dass verwandte Tabellenzeilen beim Löschen gelöscht werden. Dies dient jedoch normalerweise dazu, Ihre Datenbank sauber zu halten, anstatt zu verhindern, dass sie ungültig wird.

Gehen Sie minimal
quelle
1
Eine andere Möglichkeit, Unterstriche zu entfernen, wenn sie Teil eines Funktionsnamens oder ähnlichem sind, besteht darin, sie in Backticks zu verpacken. Das wird dann als Code angezeigt , like_so.
Richard Jones
Vielen Dank. Ihre Antwort führte mich zu einer weiteren Suche nach Arten von abhängigen Optionen, die hier beantwortet wurden: stackoverflow.com/a/25962390/3681793
bonafernando
Es gibt auch dependentOptionen, mit denen eine Entität nicht entfernt werden kann, wenn verwaiste Datensätze erstellt werden (dies ist für die Frage relevanter). ZBdependent: :restrict_with_error
thisismydesign
5

Sie können die Zerstörungsaktion in eine "if" -Anweisung im Controller einschließen:

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

Wo valid_destroy? ist eine Methode für Ihre Modellklasse, die true zurückgibt, wenn die Bedingungen zum Zerstören eines Datensatzes erfüllt sind.

Mit einer solchen Methode können Sie auch verhindern, dass dem Benutzer die Löschoption angezeigt wird. Dies verbessert die Benutzererfahrung, da der Benutzer keinen unzulässigen Vorgang ausführen kann.

Toby Hede
quelle
7
Endlosschleife?
Jenjenut233
1
Guter Fang, aber ich ging davon aus, dass sich diese Methode im Controller befindet und sich auf das Modell bezieht. Wenn es im Modell wäre, würde es definitiv Probleme verursachen
Toby Hede
hehe, tut mir leid ... ich verstehe, was du meinst, ich habe gerade "Methode für deine Modellklasse" gesehen und schnell gedacht "äh oh", aber du hast Recht - zerstöre auf dem Controller, das würde gut funktionieren. :)
jenjenut233
alles gut, in der Tat besser, sehr klar zu sein, als das Leben eines armen Anfängers mit schlechter Klarheit zu erschweren
Toby Hede
1
Ich habe darüber nachgedacht, dies auch im Controller zu tun, aber es gehört wirklich zum Modell, sodass Objekte nicht von der Konsole oder einem anderen Controller zerstört werden können, der diese Objekte möglicherweise zerstören muss. Halten Sie es trocken. :)
Joshua Pinter
4

Am Ende habe ich Code von hier verwendet, um eine can_destroy-Überschreibung für activerecord zu erstellen: 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

Dies hat den zusätzlichen Vorteil, dass es trivial ist, eine Löschtaste auf der Benutzeroberfläche auszublenden / anzuzeigen

Hugo Forte
quelle
3

Stand der Schienen 6:

Das funktioniert:

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 funktioniert nicht: https://github.com/rails/rails/issues/32376

Da Rails 5 throw(:abort)erforderlich ist, um die Ausführung abzubrechen: https://makandracards.com/makandra/20301-cancelling-the-activerecord-callback-chain

prepend: trueist erforderlich, damit dependent: :destroyes nicht ausgeführt wird, bevor die Validierungen ausgeführt werden: https://github.com/rails/rails/issues/3458

Sie können dies zusammen aus anderen Antworten und Kommentaren fischen, aber ich fand keine von ihnen vollständig.

Als Nebenbemerkung verwendeten viele eine has_manyBeziehung als Beispiel, in der sie sicherstellen möchten, dass keine Datensätze gelöscht werden, wenn verwaiste Datensätze erstellt werden. Dies kann viel einfacher gelöst werden:

has_many :entities, dependent: :restrict_with_error

Das ist mein Design
quelle
2

Sie können auch den before_destroy-Rückruf verwenden, um eine Ausnahme auszulösen.

Matthias Winkelmann
quelle
2

Ich habe diese Klassen oder Modelle

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

Wenn Sie nun ein Unternehmen löschen, überprüft dieser Prozess, ob Produkte mit Unternehmen verknüpft sind. Hinweis: Sie müssen dies oben in der Klasse schreiben, um es zuerst zu validieren.

Mateo Vidal
quelle
1

Verwenden Sie die ActiveRecord-Kontextüberprüfung in 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
Schwertkampf
quelle
Sie können nicht validieren on: :destroy, siehe dieses Problem
Sekretär