Cuando nos encontramos aprendiendo Ruby on Rails comenzamos a trabajar con modelos aislados, sin embargo, a medida que nuestros proyectos van creciendo, así lo hace la complejidad de nuestras bases de datos. Nuestros modelos comenzarán a tener relación con otros, ya sea una relación uno a uno, uno a muchos o muchos a muchos. Cuando esto ocurra debemos de ser capaces de mantener la integridad de nuestra base de datos, de tal forma que no exista alguna inconsistencia en los datos; Es por ello que en este post explicaremos las diferentes formas en las cuales podemos eliminar registros en Ruby on Rails de forma encadenada, algo que en base de datos conocemos como Eliminación es cascada (on delete cascade)
Relación uno a muchos
Para este post trabajaremos con dos modelos, User y Note. La relación será de uno a muchos. Un usuario podrá tener muchas notas y una nota le pertenece a un usuario.
rails g model User username:string
rails g model Note content:string user:references
Para que exista una relación entre los modelos colocaremos user:references. De esta forma nuestra tabla notes poseerá una llave foránea, user_id, esto hará que sea obligatorio que todos los registros en la tabla notes posean un user_id válido, es decir, un user_id que exista en la tabla users. No puede existir una nota sin un usuario existente 😃.
Una vez con los modelos ejecutamos las migraciones.
rake db:migrate
El siguiente paso es crear un par de registros con los cuales podamos probar nuestros métodos.
En nuestra consola ejecutamos las siguientes sentencias.
> user = User.create username:'codi1'
> note1 = Note.create content:'Mensaje 1', user:user
> note2 = Note.create content:'Mensaje 2', user:user
Creamos un usuario al cual le asignamos dos notas.
Para que la relación sea completa agregamos la siguiente línea en nuestra clase User.
has_many :notes
Esta línea nos permite acceder a todas las notas de un usuario mediante el método notes.
> user = User.last
> user.notes
Listo, con el belongs to de la clase Note y el has many de la clase User ya tenemos una relación uno a muchos 😎.
Eliminar registros
Si nosotros intentamos eliminar el usuario de nuestra base de datos obtendremos un error.
> user.destroy
(0.1ms) begin transaction
User Destroy (1.2ms) DELETE FROM "users" WHERE "users"."id" = ? [["id", 1]]
(0.4ms) rollback transaction
ActiveRecord::InvalidForeignKey: SQLite3::ConstraintException: FOREIGN KEY constraint failed: DELETE FROM "users" WHERE "users"."id" = ?
from (irb):21
El error se debe a la integridad referencial. Si recordamos los registros de la tabla notes deben de poseer un user_id válido; No puede haber inconsistencia de datos.
Si nosotros queremos eliminar un registro el cual posee relaciones, lo que debemos de hacer es, primero eliminar todas las relaciones, para posteriormente eliminar el registro per se. En nuestro ejemplo, para eliminar un usuario de forma correcta pudiésemos ejecutar las siguientes sentencias.
note1 = user.notes.first
note2 = user.notes.second
note1.destroy
note2.destroy
user.destroy
Esto funciona, sin embargo, no es la mejor opción, principalmente, por la cantidad de sentencias que debemos de ejecutar. Imaginemos a un usuario con cien, quinientas o mil notas 😰, eliminar un usuario sería una tarea que nadie quisiera hacer 😅.
Afortunadamente en Ruby on Rails tenemos la opción de realizar una eliminación en cascada desde nuestro modelo. En este caso, al eliminar un usuario que también se eliminen sus notas. Para ello nos podremos apoyar de dos métodos.
- destroy
- delete_all
Para comprender mejor estos dos métodos, agregaremos un par de callbacks a nuestra clase Note.
class Note < ApplicationRecord
belongs_to :user
before_destroy :before_destroy_method
after_destroy :after_destroy_method
def before_destroy_method
puts "Antes de eliminar!"
end
def after_destroy_method
puts "Después de eliminar!"
end
end
destroy
Comencemos con el método destroy, para ello debemos de modificar el has_many de nuestra clase User.
has_many :notes, dependent: :destroy
Al nosotros utilizar dependent: :destroy en nuestro modelo, cada registro que tenga relación con el objeto a eliminar será instanciado, para posteriormente ejecutar su método destroy.
Si tratamos de eliminar nuestro usuario obtendremos la siguiente salida.
> user.destroy
(0.1ms) begin transaction
Note Load (0.2ms) SELECT "notes".* FROM "notes" WHERE "notes"."user_id" = ? [["user_id", 1]]
Antes de eliminar!
Note Destroy (0.3ms) DELETE FROM "notes" WHERE "notes"."id" = ? [["id", 1]]
Después de eliminar!
Antes de eliminar!
Note Destroy (0.1ms) DELETE FROM "notes" WHERE "notes"."id" = ? [["id", 2]]
Después de eliminar!
User Destroy (0.1ms) DELETE FROM "users" WHERE "users"."id" = ? [["id", 1]]
(0.8ms) commit transaction
=> #<User id: 6, username: "codi1", created_at: "2018-09-16 22:55:09", updated_at: "2018-09-16 22:55:09">
Podemos observar que el primer paso es eliminar las notas del usuario. Las notas se instancian y eliminan de una en una, lo cual hace ejecutar todos los callbacks before y after destroy. Una vez las notas han sido eliminadas, el siguiente paso es eliminar al usuario.
dependent: :destroy es la forma más orgánica y segura de eliminar registros en cascadas, ya que todos los objetos a eliminar harán uso del método destroy.
delete_all
Al igual que con el método destroy, con delete_all debemos de modificar el has_many de la clase User.
has_many :notes, dependent: :delete_all
Al nosotros hacer uso de dependent: :delete_all en nuestro modelo, los registros que tengan relación con el objeto a eliminar serán eliminados mediante la sentencia SQL, DELETE, es decir, los objetos relacionados no serán instanciados. Eliminar los registros directamente de la base de datos hará que los callbacks en nuestros modelos no sean ejecutados.
Si tratamos de eliminar nuestro usuario obtendremos la siguiente salida.
> user.destroy
(0.1ms) begin transaction
Note Destroy (0.4ms) DELETE FROM "notes" WHERE "notes"."user_id" = ? [["user_id", 2]]
User Destroy (0.1ms) DELETE FROM "users" WHERE "users"."id" = ? [["id", 2]]
(1.3ms) commit transaction
=> #<User id: 2, username: "codi1", created_at: "2018-09-16 23:13:42", updated_at: "2018-09-16 23:13:42">
Cómo podemos observar el primer paso es eliminar todas las notas que le corresponda al usuario. A diferencia de dependent: :destroy donde las notas se eliminaban por su id, con dependent: :delete_all las notas se eliminan por su user_id. Una vez las notas han sido eliminadas, el siguiente paso es eliminar el usuario.