Mercredi 24 mai 2006
Quand on bosse sur RoR, on a deux façon d'utiliser les liens de N vers N . Une confrontation avait plusieurs joueurs et un joueur avait plusieurs participants.
J'avais parlé de la déclaration has_and_belongs_to_many (poétiquement surnommée hbtm) . Cette déclaration donne le droit d'utiliser @joueur.confrontations et @confrontation.joueurs pour accéder rapidement aux objets liés à l'autre. Niveau productivité c'est excellent.
Toutefois, si par hasard on a besoin de rajouter une valeur ou deux entre les deux objets, hbtm devient moins pertinent. Encore heureux, nos chers développeurs ont pensé à nous. Ils ont créé la déclaration has_many :through => . Tout est bien expliqué et assorti d'exemples dans le blog de has_many :through . Je vais vous présenter l'intérêt de cette déclaration, puis coller un peu du code que j'ai mis pour implémenter tout ça.
Son principe est de créer une table d'objets tiers avec les identifiants des deux (ou plus) objets liés. Des colonnes supplémentaires permettent de rajouter des spécificités à ces liens.
Dans mon jeu, puisque le but est de faire un toolkit simple d'utilisation pour l'administrateur, je devais mettre en oeuvre un moyen rapide de rajouter une unité ou un type de terrain. Chaque unité ayant un coût de déplacement différent par type de terrain, il faut également pouvoir aisément modifier ce coût.
J'ai donc créé trois bases : matériaus (non ce n'est pas une faute d'orthographe, rails ne connait pas les pluriels français) descriptions et couts.
create table materiaus (
id int not null auto_increment,
nom varchar(100) not null,
couleur_html char(6) not null,
bonus_defense int not null,
primary key (id)
);
create table descriptions (
id int not null auto_increment,
nom varchar(100) not null,
distance_attaque int not null,
max_vie int not null,
max_degats int not null,
price int not null,
primary key (id)
);
create table couts (
id int not null auto_increment,
description_id int not null,
materiau_id int not null,
move_on int not null,
primary key (id)
);
Tout est dans le "move_on" cette valeur là correspond pile poil à mon désir. On appelle cela "l'association riche". J'aurais aussi pu mettre un "default 16", mes unités ayant au maximum 15points de déplacement, celà voudrait dire qu'oublier de remplir la case empécherait tout déplacement. Mais bon, j'ai opté pour ne rien mettre par défaut car de toute manière l'admin devra changer 95% des coûts. Autant tout simplifier du côté serveur et code, l'utilisateur changera tous les coûts de déplacement mais au passage il les créera (donc pas la peind d'automatiser cela).
La déclaration niveau classes donne :
class Materiau < ActiveRecord::Base
has_many :dames
has_many :couts, :dependent => true
has_many :descriptions, :through => :couts
end
class Cout < ActiveRecord::Base
belongs_to :materiau
belongs_to :description
end
class Description < ActiveRecord::Base
has_many :unites
has_many :couts, :dependent => true
has_many :materiaus, :through => :couts
end
Maintenant, reste à implémenter tout ça correctement. L'utilisateur doit rapidement pouvoir ajouter une description d'unité ou un matériau de terrain et ne pas oublier de remplir les cases coûts quand on rajoute l'un ou l'autre. Et bien, rien de plus simple ! Regardez bien :
Je fais un scaffold sur chaque classe Materiau Description et Cout. Pour les deux premiers, rien est à changer, si ce n'est changer la redirection aprés la création d'une description/d'un matériau, on renvoie sur les couts pour ne pas les oublier !
Un simple :
Pour les coûts, seuls des changements mineurs sont a effectués : la présentation ordonnée des coûts en tableau à double entrée. Dans la méthode "list" du controlleur correspondant au coûts on rajoute la recherche des descriptions et des matériaus.
@materiaus = Materiau.find(:all, :order => "nom")
@descriptions = Description.find(:all, :order => "nom")
Il ne reste plus qu'à faire le tableau.
Bon, là j'ai un peu fais de la merde car j'ai rajouté des éléments applicatifs dans la vue, et en prime il y a beaucoup d'accès à la base de donnée. J'ai dérogé à ces règles de codage. Je n'ai pas encore trop eu l'idée magique pour changer ça. Le problème vient du fait que rails ne gère pas les clés primaires doubles.
Peut-être un find_couts(:all) bien ordonné devrait faire l'affaire, mais dans ce cas il faudra alourdir le code de création d'unité et de matériaus en créant par défaut les coûts manquant pour qu'il "n'y ait pas de trous".
Enfin, j'y travaillerai mais puisque rajouter une unité ou un type de matériau n'est pas une action que l'on fait à tour de bras, cela ne charge pas trop le serveur.
Dans le cas où le coût n'existe pas, on passe en paramètre la description et le materiau, et on les rajoute à la main dans la déclaration de la méthode "create" :
Bien entendu, il reste une chose à faire : virer tout les trucs inutiles comme la méthode "destroy" dans l'échaffaudage créé sur la classe coûts.
Voilà pour cet article. D'ailleurs je remercie aussi les gens du channel irc #rubyonrails.fr sur freenode qui m'ont bien aidé à combler le "through" (bide inside).
J'avais parlé de la déclaration has_and_belongs_to_many (poétiquement surnommée hbtm) . Cette déclaration donne le droit d'utiliser @joueur.confrontations et @confrontation.joueurs pour accéder rapidement aux objets liés à l'autre. Niveau productivité c'est excellent.
Toutefois, si par hasard on a besoin de rajouter une valeur ou deux entre les deux objets, hbtm devient moins pertinent. Encore heureux, nos chers développeurs ont pensé à nous. Ils ont créé la déclaration has_many :through => . Tout est bien expliqué et assorti d'exemples dans le blog de has_many :through . Je vais vous présenter l'intérêt de cette déclaration, puis coller un peu du code que j'ai mis pour implémenter tout ça.
Son principe est de créer une table d'objets tiers avec les identifiants des deux (ou plus) objets liés. Des colonnes supplémentaires permettent de rajouter des spécificités à ces liens.
Dans mon jeu, puisque le but est de faire un toolkit simple d'utilisation pour l'administrateur, je devais mettre en oeuvre un moyen rapide de rajouter une unité ou un type de terrain. Chaque unité ayant un coût de déplacement différent par type de terrain, il faut également pouvoir aisément modifier ce coût.
J'ai donc créé trois bases : matériaus (non ce n'est pas une faute d'orthographe, rails ne connait pas les pluriels français) descriptions et couts.
create table materiaus (
id int not null auto_increment,
nom varchar(100) not null,
couleur_html char(6) not null,
bonus_defense int not null,
primary key (id)
);
create table descriptions (
id int not null auto_increment,
nom varchar(100) not null,
distance_attaque int not null,
max_vie int not null,
max_degats int not null,
price int not null,
primary key (id)
);
create table couts (
id int not null auto_increment,
description_id int not null,
materiau_id int not null,
move_on int not null,
primary key (id)
);
Tout est dans le "move_on" cette valeur là correspond pile poil à mon désir. On appelle cela "l'association riche". J'aurais aussi pu mettre un "default 16", mes unités ayant au maximum 15points de déplacement, celà voudrait dire qu'oublier de remplir la case empécherait tout déplacement. Mais bon, j'ai opté pour ne rien mettre par défaut car de toute manière l'admin devra changer 95% des coûts. Autant tout simplifier du côté serveur et code, l'utilisateur changera tous les coûts de déplacement mais au passage il les créera (donc pas la peind d'automatiser cela).
La déclaration niveau classes donne :
class Materiau < ActiveRecord::Base
has_many :dames
has_many :couts, :dependent => true
has_many :descriptions, :through => :couts
end
class Cout < ActiveRecord::Base
belongs_to :materiau
belongs_to :description
end
class Description < ActiveRecord::Base
has_many :unites
has_many :couts, :dependent => true
has_many :materiaus, :through => :couts
end
Maintenant, reste à implémenter tout ça correctement. L'utilisateur doit rapidement pouvoir ajouter une description d'unité ou un matériau de terrain et ne pas oublier de remplir les cases coûts quand on rajoute l'un ou l'autre. Et bien, rien de plus simple ! Regardez bien :
Je fais un scaffold sur chaque classe Materiau Description et Cout. Pour les deux premiers, rien est à changer, si ce n'est changer la redirection aprés la création d'une description/d'un matériau, on renvoie sur les couts pour ne pas les oublier !
Un simple :
redirect_to :controller => 'stationservice' ,:action => 'list'
dans la méthode create, en cas de création réussie fait l'affaire.Pour les coûts, seuls des changements mineurs sont a effectués : la présentation ordonnée des coûts en tableau à double entrée. Dans la méthode "list" du controlleur correspondant au coûts on rajoute la recherche des descriptions et des matériaus.
@materiaus = Materiau.find(:all, :order => "nom")
@descriptions = Description.find(:all, :order => "nom")
<table>
<!-- on commence par faire une ligne avec les noms des matériaus -->
<tr><td> </td>
<% for materiau in @materiaus -%>
<td><%= materiau.nom -%></td>
<% end -%>
</tr>
<!-- les autres lignes commencent par le nom de l'unité suivi de toutes les valeurs qui vont bien -->
<% for description in @descriptions -%>
<tr><td><%= description.nom -%></td>
<% for materiau in @materiaus -%>
<td>
<% if cout=Cout.find_by_materiau_id_and_description_id(materiau.id,description.id) -%>
<%= link_to cout.move_on , :action => 'edit' , :id => cout %>
<% else -%>
<!-- si la valeur n'est pas présente, on met un lien pour l'inventer -->
<%= link_to "nouveau" , :action => 'new' , :description_id => description.id , :materiau_id => materiau.id %>
<% end %>
</td>
<% end -%>
</tr>
<% end -%>
</table>
<!-- on commence par faire une ligne avec les noms des matériaus -->
<tr><td> </td>
<% for materiau in @materiaus -%>
<td><%= materiau.nom -%></td>
<% end -%>
</tr>
<!-- les autres lignes commencent par le nom de l'unité suivi de toutes les valeurs qui vont bien -->
<% for description in @descriptions -%>
<tr><td><%= description.nom -%></td>
<% for materiau in @materiaus -%>
<td>
<% if cout=Cout.find_by_materiau_id_and_description_id(materiau.id,description.id) -%>
<%= link_to cout.move_on , :action => 'edit' , :id => cout %>
<% else -%>
<!-- si la valeur n'est pas présente, on met un lien pour l'inventer -->
<%= link_to "nouveau" , :action => 'new' , :description_id => description.id , :materiau_id => materiau.id %>
<% end %>
</td>
<% end -%>
</tr>
<% end -%>
</table>
Bon, là j'ai un peu fais de la merde car j'ai rajouté des éléments applicatifs dans la vue, et en prime il y a beaucoup d'accès à la base de donnée. J'ai dérogé à ces règles de codage. Je n'ai pas encore trop eu l'idée magique pour changer ça. Le problème vient du fait que rails ne gère pas les clés primaires doubles.
Peut-être un find_couts(:all) bien ordonné devrait faire l'affaire, mais dans ce cas il faudra alourdir le code de création d'unité et de matériaus en créant par défaut les coûts manquant pour qu'il "n'y ait pas de trous".
Enfin, j'y travaillerai mais puisque rajouter une unité ou un type de matériau n'est pas une action que l'on fait à tour de bras, cela ne charge pas trop le serveur.
Dans le cas où le coût n'existe pas, on passe en paramètre la description et le materiau, et on les rajoute à la main dans la déclaration de la méthode "create" :
@cout = Cout.new(params[:cout])
@cout.materiau_id = params[:materiau_id]
@cout.description_id = params[:description_id]
Ceci n'es pas un choix délibéré de faire des tâches de si bas niveau (je comprends qu'en tant que railers on n'y soit pas habitué), c'est que la déclaration has_many :through ne permet pas la "proxy sélection" (ou l'opérateur << qu'on peut utiliser pour la conseur hbtm).@cout.materiau_id = params[:materiau_id]
@cout.description_id = params[:description_id]
Bien entendu, il reste une chose à faire : virer tout les trucs inutiles comme la méthode "destroy" dans l'échaffaudage créé sur la classe coûts.
Voilà pour cet article. D'ailleurs je remercie aussi les gens du channel irc #rubyonrails.fr sur freenode qui m'ont bien aidé à combler le "through" (bide inside).


