Article_right_arrow Input & textarea : attention aux couleurs ! Article_left_arrow Numériser une image avec The Gimp sous Linux

Ruby on Rails - le modèle has_and_belongs_to_many

01 février 2010      Commentaires (4)    

Rails 2.3.2 && Ruby 1.8.7

Imaginons une petite application tout bête pour enregistrer des projets et les membres qui y participent dans une base de données. Les membres peuvent être sur différents projets et les projets peuvent avoir plusieurs membres. Pour simplifier le tutoriel, on n'enregistre que le nom du projet et le pseudo du membre.

Ce petit tuto sans prétention explique comment procéder pas à pas. Il implique tout de même que vous sachiez créer un projet et configurer le fichier database.yml dans le répertoire config.

Echaffaudage des deux modèles

On va utiliser l'échafaudage pour créer... pratiquement tout ce dont on a besoin : les modèles, les controllers, les vues et la base des formulaires.

ruby script/generate scaffold Projet nom:string

puis

ruby script/generate scaffold Membre pseudo:string

Attention :

Le nom du modèle doit commencer par une majuscule et être un mot au singulier. Tous les types des propriétés (nom et pseudo) commencent par une minuscule. La création de la table, plus tard, ne fonctionnera pas s'il y a des majuscules qui trainent.

Modification des modèles

On va ensuite connecter les deux modèles. On a dit que chaque projet peut avoir plusieurs membres et chaque membre peut participer à plusieurs projets. C'est le parfait exemple d'une relation has_and_belongs_to_many.

On modifie donc les deux modèles (membre.rb et projet.rb dans app/model) :

class Project < ActiveRecord::Base
  has_and_belongs_to_many :membres
end

class Member < ActiveRecord::Base
  has_and_belongs_to_many :projets
end

Attention : notez que le nom du modèle est au pluriel.

Création de la table de jointure

Ce genre de relation exige ce qu'on appelle une table de jointure. C'est une nouvelle table dans la base de données qui enregistre chaque couple. A chaque fois qu'un nouveau membre s'inscrit à un projet, une nouvelle ligne est créée dans cette table.

Rails possède une convention de nommage pour les tables de jointure : il faut appeler cette table du nom des modèles concernés (pour nous : membre et projet) au pluriel, dans l'odre alphétique, séparés par un underscore.

Notre table s'appellera donc membres_projets

Pour créer la migration qui créera la table dans la base :

ruby script/generate migration create_membres_projets

On retrouve ce fichier dans le répertoire db/migration. Il faut le construire de cette manière :

class CreateMembresProjets < ActiveRecord::Migration
  def self.up
    create_table :membres_projets, :id => false do |t|
      t.integer :membre_id
      t.integer :projet_id
    end
  end

  def self.down
    drop_table : membres_projets
  end
end

Attention :

:id => false est très important. S'il n'y est pas, la table ne peut pas fonctionner. En effet, une table de jointure n'a pas d'index incrémenté automatiquement à chaque nouvelle entrée et n'a donc pas d'id.

Enregistrez le tout. Il est temps de construire les tables :

rake db:migrate

Pour la suite des évènements, testez le tout et créez quelques membres et quelques projets.

ruby script/server

Puis rendez vous sur http://localhost:3000/membres et http://localhost:3000/projets

Modification du formulaire

Il va ensuite falloir inscrire les membres aux projets par le biais des formulaires.

Dans le controller des membres, on se met la liste des projets à disposition pour les vues, et ce dans l'ordre alphétique - tant qu'à faire - pour les retrouver plus facilement.

# Dans app/controllers/membres_controller.rb
def edit
  @membre = Membre.find(params[:id])
  @projets = Projet.find(:all, :order => "nom")
end

Puis, dans la vue (app/views/membres/edit.erb.html), on ajoute la formule magique pour créer la balise select à choix multiple :

<h1>Editing membre</h1>

<% form_for(@membre) do |f| %>
  <%= f.error_messages %>

  <p>
    <%= f.label :pseudo %><br />
    <%= f.text_field :pseudo %>
  </p>
  <p>Projets :</p><br />
  <%= collection_select(:membre, :projet_ids, @projets, :id, :nom, {:prompt => " "}, { :multiple => true })
  <p>
    <%= f.submit 'Update' %>
  </p>
<% end %>

<%= link_to 'Show', @membre %> |
<%= link_to 'Back', membres_path %>

