Prenons par exemple le cas d'un système de gestion des utilisateurs et des groupes. Un utilisateur peut appartenir à un ou plusieurs groupes et un groupe peut avoir un ou plusieurs utilisateurs. On a donc bien une relation N <=> N (many to many).

Commençons donc par créer notre projet rails à l'aide de la commande : rails -d sqlite3 groups et déplaçons nous dans le répertoire nouvellement créé : cd groups.

Nous allons créer les modèles correspondant à nos tables groups et users : ./script/generate model User et ./script/generate model Group.

Nous avons maintenant les fichiers de migration db/migrate/001_create_users.rb et db/migrate/002_create_groups.rb qui vont nous permettre de créer les schémas pour nos tables users et groups :

class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.column :name, :string
    end
  end
  def self.down
    drop_table :users
  end
end
class CreateGroups < ActiveRecord::Migration
  def self.up
    create_table :groups do |t|
      t.column :name, :string
    end
  end
  def self.down
    drop_table :groups
  end
end

Nous avons également besoin d'une table de jointure pour lier les données des tables users et groups entre elles. Nous créons donc un fichier de migration à l'aide de la commande ./script/generation migration GroupsUsers que nous remplirons de la façon suivante :

class GroupsUsers < ActiveRecord::Migration
  def self.up
    create_table :groups_users, :id => false do |t|
        t.column :user_id, :integer
        t.column :group_id, :integer
    end
  end
  def self.down
    drop_table :groups_users
  end
end

Vous devez absolument préciser :id => false lors de la création de la table, sans cette précision les enregistrements dans la table ne pourront pas se faire correctement. Les tables de jointure en RoR ne doivent jamais posséder de champs id.

Maintenant que le schéma est en place, créons la base de données à l'aide de la commande rake migrate.

Il faut maintenant préciser à nos modèles qu'ils sont liés entre eux. Dans le fichier app/model/group.rb, on aura donc :

class Group < ActiveRecord::Base
  has_and_belongs_to_many :users
end

et dans le fichier app/model/user.rb, nous aurons :

class User < ActiveRecord::Base
  has_and_belongs_to_many :groups
end

Nos modèles sont maintenant correctement liés ce qui nous donne accès à toutes les méthodes magiques de Rails. Nous allons utiliser le scaffolding pour nous simplifier la tâche de création des vues et controlleurs. On exécute donc les commandes ./script/generate scaffold Groups et ./script/generate scaffold Users.

Nous pouvons maintenant passer aux choses sérieuses, c'est à dire l'intégration de la gestion des groupes au sein même du formulaire d'édition des utilisateurs. Dans notre fichier app/controllers/users_controller.rb nous allons modifier la méthode edit pour avoir accès à la liste des groupes disponibles. Elle devra ressembler au code suivant :

 def edit
   @user = User.find(params[:id])
   @groups = Group.find(:all)
 end

Modifions maintenant le partiel _form.rhtml qui est utilisé par les vues chargées de l'édition :

<%= error_messages_for 'user' %>
<p><label for="user_name">Name</label><br/>
<%= text_field 'user', 'name'  %></p>
<%= select_tag("groups", options_for_select(@groups.collect { |group| [group.name, group.id] },
                                                                        @user.groups.collect { |group| group.id }),
                                                                        :multiple => true) %>

Nous avons donc simplement ajouté la ligne select_tag qui nous permet de créer une liste déroulante contenant tous les groupes disponibles (@groups), en prenant soin de pré-sélectionner les groupes auxquels l'utilisateur en cours d'édition appartient (@user.groups.collect { |group| group.id }) et en rendant la selection multiple possible.

Nous n'avons plus maintenant qu'à modifier la méthode de création d'un utilisateur ainsi que la méthode de mise à jour dans notre controller user_controller.rb :

 def create
   @user = User.new(params[:user])
   if @user.save
     @user.group_ids = params[:groups]
     flash[:notice] = 'User was successfully created.'
     redirect_to :action => 'list'
   else
     render :action => 'new'
   end
 end
 def update
   @user = User.find(params[:id])
   @user.group_ids = params[:groups]
   if @user.update_attributes(params[:user])
     flash[:notice] = 'User was successfully updated.'
     redirect_to :action => 'show', :id => @user
   else
     render :action => 'edit'
   end
 end

La seule ligne ajoutée dans les deux méthodes est @user.group_ids = params[:groups] qui permet de remplacer les associations actuelles de la table de jointure pour l'utilisateur en cours d'édition par celles sélectionnées dans notre liste déroulante.

On voit donc avec cet exemple qu'en très peu de lignes de code (6 exactement), on arrive à mettre en place des formulaires liés à plusieurs tables qui nous permettent d'effectuer des modifications de façon intuitive et surtout beaucoup plus rapidement.