FRESH → the Hive

Fresh from the Hive is Studio Melipone's weblog about visual delights, startup love and much more.

As opposed to whatever SEO stuff you should pay attention to this blog is run in a complete natural non sense. Enjoy!

Why don't you grab our RSS?

 

#rails Join nested associations.

Je vais traiter dans ce post d’un problème rencontré récemment, qu’il est toujours bon de rappeler lorsqu’il est facile à résoudre.
Il s’agit d’un subtil mélange entre des nested_attributes_for, des relations has_and_belongs_to_many, et de manipuler des objets en les liant de la façon la plus découplée possible … afin de pouvoir par exemple supprimer une relation sans pour autant supprimer l’objet “dépendant” de la relation…

Un Project contient des Developer, et je veux pouvoir ajouter, modifier, et enlever autant de developer que je veux de mon project.
Là où ça va se compliquer c’est qu’on veux pouvoir réutiliser un Developer dans plusieurs Project

Mais pour présenter un problème, un bon exemple vaux mieux que tous les discours :

Notre premier modèle :

  1. class Project < ActiveRecord::Base
  2.   has_and_belongs_to_many :developers, :join_table => :projects_developers, :uniq => true
  3.   accepts_nested_attributes_for :developers, :allow_destroy => true
  4. end

Notre deuxième modèle :

  1. class Developer < ActiveRecord::Base
  2.   has_and_belongs_to_many :projects, :join_table => :projects_developers, :uniq => true
  3. end

Nous voilà donc avec nos 2 modèles, que je manipulerais via le formulaire donc voici un extrait de code :

  1. <% form_for @project do |f| %>
  2.     …
  3.     …
  4.     <% for @developer in @project.developers %>
  5.         <% f.fields_for :developers, @developer do |dev| %>
  6.             …
  7.             …
  8.         <% end %>
  9.     <% end %>
  10. <% end %>

Pour l’ajout et la modification tout va bien, par contre pour la suppression, on remarque que ce n’est pas seulement l’association qui est supprimée, mais également l’objet.
Ce qui pour notre cas pose un énorme problème : On veux pouvoir réutiliser une Developer dans plusieurs Project !

Après quelques recherches on tombe sur une solution assez simple

Afin de résoudre ce petit souci, on va placer un before_save dans le modèle, afin de passer par dessus le comportement normal.
Notre modèle conteneur Project, qui devient donc :

  1. class Project < ActiveRecord::Base
  2.   has_and_belongs_to_many :developers, :join_table => :projects_developers, :uniq => true
  3.   accepts_nested_attributes_for :developers, :allow_destroy => true
  4.  
  5.   def before_save
  6.     remove_marked_developers
  7.   end
  8.  
  9.   def remove_marked_developers
  10.     marked = developers.select { |dev| dev.marked_for_destruction? }
  11.     marked.each { |dev| developers.delete(dev) } #delete from join table only
  12.   end
  13.  
  14. end

Cela va simplement avoir pour effet de supprimer la ligne dans la table de jointure, en évitant de supprimer l’objet lui-même.

Ce problème est répertorié sur le Lighthouse du Ruby on Rails, on verra donc surement très vite un correctif rendant cette petite astuce inutile.

Vous pouvez également trouver de nombreuses informations dans les RailsCasts de Ryan Bates aux épisodes #17, #196 et #197

Mais si vous connaissez une autre solution ou une technique magique, on est preneur ! ;)