RoR : De l'utilisation de has_and_belongs_to_many (un formulaire pour plusieurs tables)
Par Bounga le dimanche, 27 août 2006, 23:31 - Trucs et astuces - Lien permanent
SQL, c'est ce qu'on appel une relation N <=> N. En Ruby on Rails, on décrit cela dans les modèles à l'aide de has_and_belongs_to_many.
Je vais ici tenter de vous expliquer comment mettre en oeuvre une telle relation dans un seul et unique formulaire qui sera capable de gérer tous les aspects liés à un enregistrement en une seule opération.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.
Commentaires
Y a plus rapide que
<%= select_tag("groups", options_for_select(@groups.collect { |group| [group.name, group.id] },
@user.groups.collect { |group| group.id }),
:multiple => true) %>
=> collection_select('user', 'group_id', @groups, 'id', 'name');