Rails 4: el utilizar o no inverse_of en mis relaciones ¿qué me implica? y ¿cómo afecta la versión de Rails 4.0 y 4.2?


rubyonrailsEn algún momento de nuestros proyectos hemos utilizado inverse_of, pero me pregunto ¿nos hemos detenido a investigar todas sus posibilidades? y ¿qué ocurre si cambio de versión en mi proyecto y no lo tengo claro? , ¿qué problemas me puedo encontrar? cuidado!, tienes que considerar ciertos aspectos de los que vamos a ver en este post. Voy a explicar el uso de inverse_of en la versión 4.0 de Rails y cómo se comporta también en estos casos con la versión Rails 4.2 :inverse_of (puedes ver este pull request). Existen varios casos en los que un inverse_of debes saber utilizarlo y yo te explico cómo puedes entenderlo y aplicarlo.

Primer caso: Creación de objetos y con accepts_nested_attributes_for

En este primer caso tenemos dos modelos User y Task

class User < ActiveRecord::Base

      has_many :interest_users, inverse_of: :user
      has_many :interests, through: :interest_users
      has_many :tasks, inverse_of: :user

      accepts_nested_attributes_for :interest_users, allow_destroy: true
      accepts_nested_attributes_for :tasks

      validates_presence_of :email
end

class Task < ActiveRecord::Base
      belongs_to :user, inverse_of: :tasks

      validates_presence_of :name
      validates :user, presence: true
end

veamos que ocurre si ejecutamos en consola sin los “inverse_of” en las relaciones con Rails 4.0.0:

2.1.1 :024 > User.create({ name: ‘User 23’, email: “user23@example.com”, tasks_attributes: [{ name: ‘task 23’ }] })
(0.1ms) begin transaction
(0.1ms) rollback transaction

=> #<User id: nil, name: “User 23”, email: “user23@example.com”, created_at: nil, updated_at: nil>

No se completó el create, fíjate que tenemos un (0.1ms) rollback transaction y para ver el mensaje de validación que nos está devolviendo, lo volvemos a ejecutar con create! para que nos devuelva el mensaje de a validación:

2.1.1 :025 > User.create!({ name: ‘User 23’, email: “user23@example.com”, tasks_attributes: [{ name: ‘task 23’ }] })
(0.1ms) begin transaction
(0.0ms) rollback transaction
ActiveRecord::RecordInvalid: Validation failed: Tasks user can’t be blank

IMPORTANTE: Efectivamente nos está dando un problema de validación, pero me pregunto ¿por qué nos está dando este error de validación si yo le he pasado todos los parámetros para que esto no suceda?. Probando me doy cuenta de dos cosas. Una es que cuando creo el objeto User con los datos en el create, me está devolviendo el id a nil => #<User id: nil, name: “User 23”, email: “user23@example.com”, created_at: nil, updated_at: nil> y dos cuando verifique en el modelo Task nuestra validación de validates :user, presence: true, nos devuelve el error de validación ya que no lo ha insertado aún en la base de datos, es decir, no dispone de ese registro para verificarlo. Es un error que nos puede volver locos si no sabemos este detalle.

Ahora haremos lo mismo pero esta vez tenemos activo inverse_of:

2.1.1 :031 > User.create!({ name: ‘User 23’, email: “user23@example.com”, tasks_attributes: [{ name: ‘task 23’ }] })
(0.1ms) begin transaction
SQL (0.3ms) INSERT INTO “users” (“created_at”, “email”, “name”, “updated_at”) VALUES (?, ?, ?, ?) [[“created_at”, Sat, 03 Jan 2015 18:15:11 UTC +00:00], [“email”, “user23@example.com”], [“name”, “User 23”], [“updated_at”, Sat, 03 Jan 2015 18:15:11 UTC +00:00]]
SQL (0.2ms) INSERT INTO “tasks” (“created_at”, “name”, “updated_at”, “user_id”) VALUES (?, ?, ?, ?) [[“created_at”, Sat, 03 Jan 2015 18:15:11 UTC +00:00], [“name”, “task 23”], [“updated_at”, Sat, 03 Jan 2015 18:15:11 UTC +00:00], [“user_id”, 12]]
(789.0ms) commit transaction
=> #<User id: 12, name: “User 23”, email: “user23@example.com”, created_at: “2015-01-03 18:15:11”, updated_at: “2015-01-03 18:15:11”>

