Rails : association & inverse sur un même objet
Nous venons de rencontrer un cas bien particulier (mais pas tant que ça finalement) lors d’un récent développement. Nous avons un objet Produit et nous souhaitons l’associer à un autre produit en vue de répondre au besoin "tel produit est associé à tel produit".
Jusque ici rien de compliqué. Cependant si l’on rentre un peu plus dans le détail et dans la demande qui nous a été formulée, on se rend compte que l’on a finalement besoin de : “Si j’associe mon produit A au produit B, est-ce que le produit B sera automatiquement associé au produit A ?” . La réponse est non, mais nous allons faire en sorte d’y répondre favorablement.
On crée nos tables produits et produit_associes, puis les modèles qui vont avec :
# ruby2 / rails4.0
# db/migrate/1...
def change
create_table :produits do |t|
t.string :intitule
t.string :description
# etc.
t.timestamps
end
end
# db/migrate/2...
def change
create_table :produit_associes do |t|
t.integer :pdt_origine_id
t.integer :pdt_associe_id
t.timestamps
end
end
# app/models/produit.rb
class Produit < ActiveRecord::Base
has_many :associes_pdts, foreign_key: "pdt_origine_id", class_name: "ProduitAssocie"
has_many :associes, through: :associes_pdts, source: "pdt_associe", dependent: :destroy
end
# app/models/produit_associe.rb
class ProduitAssocie < ActiveRecord::Base
belongs_to :pdt_associe, foreign_key: "pdt_associe_id", class_name: "Produit"
end
Si l’on teste déjà cela dans une console (avec des données en base) on obtient ceci :
p1 = Produit.find(1)
=> #<Produit id: 1, intitule: "Mon produit 1", description: "Hello world ..">
p2 = Produit.find(2)
=> #<Produit id: 2, intitule: "2d produit", description: "Bla bla ..">
p1.associes << p2
p1.associes
=> #<ActiveRecord::Associations::CollectionProxy [#<Produit id: 2, intitule: "2d produit", description: "Bla bla ..">]>
p2.associes
=> #<ActiveRecord::Associations::CollectionProxy []>
On remarque donc que nous avons pu associer le produit 2 à notre produit 1, que lorsque nous interrogeons p1.associes il nous retourne bien le produit 2. Dans le cas inverse (p2.associes) le tableau est vide, et nous voulons qu’automatiquement p2 match avec p1 sur cette association inverse.
Pour cela nous allons modifier notre modèle produit_associe et lui indiquer d’enregistrer (ou supprimer) l’inverse de l’association créée/détruite :
# app/models/produit_associe.rb
class ProduitAssocie < ActiveRecord::Base
belongs_to :pdt_associe, foreign_key: "pdt_associe_id", class_name: "Produit"
after_create :create_inverse, unless: :has_inverse?
after_destroy :destroy_inverses, if: :has_inverse?
def create_inverse
self.class.create(inverse_produit_assoc)
end
def destroy_inverses
inverses.destroy_all
end
def has_inverse?
self.class.exists?(inverse_produit_assoc)
end
def inverses
self.class.where(inverse_produit_assoc)
end
def inverse_produit_assoc
{ pdt_origine_id: pdt_associe_id, pdt_associe_id: pdt_origine_id }
end
end
Si l’on reteste en console on obtient désormais :
p1.associes
=> #<ActiveRecord::Associations::CollectionProxy [#<Produit id: 2, intitule: "2d produit", description: "Bla bla ..">]>
p2.associes
=> #<ActiveRecord::Associations::CollectionProxy [#<Produit id: 1, intitule: "Mon produit 1", description: "Hello world ..">]>
Dès lors que l’on associe un produit à un autre, on crée l’association inverse automatiquement. L’ajout de “dependent: :destroy” dans le modèle Produit nous permettra de faire la même chose lorsque l’on supprimera l’une des deux associations existantes.
Originally published at Sois-net.