Hola gente,
Tenemos por aquí el siguiente dilema:
LA DOBLE VINCULACIÓN
Tenemos el modelo Trip y el modelo Destination tal que:
Trip has_many :destination
Destination belongs_to :trip
Una de las condiciones de Trip es que tiene que tener al menos una
Destination. Y una de las condiciones de Destination es que tienen que
pertencer a un Trip.
Como ejemplo simplificado tenemos esto:
class Trip < ActiveRecord::Base
has_many :destinations
def validate
errors.add(:destinations, ‘Debes indicar al menos un destino’)
if destinations.empty?
end
end
class Destination < ActiveRecord::Base
belongs_to :trip
validates_presence_of :trip, :message => “Hay que asociarla a un Trip”
end
Entonces la creación de ambos modelos está sumamente linkada, sobre
todo a la hora de la creación del Trip que hay que crear el Trip y por
lo menos una Destination al mismo tiempo.
Ni corto ni perezoso se me ocurrió hacer esto:
trip =
Trip.new(
:name => 'el viaje',
:destinations => [
{ :name => 'madrid' },
{ :name => 'valencia' }
]
)
Pero me indica este error:
ActiveRecord::AssociationTypeMismatch: Destination(#18941590)
expected, got Hash(#102590)
Vamos que lo que me pide es que le haga esto:
trip =
Trip.new(
:name => 'el viaje',
:destinations => [
Destination.new( :name => 'madrid' ),
Destination.new( :name => 'valencia' )
]
)
En este punto empiezo a echar de menos algo de magia, y me empiezo a
preguntar si algo estoy haciendo mal. (Pregunta 1)
Aún así y por culpa de la doble dependencia tenemos que las nuevas
Destination no son correctas y no pueden ser grabadas, así como
tampoco el nuevo Trip:
puts trip.valid?
false
puts trip.errors.full_messages
Destinations is invalid
Destinations is invalid
trip.destinations.each do |destination|
puts "#{destination.name}: #{destination.errors.full_messages}"
end
madrid: Trip Hay que asociarla a un Trip
valencia: Trip Hay que asociarla a un Trip
ELIMINANDO DOBLE VALIDACIÓN y LA CREACIÓN PASO A PASO
En este punto ya empiezo a ceder y ver que no voy a poder crear todo
de un plumazo. Entonces voy a tener que crear primero el Trip y luego
ir asociándole Destinations una por una.
trip = Trip.create!( :name => 'el viaje' )
trip.destinations << Destination.new( :name => 'madrid' )
trip.destinations << Destination.new( :name => 'madrid' )
Veo que el operador << es bastante inteligente y no sólo instancia los
Destination sino que les asocia el trip_id y los guarda en base de
datos:
Trip Create (0.000594) INSERT INTO “trips” (“name”, “updated_at”,
“created_at”) VALUES(‘el viaje’, ‘2008-12-06 17:21:33’, ‘2008-12-06
17:21:33’)
Trip Load (0.000441) SELECT * FROM “trips” WHERE (“trips”.“id” =
996332878)
Destination Create (0.000205) INSERT INTO “destinations” (“name”,
“updated_at”, “trip_id”, “created_at”) VALUES(‘madrid’, ‘2008-12-06
17:21:33’, 996332878, ‘2008-12-06 17:21:33’)
Trip Load (0.000285) SELECT * FROM “trips” WHERE (“trips”.“id” =
996332878)
Destination Create (0.000120) INSERT INTO “destinations” (“name”,
“updated_at”, “trip_id”, “created_at”) VALUES(‘madrid’, ‘2008-12-06
17:21:33’, 996332878, ‘2008-12-06 17:21:33’)
Ahora ya puedo crear Trips y Destinations y asociarlos unos a otros
pero pierdo la validación de que un Trip nunca pueda estar con su
array de destinations vacío. (Pregunta 2)
EN EL CONTROLADOR
Por otro lado si subimos al controlador nos encontramos con que el
TripsController.create debe crear tanto el nuevo objeto Trip como las
Destinations que el cliente haya definido en el formulario.
Para ello me habría gustado que bastase con esto:
def create
@trip = Trip.new(params[:trip])
render :action => ‘show’
end
De tal modo que esto funcionara:
post(
:create,
:trip => {
:name => 'nombre del viaje',
:destinations => [
{ :name => 'madrid' },
{ :name => 'madrid' }
]
}
)
Y que generase una petición como esta:
Parameters: {“action”=>“create”, “trip”=>{“name”=>“nombre del viaje”,
“destinations”=>[{“name”=>“madrid”}, {“name”=>“madrid”}]},
“controller”=>“trips”}
Podría crear un formulario web creo que fácilmente usando helpers de
rails para que me generase este tipo de petición, pero no funciona.
Volvemos a tener el error de antes:
ActiveRecord::AssociationTypeMismatch: Destination(#18904540)
expected, got HashWithIndifferentAccess(#9626070)
Entonces veo que tengo que hacer lógica de creación de estos modelos
en el controlador, para crear antes el Trip y luego los Destinations.
No sé muy bien como es la mejor manera de hacer esto. (pregunta 3).
Además de que estoy seguro de que estoy rompiendo alguna regla REST al
crear instancias de un modelo en el controlador del otro.
EL RESUMEN
Entonces resumiendo creo que me quedo con 3 preguntas de todas las que
me podrían surgir:
Pregunta 1: ¿Es ActiveRecord capaz de soportar la creación de
elementos hijos a la par que se crea el padre?
Pregunta 2: ¿Hay alguna manera recomendada de gestionar este tipo de
vinculaciones recíprocas?
Pregunta 3: ¿Hay algún patrón que describa la mejor manera de realizar
creaciones de recursos anidados como este caso?
Perdón por lanzar 3 preguntas en el mismo hilo pero podéis ver que
estar estrechamente unidas y puede que dando respuesta a una se
resuelvan las demás.
ACERCAMIENTOS
He visto este screencast del increible Ryan:
Veo que usa atributos virtuales para la creación de elementos
asociados y hace uso del before_save.
En este tipo de intentos estamos ahora pero con la doble
vinculacióntenemos un pequeño bloqueo y no sabemos si usar el before_save, el
after_save… hacerlo todo en el controlador, usar transacciones por si
una creación falla eliminar todas las anteriores…
En fín… tengo la esperanza de que una frase adecuada dé solución a
todo este galimatías de dudas. Perdón por el
tostón.
Cualquier comentario es bien venido.
Muchas gracias
f.