Sortir des conventions dans Rails - retour d'expérience.
Par jblanche le mardi 8 avril 2008, 23:01 - geeks - Lien permanent
Tout d'abord, sachez qu'il ne faut jamais outrepasser les conventions de Rails sans une excellente raison.
Toutefois, il arrive que l'on ai une excellente raison, le cas présenté dans ce billet sera celui (tiré d'une application réelle) d'une base de données assez mal conçue où les noms de tables sont tous au singulier et où, comble du massacre, les id d'une des tables sont sous forme de chaine de caractères.
Bien sur, cette base de données est utilisée par 18 autres applications dans la société et vous ne pouvez en aucun cas en modifier la structure (sinon ce serait trop facile).
Problème N°1 : Les noms de tables.
Notre premier problème est donc que Rails considère par défaut qu'un nom de modèle doit-être un singulier et que le nom de la table correspondante doit être un pluriel.
Nous pourrions donc ajouter à l'un de nos modèles la ligne suivante :
self.pluralize_table_names = false
Mais selon moi, cette propriété étant commune à tous nos modèles, il n'est pas logique de la placer dans l'un d'entre eux. Nous allons donc spécifier cette propriété générale à tous les instances d'AciveRecord ailleurs.
Pour cela, la version 2 de Rails nous offre un dossier config/initializers où placer tous nos scripts à exécuter au démarrage de l'application après les configurations par défaut, c'est donc l'endroit parfait pour rajouter un script "do_not_pluralize.rb" avec pour contenu :
module ActiveRecord
class Base
self.pluralize_table_names = false
end
end
Et voilà, nous venons d'overrider le paramètre pluralize_table_names d'ActiveRecord::Base et désormais, tout est clean.
Problème N°2 : Des chaînes de caractères comme id, drôle d'idée...
Les id de notre table "comment" (nom d'emprunt) sont donc des strings. Cela ne pose aucun problème à activeRecord qui saura évidemment faire un find("a12.bcd.3456789") si "a12.bcd.3456789" est la valeur du champ id de notre enregistrement.
Là où intervient le problème, c'est que comme vous êtes quelqu'un d'attentif, vous savez qu'avec Rails2, REST et les named routes ont enfin droit leurs heures de gloire.
Or, cela implique que l'URL d'accès à votre enregistrement soit de la forme :
/comments pour la liste des commentaires.
/comments/1 pour le commentaire 1
/comments/last pour une méthode supplémentaire last que vous auriez définie et qui vous ramènerait probablement les derniers commentaires.
Or avec notre id, l'url est de la forme "/comments/a12.bcd.3456789".
Rails pense alors que 'a12.bcd.3456789' est une méthode à appeler mais il ne la trouvera évidement jamais.
Edit du 17/04/08
Avant de vous montrer la solution que j'ai utilisé dans mon cas je vais suite au commentaire de Nicolas, de boldr.fr vous montrer une solution qui fonctionnera dans la majorité des cas (mais pas le mien.)
Afin de dire à rails d'utiliser autre chose qu'un id numérique dans les url de la forme "/comments/1" il existe une option des ressources appellée member_path. En modifiant donc votre fichier "config/routes.rb", vous pouvez indiqué à Rails d'utiliser l'id de votre table pour avoir des url de la forme "/comments/a12bcd3456789" comprises par rails.
Il suffit donc de remplacer :
map.resources :comments
par
map.resources :comments, :member_path => '/comments/:id'
Grâce à celà, vous éviterez beaucoup d'ennuis mais malheureusement, la solution ne fonctionne pas si l'id nouveau contient des '.' car rails se sert de ce caractère pour séparer la partie relative à l'url de l'action de la partie relative au format de retour (html, xml, js...).
La solution dans mon cas devait donc venir d'autre part...
Dans le cas que j'ai eu à d'étudier, le salut est venu du fait que la suite de nombre suffixant l'id (ici '3456789') était unique, j'ai donc décider d'utiliser celle-ci comme paramètre de l'URL.
Pour récupérer ce nouvel id (que j'appellerai numeric_id), j'ai donc écrit la méthode suivante :
def numeric_id
self.id.split('.').last.to_i
end
Il ne me restait plus qu'à indiquer à rails d'utiliser ce paramètre dans les URL grâce à la méthode to_param [1] (toujours dans mon modèle).
def to_param
"#{self.numeric_id}"
end
Grâce à cela, j'ai donc des url "/comments/3456789" que Rails analisera sans problème.
Dernières modifications, il faut indiquer aux méthodes agissant sur un seul enregistrement dans le controller (show, edit, update, destroy et celle que vous pourriez créer) d'utiliser notre nouvel identifiant numérique plutôt que celui par défaut.
Pour cela je rajoute une méthode find_by_numeric_id dans notre modèle :
def self.find_by_numeric_id(nid) Comment.find(:first,:conditions => ['id LIKE ? ', '%'+nid.to_s]) endEt je modifie les méthodes précédemment citées pour utiliser notre find, ma méthode show devient donc :
# GET /comments/1
# GET /comments/1.xml
def show
@comment = Comment.find_by_numeric_id(params[:id])
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => @comment }
end
end
Et voilà vous avez désormais une base seine sur laquelle faire reposer la suite de votre développement.
Je me répète mais ce genre de modification ne doit être utilisé que si vous ne pouvez pas faire autrement
[1]Attention, si vous prévoyez d'utiliser la méthode to_param dans le cas d'un model Active_ressource, allez faire un tour ici afin de résoudre un bug de rails (corrigé dans la version SVN) qui fait que activeressource utilise parfois l'id au lieu de to_param.
Commentaires
Pour l'id tu peux passer par la redéfinition du member_path (map.resources :users, :member_path => '/users/:permalink') et ton permalink est ton id que tu nommes comme tu veux (peut-être même que tu peux mettre :id, mais j'ai jamais utilisé member_path).
Merci pour ce conseil, j'ai fouiné un peu dans la documentation et effectivement celà fonctionne bien.
Malheureusement dans le cas qui me concerne, les id contiennent des '.', qui est censé être le caractère séparateur entre l'url et le format donc ca ne fonctionne pas mais pour d'autre cas, c'est un excellent moyen de s'en sortir (je vais modifier mon billet en fonction dans l'apres-midi).
Si j'avais eu acces a ce genre de tutorial pour l'implementation de mon projet, j'aurais facilement gagne 1 semaine de travail... Maintenant j'apprecie la clarte et je suis persuade que ca aidera ces pauvres bougres qui se lancent tout joyeux et plein d'inoncence dans une application Rails avec une base de donnees "assez mal conçue"... Je maintiens que quand meme, Rails est pas super comprehensif a ce niveau.... le salaud !
Merci et continue comme ca
C'est vrai qu'on a un peu galéré mais finalement je trouve que rails à montré qu'il était capable sans d'énormes modifications de modifier le comportement conventionnel pour un comportement "personnel" (tout cela tient finalement en même pas 10 lignes de "vrai" code).
Et puis tu oublie de préciser ton émerveillement quand tu as voulu un webservice et que tu t'es rendu compte que tu l'avais déjà
Fil des commentaires de ce billet