En este caso nos ha creado ya los datos en users y tasks tal como podemos ver y nos crea la asociación de nuestros modelos que andábamos buscando. Para la versión de Rails 4.2.0 no necesitamos el inverse_of.

Segundo caso: relaciones has_many/belongs_to

Disponemos de dos modelos, un modelo User y un modelo Task con la siguiente declaración:

class User < ActiveRecord::Base
      has_many :tasks, inverse_of: :user

      accepts_nested_attributes_for :tasks,
            allow_destroy: true,
            reject_if: :all_blank
end

class Task < ActiveRecord::Base
      belongs_to :user, inverse_of: :tasks

      validates_presence_of :name
end

Vamos a realizar la primera exploración desde nuestra consola con la versión de Rails 4.0.0:

2.1.1 :008 > user = User.create(name: ‘User 1’, email: ‘user1@example.com’)
(0.1ms) begin transaction
SQL (0.4ms) INSERT INTO “users” (“created_at”, “email”, “name”, “updated_at”) VALUES (?, ?, ?, ?) [[“created_at”, Sat, 03 Jan 2015 16:32:30 UTC +00:00], [“email”, “user1@example.com”], [“name”, “User 1”], [“updated_at”, Sat, 03 Jan 2015 16:32:30 UTC +00:00]]
(2045.1ms) commit transaction
=> #<User id: 2, name: “User 1”, email: “user1@example.com”, created_at: “2015-01-03 16:32:30”, updated_at: “2015-01-03 16:32:30”>
2.1.1 :009 > task = user.tasks.create(name: “tasks 1”)
(0.0ms) begin transaction
SQL (0.3ms) INSERT INTO “tasks” (“created_at”, “name”, “updated_at”, “user_id”) VALUES (?, ?, ?, ?) [[“created_at”, Sat, 03 Jan 2015 16:32:38 UTC +00:00], [“name”, “tasks 1”], [“updated_at”, Sat, 03 Jan 2015 16:32:38 UTC +00:00], [“user_id”, 2]]
(656.1ms) commit transaction
=> #<Task id: 2, user_id: 2, name: “tasks 2”, created_at: “2015-01-03 16:32:38”, updated_at: “2015-01-03 16:32:38”>
2.1.1 :010 > task.user == user
=> true

El resultado es, primero nos creamos un usuario con el método create (recuerda que no necesita hacer un save como el método new), nos creamos una tarea asociada al usuario creado anteriormente y finalmente comprobamos si ambos objetos son el mismo con inverse_of activo en los modelos que hemos descrito anteriormente. Como podemos comprobar, nos dice que son iguales ambos objetos según nos devuelve la comparación task.user == user.

Ahora ¿qué ocurre si hacemos lo mismo pero quitando inverse_of?:

2.1.1 :014 > user = User.create(name: ‘User 2’, email: ‘user2@example.com’)
(0.1ms) begin transaction
SQL (23.3ms) INSERT INTO “users” (“created_at”, “email”, “name”, “updated_at”) VALUES (?, ?, ?, ?) [[“created_at”, Sat, 03 Jan 2015 17:01:31 UTC +00:00], [“email”, “user2@example.com”], [“name”, “User 2”], [“updated_at”, Sat, 03 Jan 2015 17:01:31 UTC +00:00]]
(500.5ms) commit transaction
=> #<User id: 3, name: “User 2”, email: “user2@example.com”, created_at: “2015-01-03 17:01:31”, updated_at: “2015-01-03 17:01:31”>
2.1.1 :015 > task = user.tasks.create(name: “tasks 2”)
(0.0ms) begin transaction
SQL (0.3ms) INSERT INTO “tasks” (“created_at”, “name”, “updated_at”, “user_id”) VALUES (?, ?, ?, ?) [[“created_at”, Sat, 03 Jan 2015 17:01:36 UTC +00:00], [“name”, “tasks 2”], [“updated_at”, Sat, 03 Jan 2015 17:01:36 UTC +00:00], [“user_id”, 3]]
(536.4ms) commit transaction
=> #<Task id: 3, user_id: 3, name: “tasks 2”, created_at: “2015-01-03 17:01:36”, updated_at: “2015-01-03 17:01:36”>
2.1.1 :016 > task.user == user
User Load (0.2ms) SELECT “users”.* FROM “users” WHERE “users”.”id” = ? ORDER BY “users”.”id” ASC LIMIT 1 [[“id”, 3]]
=> true