Le paramètre prompt est facultatif et permet simplement d'avoir une ligne de texte (ou vide) sans valeur associée.

Le paramètre multiple est celui qui permet de faire plusieurs choix dans la liste. Si on l'omet, on se retrouve avec un menu déroulant.

Modification des aperçus

Maintenant, il faut afficher tout ça. On doit pouvoir consulter qui sont les membres inscrits à un projet et quels sont les projets auxquels un membre est inscrit.

Pour les membres (app/views/membres/show.erb.html) :

<p>
  <b>Pseudo:</b>
  <%=h @membre.pseudo %>
</p>
<p>Inscrits aux projets suivants</p>
<ul>
<% for projet in @membre.projets %>
  <li><%=h projet.nom %></li>
<% end %>
</ul>

<%= link_to 'Edit', edit_membre_path(@membre) %> |
<%= link_to 'Back', membres_path %>

Et pour les projets (app/views/projets/show.erb.html) :

<p>
  <b>Nom:</b>
  <%=h @projet.nom %>
</p>
<p>Membres inscrits :</p>
<ul>
<% for membre in @projet.membres %>
  <li><%=h membre.pseudo %></li>
<% end %>
</ul>

<%= link_to 'Edit', edit_projet_path(@projet) %> |
<%= link_to 'Back', projets_path %>

Et voilà !

Qu'est-ce que vous en pensez ?

Comment_arrow Stéphane, le 21 June 2011

Bonjour,

le tuto est bien écrit, mais il ne fonctionne pas...

Je vois le menu déroulant contenant les choix possibles mais impossible de sauver. Il me semble que le tuto n'indique pas comment mettre à jour la table de jointure ?? (ou bien est-ce automatique)

Voici l'erreur, incompréhensible et sans numéro de ligne :
NoMethodError in ProductsController#update
undefined method `reject' for "2":String

De plus, le paramètre :multiple=>true ne génère qu'un simple menu déroulant sous Chrome 6. Est-ce normal ?

Merci pour les infos

Comment_arrow Lily, le 21 June 2011

Je ne peux pas vous aider sans voir votre code. Je confirme que le tuto ci-dessus fonctionne, c'est encore la méthode que j'utilise.
Pour répondre à vos questions :
1) les tables de jointures sont en effet automatiquement mises à jour grâce aux conventions de nommage (d'où l'importance de nommer la table de jointure correctement : nom des deux tables au pluriel par ordre alphabétique)
2) le paramètre :multiple => true fonctionne sous tous les navigateurs courants (IE, FF, Safari, Opera et Chrome chez moi, mais je ne teste pas sous toutes les versions de chaque navigateur. Néanmoins, c'est un paramètre qui se traduit par un simple code HTML.
3) NoMethodError in ProductsController#update semble laisser entendre qu'il y a un problème dans votre controller Products. En général, les méthodes "create" et "update" n'ont pas besoin d'être modifiées, en tout cas pas dans le cadre du tuto ci-dessus. Vous pourriez aussi vous assurer que les routes sont correctes si vous n'avez pas généré vos controlleurs avec un scaffold (si vous avez généré un controller uniquement, les routes ne sont pas configurées automatiquement pour vous)
La plupart du temps, lorsqu'il y a une typo dans le code d'une relation habm, l'enregistrement ne s'effectue pas, tout simplement.
Bon courage.

Comment_arrow TotorZ, le 22 September 2011

Dans la migration, remplacer :
drop_table : membres_projets
par
drop_table :membres_projets
C'est pas grand chose mais ça évite l'erreur au rollback
Merci pour le tuto ;)

Comment_arrow TotorZ, le 22 September 2011

C'est encore moi ! C'est cette migration qui me chagrine.
Il manque les index, et aussi la nouvelle syntaxe Rails.

Peut être que certains préfèreront donc :
class CreateMembresProjects < ActiveRecord::Migration
def change
create_table :membres_projects, :id => false do |t|
t.references :membre
t.references :project
t.timestamps
end
add_index :membres_projects, [:membre_id, :project_id]
end
end

Laissez un p'tit mot !

Votre nom :

Votre e-mail :

Votre adresse e-mail ne sera pas affichée sur le site.




Article_right_arrow Input & textarea : attention aux couleurs ! Article_left_arrow Numériser une image avec The Gimp sous Linux