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 => :coutsendclass Cout < ActiveRecord::Base
belongs_to
:materiau belongs_to
:descriptionendclass Description < ActiveRecord::Base
has_many
:unites has_many
:couts, :dependent => true has_many
:materiaus, :through => :coutsendMaintenant, 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")
Il ne reste plus qu'à faire le tableau.
<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).
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).