En este caso el resultado es el mismo obviamente ya que que hemos realizado las mismas operaciones, pero fíjate en el detalle de la devolución de dicha comparativa, primero nos devuelve una consulta SQL a la base de datos y después nos dice true. Por lo que demostramos que con inverse_of activo nos ahorramos una consulta a la base de datos ya que cuando hacemos un task.user en la que disponemos de una relación belongs_to :user, ya está disponible en memoria y por tanto no necesita ninguna consulta adicional, en los casos que entremos por un has_many no tiene efecto alguno, es decir, tendremos una consulta la la base de datos.

El resultado de probar esto mismo en la versión 4.2.0 y la 4.0.0 es el mismo sin variación alguna.

Tercer caso: relaciones has_many through

Ahora en este otro caso disponemos de los modelos de User, Interest y la intermedia InterestUser:

class User < ActiveRecord::Base

      has_many :interest_users, inverse_of: :user
      has_many :interests, through: :interest_users
      has_many :tasks, inverse_of: :user

      accepts_nested_attributes_for :interest_users, allow_destroy: true
      accepts_nested_attributes_for :tasks

      validates_presence_of :email

end

class InterestUser < ActiveRecord::Base
       belongs_to :user, inverse_of: :interest_users
       belongs_to :interest, inverse_of: :interest_users
end

class Interest < ActiveRecord::Base
      validates_presence_of :name
      has_many :interest_users, inverse_of: :interest
      has_many :users, through: :interest_users
end

Vemos que ocurre si ejecutamos desde nuestra consola con un Rails 4.0.0 sin tener activo inverse_of:

2.1.1 :009 > user = User.create(name: ‘User 55’, email: ‘user55@example.com’)
(0.1ms) begin transaction
SQL (0.3ms) INSERT INTO “users” (“created_at”, “email”, “name”, “updated_at”) VALUES (?, ?, ?, ?) [[“created_at”, Sat, 03 Jan 2015 17:57:30 UTC +00:00], [“email”, “user55@example.com”], [“name”, “User 55”], [“updated_at”, Sat, 03 Jan 2015 17:57:30 UTC +00:00]]
(496.1ms) commit transaction
=> #<User id: 10, name: “User 55”, email: “user55@example.com”, created_at: “2015-01-03 17:57:30”, updated_at: “2015-01-03 17:57:30”>
2.1.1 :010 > interest = user.interests.build(name: ‘my interest’)
=> #<Interest id: nil, name: “my interest”, created_at: nil, updated_at: nil>
2.1.1 :011 > interest.save!
(0.1ms) begin transaction
SQL (0.3ms) INSERT INTO “interests” (“created_at”, “name”, “updated_at”) VALUES (?, ?, ?) [[“created_at”, Sat, 03 Jan 2015 17:57:39 UTC +00:00], [“name”, “my interest”], [“updated_at”, Sat, 03 Jan 2015 17:57:39 UTC +00:00]]
(725.6ms) commit transaction
=> true

En este punto podemos decir que ya tenemos creados nuestros los objetos de User y el objeto Interest, ahora verificamos las relaciones para ver los resultados, es decir haremos un interest.user y la intermedia con interest.interest_user:

2.1.1 :012 > interest.users
User Load (0.2ms) SELECT “users”.* FROM “users” INNER JOIN “interest_users” ON “users”.”id” = “interest_users”.”user_id” WHERE “interest_users”.”interest_id” = ? [[“interest_id”, 2]]
=> #<ActiveRecord::Associations::CollectionProxy []>
2.1.1 :013 > interest.interest_users
InterestUser Load (0.2ms) SELECT “interest_users”.* FROM “interest_users” WHERE “interest_users”.”interest_id” = ? [[“interest_id”, 2]]
=> #<ActiveRecord::Associations::CollectionProxy []>

