On The Rails Again

Associations polymorphiques

Posté par Nima le 29 avril 2012

Pré-requis :
Ce que nous allons voir :
  • créer une association polymorphique ;
  • utiliser une association polymorphique comme ressource imbriquée ;
  • créer une ressource polymorphique de façon générique.

Qu'est-ce que c'est ?

L'association polymorphique est une association qui permet à un modèle d'appartenir à plusieurs autres modèles. Il ne dépend donc pas d'un unique modèle. Par exemple, un modèle Comment peut appartenir à des Articles, mais aussi à des Events. De cette façon, vous n'avez pas besoin de dupliquer des modèles ayant des informations similaires.

Un exemple concret

Illustrons le problème par un exemple. Supposons que nous avons trois modèles : Article, Event et Photo. Le but est de créer un unique modèle Comment pour nos trois types de ressources.

Voilà un schéma représentatif de ce que l'on veut obtenir :

Représentation schématique de la relation polymorphique

Création du modèle

Comme toujours, des conventions sont à respecter pour que tout se passe comme prévu. Le modèle Comment n'étant plus lié à un modèle spécifique, il faut lui spécifier un champ qui réfèrera à l'ID de la ressource auquel il appartient ainsi que le type de ce dernier. Voilà comment générer le modèle Comment 

$ rails g model Comment content:text commentable_id:integer commentable_type:string

Migration

Pour récapituler, il faut stoquer l'ID de l'objet auquel le commentaire appartient et le type de ce dernier. Côté migration, deux façons d'écrire :

# db/migrate/201101249412_create_comments.rb
class CreateComments < ActiveRecord::Migration
  def change
    create_table :comments do |t|
      t.string :content
      t.integer :commentable_id
      t.string  :commentable_type
      t.timestamps
    end
  end
end

Ou :

# db/migrate/201101249412_create_comments.rb
class CreateComments < ActiveRecord::Migration
  def change
    create_table :comments do |t|
      t.string :content
      t.integer :commentable, :polymorphic => true
      t.timestamps
    end
  end
end

Associations dans les modèles

Il faut maintenant déclarer dans les modèles concernés les associations de la façon suivante :

# app/models/comment.rb
class Comment < ActiveRecord::Base
  belongs_to :commentable, :polymorphic => true
  ...
# app/models/event.rb
class Event < ActiveRecord::Base
  has_many :comments, :as => :commentable
  ...
# app/models/photo.rb
class Photo < ActiveRecord::Base
  has_many :comments, :as => :commentable
  ...
# app/models/article.rb
class Article < ActiveRecord::Base
  has_many :comments, :as => :commentable
  ...

Création d'un commentaire

Si on veut maintenant créer un commentaire et l'associer à un article, on peut simplement faire 

@article.comments.create(:content => 'Mon contenu')

Aller plus loin

Ressources imbriquées

Si on veut généraliser et que l'on veut accéder par exemple à tous les commentaires d'un article par l'URL /articles/1/comments il faut utiliser les ressources imbriquées et pour cela modifier les routes.

# config/routes.rb
resources :model_name do
  resources :comments
end

En faisant ça pour chaque modèle, la route /model/1/comments redirigera alors vers l'action index de CommentsController. Et c'est dans cette méthode qu'il faut, en fonction du type de la ressource, afficher les commentaires.

Récupérer la ressource

Ryan Bates nous propose dans son RailsCast une méthode pour récupérer la ressource concernée.

# app/controllers/comments_controller.rb
private
def find_commentable
  params.each do |name, value|
    # Regex correspondant à la forme model_id
    if name =~ /(.+)_id$/
      # $1 correspond au nom du modèle
      return $1.classify.constantize.find(value) 
    end
  end
  nil # Retourne nil si rien n'a été trouvé
end

La méthode ci-dessus parcours tous les paramètres envoyés par le client et cherche un paramètre se terminant par _id. Si nous étions à l'url /articles/1/comments, nous aurions par exemple article_id avec comme valeur 1.

Si la méthode trouve une correspondance dans les paramètres, elle appelle la méthode classify sur le nom du modèle. La méthode classify transforme les chaînes de caractère s'apparentant à des noms de tables en des chaînes de caractère pouvant correspondre à des noms de classe. Par exemple 

"egg_and_hams".classify # => "EggAndHam"
"posts".classify        # => "Post"

Puis, la méthode constantize est appelé pour essayer de trouver une constante correspondante. «Article» deviendra alors la constante correspondant à la classe Article.

Enfin, la méthode find est appelé pour récupérer l'objet en question.

Afficher et créer un commentaire

De cette façon, on peut maintenant afficher dans notre vue index de comments, tous les commentaires de l'objet concerné.

# app/controllers/comments_controller.rb
def index
  @commentable = find_commentable
  @comments    = @commentable.comments
end

Et dans la vue :

# app/views/comments/index.html.erb
<h1>Liste des commentaires</h1>
<ul id="comments">
  <% @comments.each do |comment| %>
    <li><%= comment.content %></li>
  <% end %>
</ul>
<h2>Nouveau commentaire</h2>
<% form_for [@commentable, Comment.new] do |form| %>
  <ol class="formList">
    <li>
      <%= form.label :content %>
      <%= form.text_area :content, :rows => 5 %>
    </li>
    <li><%= submit_tag "Add comment" %></li>
  </ol>
<% end %>

Et enfin, pour l'action create du contrôleur 

# app/controllers/comments_controller.rb
def create
  @commentable = find_commentable
  @comment = @commentable.comments.build(params[:comment])

Voilà pour ce qui est des associations polymorphiques !

Références