Go to Hackademy website

Le méconnu inverse_of

Martin Catty

Posté par Martin Catty dans les catégories back

Rails: le méconnu inverse_of

Active record: le patron de conception

Active record, avant d’être le nom d’une célèbre gem inclue dans rails est le nom d’un design pattern, permettant de faire du mapping objet - relationnel.

C’est à dire qu’il vous permet de manipuler sous forme d’objets vos données stockées dans une base de données.

Optimiser la communication entre les objets et la base de donnée

La gem ActiveRecord vous offre un maximum de confort en mettant à disposition toutes les méthodes pour gérer les opérations basiques de CRUD mais aussi les associations entre tables.

Imaginons deux class product et category:

  class Category < ActiveRecord::Base
    has_many :products
  end
  class Product < ActiveRecord::Base
    belongs_to :category
  end

Regardons quels sont les appels effectués à la base lorsque nous parcourons les objets:

  irb(main):008:0> c2 = Category.first(:include => :products)
    Category Load (0.1ms)  SELECT "categories".* FROM "categories" LIMIT 1
    Product Load (0.2ms)  SELECT "products".* FROM "products" WHERE "products"."category_id" IN (1)
  => #<Category id: 1, name: "Téléphone", created_at: "2012-01-27 11:21:59", updated_at: "2012-01-27 11:21:59">
  irb(main):009:0> c3 = c2.products.first.category
    Category Load (0.2ms)  SELECT "categories".* FROM "categories" WHERE "categories"."id" = 1 LIMIT 1
  => #<Category id: 1, name: "Téléphone", created_at: "2012-01-27 11:21:59", updated_at: "2012-01-27 11:21:59">
  irb(main):010:0> c3.object_id == c2.object_id
  => false

ActiveRecord n’est pas suffisamment malin pour comprendre que nous sommes en fait sur la même catégorie quand nous demandons la catégorie d’un produit qui fait parti de la liste des produits de cette même catégorie.

Le rôle d’inverse_of

Redéfinissons nos relations pour utiliser inverse_of:

  class Category < ActiveRecord::Base
    has_many :products, :inverse_of => :category
  end
  class Product < ActiveRecord::Base
    belongs_to :category, :inverse_of => :products
  end
  irb(main):004:0> c2 = Category.first(:include => :products)
    Category Load (0.2ms)  SELECT "categories".* FROM "categories" LIMIT 1
    Product Load (0.2ms)  SELECT "products".* FROM "products" WHERE "products"."category_id" IN (1)
  => #<Category id: 1, name: "Téléphone", created_at: "2012-01-27 11:21:59", updated_at: "2012-01-27 11:21:59">
  irb(main):005:0> c3 = c2.products.first.category
  => #<Category id: 1, name: "Téléphone", created_at: "2012-01-27 11:21:59", updated_at: "2012-01-27 11:21:59">
  irb(main):006:0> c3.object_id == c2.object_id
  => true

En explicitant la relation inverse ActiveRecord sait maintenant retrouver ses petits et éviter d’inutiles requêtes SQL.

Les formulaires imbriqués

Imaginons que vous souhaitiez pouvoir créer directement une catégorie avec un ou plusieurs produits et que vous produits doivent obligatoirement avoir une catégorie.

  class Category < ActiveRecord::Base
    has_many :products, :inverse_of => :category

    accepts_nested_attributes_for :products
  end
  class Product < ActiveRecord::Base
    belongs_to :category, :inverse_of => :products

    validates_presence_of :category
  end

Sans inverse_of vous risquez d’avoir des soucis à cause de la validation. En effet, le produit ayant une validation sur sa catégorie, il va chercher à la charger.

Sans inverse_of il essayera de la charger depuis la base de donnée, alors qu’elle n’existe pas encore. Avec, il sera capable de récupérer directement l’objet.

Identity map

Très franchement ActiveRecord nous a habitué à un peu plus de magie et d’automatisme pour que l’on n’ait pas envie de définir manuellement tous ses inverse_of.

D’autant plus qu’il semble assez simple de retrouver les associations concordantes et que cette fonctionnalité est présente dans datamapper.

C’est en fait le rôle de la fonction d’identity map introduite par rails 3.1 mais désactivée par défaut.

L’objectif est de n’avoir jamais à recharger un objet depuis la base quand il est déjà en mémoire.

L’activation de la fonctionnalité se fait dans application.rb

  config.active_record.identity_map = true
  irb(main):005:0> ActiveRecord::IdentityMap.use { c = Category.find(1) ; c2 = Category.find(1) }
    Category Load (57.9ms)  SELECT "categories".* FROM "categories" WHERE "categories"."id" = ? LIMIT 1  [["id", 1]]
    Category Loaded  From Identity Map (id: 1)
  => #<Category id: 1, name: "Téléphone", created_at: "2012-01-27 11:21:59", updated_at: "2012-01-27 11:21:59">

L’utilisation se fait dans un bloc et l’usage est encore limité. Par exemple utiliser Category.first, même dans le bloc entrainera tout de même 2 requêtes.

De plus l’activation de la fonction ne permet pas pour le moment de remplacer le inverse_of.

C’est pourtant une fonctionnalité très importante car elle permettra de fortement limiter le nombre d’instanciation d’objets.

Pour autant elle n’est pas simple à mettre en place en conservant un comportement tread-safe : on imagine bien deux morceaux de code entrain de mettre à jour le même objet en mémoire.

En résumé, utilisez le inverse_of dès aujourd’hui si ce n’est pas encore le cas car Rails n’est pas prêt de le faire automatiquement pour vous.

L’équipe Synbioz.

Libres d’être ensemble.

Articles connexes

Une brève histoire d'Elixir et Erlang/OTP

31/01/2019

Je développe depuis plusieurs années en Ruby. Depuis mon arrivée chez Synbioz, j’expérimente en plus avec Elixir de façon assez naturelle. En quoi Elixir est-il différent, me demanderez-vous ? Pour...

Écrire une thread pool en Ruby

10/01/2019

Pouvoir exécuter plusieurs tâches en parallèle, que ce soit dans un script ou une application, peut être vraiment très utile, surtout dans le cas où le traitement de ces tâches peut être très long....

Translation temporelle

31/05/2018

Cette semaine, je me suis essayé à un nouveau format d’article qui se présente sous la forme d’une nouvelle de Science-Fiction. Je tiens en passant à remercier Valentin pour ses illustrations....

Authentifier l'accès à vos ressources avec Dragonfly

11/05/2017

Pour ceux qui ne connaissent pas Dragonfly, c’est une application Rack qui peut être utilisée seule ou via un middleware. Le fait que ce soit une application Rack la rend compatible avec toutes les...