Este resultado me descuadra, he perdido los datos o mejor no se han guardado y me pregunto ¿por qué ahora si todo ha dado bien no tengo lo que estoy preguntando? y me hace sospechar que el inverse_of debe tomar partido en el asunto, pero aún no tengo una respuesta. En el repaso de los pasos de primero hago un create de User, después sobre el objeto user construyo la relación con un build y cuando termino le hago una save! y el commit de la transacción es correcto, pero la relación se la ha dejado, ¿no lo ha hecho?, pero entonces ¿qué ha pasado?…..tengo que hacer una parada e investigarlo y buscando por Internet he encontrado poca cosa aclaratoria y a la conclusión a la que he llegado es que si no estoy utilizando inverse_of en una relación de la tabla intermedia InterestUser con los belongs_to, no los crea de una forma automática,  ya que Rails no sabe las inverse_of en este caso del modelo InterestUser para que pueda finalmente crearlas de una forma automática.

Veamos finalmente si es efectivamente esto y ejecutamos desde nuestra consola nuevamente con un Rails 4.0.0 y teniendo activo inverse_of, ¿lo creará automáticamente?:

2.1.1 :018 > user = User.create(name: ‘User 55’, email: ‘user55@example.com’)
(0.1ms) begin transaction
SQL (0.3ms) INSERT INTO “users” (“created_at”, “email”, “name”, “updated_at”) VALUES (?, ?, ?, ?) [[“created_at”, Sat, 03 Jan 2015 18:02:57 UTC +00:00], [“email”, “user55@example.com”], [“name”, “User 55”], [“updated_at”, Sat, 03 Jan 2015 18:02:57 UTC +00:00]]
(438.8ms) commit transaction
=> #<User id: 11, name: “User 55”, email: “user55@example.com”, created_at: “2015-01-03 18:02:57”, updated_at: “2015-01-03 18:02:57”>
2.1.1 :019 > interest = user.interests.build(name: ‘my interest’)
=> #<Interest id: nil, name: “my interest”, created_at: nil, updated_at: nil>
2.1.1 :020 > interest.save!
(0.1ms) begin transaction
SQL (0.3ms) INSERT INTO “interests” (“created_at”, “name”, “updated_at”) VALUES (?, ?, ?) [[“created_at”, Sat, 03 Jan 2015 18:03:11 UTC +00:00], [“name”, “my interest”], [“updated_at”, Sat, 03 Jan 2015 18:03:11 UTC +00:00]]
SQL (0.2ms) INSERT INTO “interest_users” (“created_at”, “interest_id”, “updated_at”, “user_id”) VALUES (?, ?, ?, ?) [[“created_at”, Sat, 03 Jan 2015 18:03:11 UTC +00:00], [“interest_id”, 3], [“updated_at”, Sat, 03 Jan 2015 18:03:11 UTC +00:00], [“user_id”, 11]]
(494.3ms) commit transaction
=> true
2.1.1 :021 > interest.users
User Load (0.2ms) SELECT “users”.* FROM “users” INNER JOIN “interest_users” ON “users”.”id” = “interest_users”.”user_id” WHERE “interest_users”.”interest_id” = ? [[“interest_id”, 3]]
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 11, name: “User 55”, email: “user55@example.com”, created_at: “2015-01-03 18:02:57”, updated_at: “2015-01-03 18:02:57”>]>
2.1.1 :022 > interest.interest_users
=> #<ActiveRecord::Associations::CollectionProxy [#<InterestUser id: 1, user_id: 11, interest_id: 3, created_at: “2015-01-03 18:03:11”, updated_at: “2015-01-03 18:03:11”>]>

Genial, ya lo tenemos!! Las pruebas hechas para la versión de Rails 4.2.0 han sido iguales con los mismo resultados.

Update: Hablando de esto con Alfonso @joshka20, me ha recordado una prueba más que me ha faltado describir y es que en vez de build en la construcción interest = user.interests.build(name: ‘my interest’) vamos a utilizar un cretae en ambas versiones de Rails 4.0 y la 4.2 y el resultado es que no necesitamos un inverse_of si utilizamos un create en user.interest.create(name: ‘my interest’). Creo que es un dato relevante e importante a considerar.

Recuerda estos aspectos tratados en este post y evitarás tener algunos problemas en el día a día. Si has hecho alguna aproximación en este sentido y tienes datos nuevos que aportar o corrección al respecto, me gustaría escucharte!!

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s