¿Cómo 'valido' en la destrucción en rieles

81

Tras la destrucción de un recurso en reposo, quiero garantizar algunas cosas antes de permitir que continúe una operación de destrucción. Básicamente, quiero tener la capacidad de detener la operación de destrucción si noto que hacerlo colocaría la base de datos en un estado no válido. No hay devoluciones de llamada de validación en una operación de destrucción, entonces, ¿cómo se "valida" si se debe aceptar una operación de destrucción?

Stephen Cagle
fuente

Respuestas:

70

Puede generar una excepción que luego captura. Rails envuelve elimina en una transacción, lo que ayuda a las cosas.

Por ejemplo:

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

Alternativamente, puede usar la devolución de llamada before_destroy. Esta devolución de llamada se usa normalmente para destruir registros dependientes, pero puede lanzar una excepción o agregar un error en su lugar.

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.destroyahora devolverá falso y myBooking.errorsse rellenará al volver.

Airsource Ltd
fuente
3
Tenga en cuenta que donde ahora dice "... ok, siga adelante y destruya", debe poner "super", por lo que se llama al método de destrucción original.
Alexander Malfait
3
errors.add_to_base está obsoleto en Rails 3. En su lugar, debería utilizar errors.add (: base, "message").
Ryan
9
Rails no valida antes de destruir, por lo que before_destroy necesitaría devolver falso para cancelar la destrucción. Simplemente agregar errores es inútil.
graywh
24
Con Rails 5, el falseal final before_destroyes inútil. De ahora en adelante debería usar throw(:abort)(@see: weblog.rubyonrails.org/2015/1/10/This-week-in-Rails/… ).
romainsalles
1
Su ejemplo de la defensa contra los registros huérfanos se puede resolver mucho más fácilmente a través dehas_many :booking_payments, dependent: :restrict_with_error
thisismydesign
48

solo una nota:

Para raíles 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
soñador
fuente
2
Un problema con este enfoque es que la devolución de llamada before_destroy parece llamarse después de que se hayan destruido todos los booking_payments.
sunkencity
4
Ticket relacionado: github.com/rails/rails/issues/3458 @sunkencity puede declarar before_destroy antes de la declaración de asociación para evitar esto temporalmente.
lulalala
1
Su ejemplo de la defensa contra los registros huérfanos se puede resolver mucho más fácilmente a través dehas_many :booking_payments, dependent: :restrict_with_error
thisismydesign
Según la guía de rieles, las devoluciones de llamada before_destroy pueden y deben colocarse antes de las asociaciones conpendent_destroy; esto activa la devolución de llamada antes de que se llamen las destrucciones asociadas: guides.rubyonrails.org/…
grouchomc
20

Es lo que hice con 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
Raphael Monteiro
fuente
3
Este es un buen artículo que explica este comportamiento en Rails 5: blog.bigbinary.com/2016/02/13/…
Yaro Holodiuk
1
Su ejemplo de la defensa contra los registros huérfanos se puede resolver mucho más fácilmente a través dehas_many :qrcodes, dependent: :restrict_with_error
thisismydesign
6

Las asociaciones de ActiveRecord has_many y has_one permiten una opción dependiente que asegurará que las filas de la tabla relacionadas se eliminen al eliminarlas, pero esto generalmente es para mantener limpia la base de datos en lugar de evitar que no sea válida.

ir mínimo
fuente
1
Otra forma de cuidar los guiones bajos, si son parte del nombre de una función o similar, es envolverlos entre comillas invertidas. Esto debe mostrar entonces como código, like_so.
Richard Jones
Gracias. Su respuesta me llevó a otra búsqueda sobre tipos de opciones dependientes que se respondió aquí: stackoverflow.com/a/25962390/3681793
bonafernando
También hay dependentopciones que no permiten la eliminación de una entidad si crea registros huérfanos (esto es más relevante para la pregunta). Por ejemplodependent: :restrict_with_error
thisismydesign
5

Puede envolver la acción de destrucción en una declaración "si" en el controlador:

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

¿Dónde valid_destroy? es un método en su clase modelo que devuelve verdadero si se cumplen las condiciones para destruir un registro.

Tener un método como este también le permitirá evitar que se muestre la opción de eliminar al usuario, lo que mejorará la experiencia del usuario ya que el usuario no podrá realizar una operación ilegal.

Toby Hede
fuente
7
Bucle infinito, ¿alguien?
jenjenut233
1
buena captura, pero asumí que este método está en el controlador, difiriendo del modelo. Si estuviera en el modelo definitivamente causaría problemas
Toby Hede
jeje, perdón por eso ... veo lo que quieres decir, acabo de ver "método en tu clase modelo" y rápidamente pensé "uh oh", pero tienes razón: destruir en el controlador, eso funcionaría bien. :)
jenjenut233
todo bien, de hecho es mejor ser muy claro en lugar de dificultar la vida de un principiante pobre con poca claridad
Toby Hede
1
También pensé en hacerlo en el Controlador, pero realmente pertenece al Modelo para que los objetos no se puedan destruir desde la consola o cualquier otro Controlador que pueda necesitar destruir esos objetos. Mantenlo SECO. :)
Joshua Pinter
4

Terminé usando el código de aquí para crear una anulación de can_destroy en 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

Esto tiene el beneficio adicional de hacer que sea trivial ocultar / mostrar un botón de eliminación en la interfaz de usuario

Hugo Forte
fuente
4

Situación a partir de Rails 6:

Esto funciona:

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

Dado que throw(:abort)se requiere Rails 5 para cancelar la ejecución: https://makandracards.com/makandra/20301-cancelling-the-activerecord-callback-chain

prepend: truees necesario para que dependent: :destroyno se ejecute antes de que se ejecuten las validaciones: https://github.com/rails/rails/issues/3458

Pueden pescar esto juntos a partir de otras respuestas y comentarios, pero no encontré ninguno de ellos completo.

Como nota al margen, muchos usaron una has_manyrelación como ejemplo en el que quieren asegurarse de no eliminar ningún registro si crearía registros huérfanos. Esto se puede resolver mucho más fácilmente:

has_many :entities, dependent: :restrict_with_error

este es mi diseño
fuente
Una pequeña mejora: before_destroy :handle_destroy, prepend: true; before_destroy { throw(:abort) if errors.present? }permitirá que los errores de otras validaciones before_destroy pasen en lugar de finalizar el proceso de destrucción de inmediato
Paul Odeon hace
2

También puede usar la devolución de llamada before_destroy para generar una excepción.

Matthias Winkelmann
fuente
2

Tengo estas clases o modelos

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

Ahora, cuando elimina una empresa, este proceso valida si hay productos asociados con empresas. Nota: debe escribir esto en la parte superior de la clase para validarlo primero.

Mateo Vidal
fuente
1

Utilice la validación de contexto de ActiveRecord en 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
Swordray
fuente
No puede validar on: :destroy, vea este problema
thesecretmaster