Rails 2.1, ça roule maintenant ! – Le tutoriel complet – 2ème partie

On y est, voici la seconde partie de mon tutoriel sur Rails 2.1. Commencez par la première partie si vous ne l’avez pas déjà lue.

NdT : Il est très difficile, et à mon avis inutile – mais comme je suis notoirement fainéant, vous pouvez penser que je ne suis pas le plus objectif pour dire ça – de traduire les noms des concepts techniques de Rails (et de l’informatique en général) parce que leur traduction littérale en français n’a aucun sens et ne renseigne pas plus le débutant sur la fonction que l’expression anglaise qui est constamment utilisée et qu’elle laisse le geek perplexe, se demandant si c’est nouveau ou si c’est seulement mal traduit… Donc je m’en tiendrai aux expressions anglaises puisque ce sont elles que vous trouverez dans les docs, les autres tutoriels, les webcasts et les autres ressources sur Rails qui fourmillent sur le net.

Les Finders et les Named Scopes

Voici une autre idée qui a été réalisée, ce sont les has_finder.

Pour comprendre ce que sont les named_scopes, nous devons comprendre, avant tout, comment la méthode #find d’ActiveRecord::Base fonctionne. L’idée de base est qu’elle reçoit un tableau associatif d’options. On les connait déjà, :conditions, :order, :limit, etc.

Les gems comme has_finder ont ammené l’idée de fusionner tous ces tableaux associatifs en un seul. Donc si l’on a 2 tableaux associatifs de finders, chacun avec sa propre option :conditions, il pourrait sembler naturel de pouvoir les combiner en une seule requête cohérente. C’est le but de la nouvelle fonctionnalité named_scope de Rails 2.1.

Pour la présenter, commençons par ajouter quelques données bidon dans nos fichiers de fixtures (les jeux de données.) Premièrement dans test/fixtures/posts.yml :

post_one:
  title: Hello World
  body: MyText
  created_at: <%= (Time.now - 1.week).to_s(:db) %>
  updated_at: <%= (Time.now - 1.week).to_s(:db) %>

post_two:
  title: Hello Brazil
  body: MyText
  created_at: <%= (Time.now - 2.weeks).to_s(:db) %>
  updated_at: <%= (Time.now - 2.weeks).to_s(:db) %>

post_three:
 title: Hello RailsConf
 body: MyText
 created_at: <%= (Time.now).to_s(:db) %>
 updated_at: <%= (Time.now).to_s(:db) %>

post_four:
 title: Rails 2.1
 body:
 created_at: <%= (Time.now).to_s(:db) %>
 updated_at: <%= (Time.now).to_s(:db) %>

post_five:
 title: RailsCasts
 body:
 created_at: <%= (Time.now - 1.week).to_s(:db) %>
 updated_at: <%= (Time.now - 1.week).to_s(:db) %>

post_five:
 title: AkitaOnRails
 body:
 created_at: <%= (Time.now).to_s(:db) %>
 updated_at: <%= (Time.now).to_s(:db) %>

Et ensuite dans test/fixtures/comments.yml :

DEFAULTS: &DEFAULTS
  created_at: <%= Time.now.to_s(:db) %>
  updated_at: <%= Time.now.to_s(:db) %>

comment_one:
  post: post_one
  body: MyText1
  <<: *DEFAULTS

comment_two:
  post: post_one
  body: MyText2
  <<: *DEFAULTS

comment_three:
  post: post_one
  body: MyText3
  <<: *DEFAULTS

comment_four:
  post: post_one
  body: MyText4
  <<: *DEFAULTS

comment_five:
  post: post_two
  body: MyText5
  <<: *DEFAULTS

Enfin, dans le fichier test/fixtures/users.yml où le plugin restful_authentication a déjà créé quelques données de test qui servent à peupler le modèle User, nous devons seulement corriger un petit bug, une option de formattage oubliée, autour de la ligne 9 (il manque le paramètre :db à la méthode #to_s)

remember_token_expires_at: <%= 1.days.from_now.to_s :db %>

Si vous avez fait tout ça correctement, depuis la ligne de commande vous devriez pouvoir lancer :

rake db:fixtures:load

Maintenant, améliorons notre modèle Post en éditant le fichier app/models/post.rb :

class Post < ActiveRecord::Base
  has_many :comments
  named_scope :empty_body, :conditions => "(body = '' or body is null)"
  named_scope :this_week, :conditions => ["created_at > ?", 1.week.ago.to_s(:db)]
  named_scope :recent, lambda { |*args| {:conditions =>
    ["created_at > ?", (args.first || 2.weeks.ago).to_s(:db)]} }
end

La première modification est assez directe. Pour comprendre ce qu’elle fait, ouvrons un ‘script/console’ (Et pendant qu’on y est, ouvrons aussi un autre terminal et suivons le fichier log/development.log avec la commande tail -f log/development.log)

>> Post.empty_body
=> ...
>> Post.empty_body.count
=> 2

Voici ce qu’il devrait se passer dans log/development.log :

SELECT * FROM `posts` WHERE ((body = '' OR body IS NULL)) 
 
SELECT COUNT(*) AS count_all FROM `posts` WHERE ((body = '' OR body IS NULL))

C’est la même chose que si nous avions appelé ces méthodes dans une vieille version de Rails :

>> Post.find(:all, :conditions => "(body = '' or body is null)")
=> ...
>> Post.count(:conditions => "(body = '' or body is null)")
=> 2

Ce n’est pas tout. Essayons un autre exemple :

>> Post.empty_body.this_week
=> ...
>> Post.empty_body.this_week.count
=> 2

Ces méthodes génèrent les requêtes suivantes :

SELECT * FROM `posts` WHERE ((created_at > '2008-05-18 09:03:19') AND ((body = '' OR body IS NULL))) 
 
SELECT COUNT(*) AS count_all FROM `posts` WHERE ((created_at > '2008-05-18 09:03:19') AND ((body = '' OR body IS NULL)))

Voilà l’objectif des ‘named scopes’ : nommer explicitement des ‘sous-ensembles’ significatifs et permettre de les chaîner simplement pour leur donner des significations plus complexes.

Même lorsque l’on appelle ‘count’, Rails ne fait pas n’importe quoi, comme par exemple lire tous les objets concernés, les stocker dans une liste et renvoyer la taille de la liste.

Vous pouvez voir qu’il est plutôt subtil dans la manière de fusionner les conditions. Vous pourrez aussi en chaîner autant que vous le voulez – bien entendu, vous devrez faire quelques essais, mais vous devrez aussi faire attention à ne pas sur-utiliser cette fonctionnalité.

Il y a quelques pièges à éviter qui ont déjà été expliqués par Ryan Bates, je vais quand même vous en faire part :

  • Le named_scope ‘this_week’ est problématique. Regardez la commande ‘1.week.ago’. Comprenez qu’elle ne sera évaluée qu’une seule fois pendant toute la durée de vie de cette classe. Ce qui signifie que jusqu’à ce que vous redémarriez la machine virtuelle Ruby, cette commande ne sera exécutée qu’une et une seule fois. Alors, si aujourd’hui nous sommes le “18 mai 2008”, cette valeur sera stockée dans la définition de la classe et disons que vous avez été capable de maintenir votre application Rails en ligne pendant des mois. Après cette période, la valeur stockée sera toujours le “18 mai 2008”.
  • Le named_scope ‘recent’ montre comment faire ça correctement. Si vous avez besoin de paramètres dynamiques pour exécuter la requête, vous devez les mettre dans un bloc. Attention, il ne peut y avoir qu’une seule clé :conditions dans le tableau associatif des options. Nous devons placer la paire :conditions entre les accolades de l’expression lambda, mais entre ses propres accolades pour pouvoir être différenciée du tableau associatif lui-même (les accolades sont optionnelles si elles ne provoquent pas d’ambiguité.)

    Il y a une astuce avec l’étoile (*args) pour permettre d’avoir une valeur par ‘défaut’ dans le cas où le développeur ne fourni pas de date. De cette façon nous pouvons l’utiliser aussi comme ça :

    >> Post.recent.count
    => 4
    >> Post.recent(2.days.ago).count
    => 3

    Qui génère alors ce SQL :

    SELECT COUNT(*) AS count_all FROM `posts` WHERE (created_at > '2008-05-11 09:17:49') 
     
    SELECT COUNT(*) AS count_all FROM `posts` WHERE (created_at > '2008-05-23 09:17:51')

    Les named_scopes ouvrent pas mal de nouvelles manières créatives et sensées d’organiser des requêtes complexes en parties plus petites et plus cohésives. Il y a probablement d’autres pièges à leur utilisation mais nous les trouverons bien assez vite. Essayez-les autant que vous le pouvez parce qu’ils sont vraiment très utiles.

    Optimisation du chargement zélé (eager loading)

    Pendant qu’on parle des finders, il y a eu un autre changement assez peu médiatisé. Regardez-ça :

    Post.find(:all, :include => [:comments])

    Jusqu’à Rails 2.0 nous aurions vu ce genre de chose dans le journal des requêtes SQL :

    SELECT `posts`.`id` AS t0_r0, `posts`.`title` AS t0_r1, `posts`.`body` AS t0_r2, `comments`.`id` AS t1_r0, `comments`.`body` AS t1_r1 FROM `posts` LEFT OUTER JOIN `comments` ON comments.post_id = posts.id

    Mais maintenant, dans Rails 2.1, la même commande produira des requêtes SQL différentes. En fait, 2 au lieu d’une seule. “Et en quoi serait-ce une amélioration ?” Regardons les requêtes SQL en question :

    SELECT `posts`.`id`, `posts`.`title`, `posts`.`body` FROM `posts` 
     
    SELECT `comments`.`id`, `comments`.`body` FROM `comments` WHERE (`comments`.post_id IN (130049073,226779025,269986261,921194568,972244995))

    Le mot clé :include pour le chargement zélé (eager loading) a été implémenté pour s’attaquer au redoutable problème des 1+N. Avec les associations, lorsque vous chargez l’objet parent et que vous commencez à lire chaque objet de l’association un par un, va se poser le problème du 1+N. Si votre objet parent a 100 enfants, vous allez exécuter 101 requêtes ce qui n’est pas bon. Une manière d’optimiser celà est de joindre le tout en utilisant une clause OUTER JOIN dans le SQL, comme ça, les parents et les enfants sont chargés en une seule fois par une seule requête.

    Ca semblait être une bonne idée et en fait, c’en est une. Mais dans certaines situations, le monstrueux OUTER JOIN peut devenir plus lent que plusieurs requêtes plus petites. Beaucoup de discussions ont eu lieu sur le sujet et vous pouvez obtenir plus de détails en lisant les tickets 9640, 9497, 9560, L109.

    Voilà l’idée : il est généralement plus efficace de découper la monstrueuse jointure en petites requêtes, comme dans l’exemple précédent. Ca peut permettre d’éviter la surcharge liée au problème du produit cartésien. Pour les non initiés, regardons l’exécution de la version OUTER JOIN de la requête :

    mysql> SELECT `posts`.`id` AS t0_r0, `posts`.`title` AS t0_r1, `posts`.`body` AS t0_r2, `comments`.`id` AS t1_r0, `comments`.`body` AS t1_r1 FROM `posts` LEFT OUTER JOIN `comments` ON comments.post_id = posts.id ;
    +-----------+-----------------+--------+-----------+---------+
    | t0_r0     | t0_r1           | t0_r2  | t1_r0     | t1_r1   |
    +-----------+-----------------+--------+-----------+---------+
    | 130049073 | Hello RailsConf | MyText |      NULL | NULL    |
    | 226779025 | Hello Brazil    | MyText | 816076421 | MyText5 |
    | 269986261 | Hello World     | MyText |  61594165 | MyText3 |
    | 269986261 | Hello World     | MyText | 734198955 | MyText1 |
    | 269986261 | Hello World     | MyText | 765025994 | MyText4 |
    | 269986261 | Hello World     | MyText | 777406191 | MyText2 |
    | 921194568 | Rails 2.1       | NULL   |      NULL | NULL    |
    | 972244995 | AkitaOnRails    | NULL   |      NULL | NULL    |
    +-----------+-----------------+--------+-----------+---------+
    8 rows in set (0.00 sec)

    Faites attention à ceci : est-ce que vous avez vu les doublons des trois premières colonnes (de t0_r0 à t0_r2) ? Elles viennent des colonnes du modèle Post, les autres étant les colonnes correspondant aux commentaires de chacun des post. Notez que le post “Hello World” a été lu 4 fois. C’est comme ça que fonctionne cette jointure : les lignes parentes sont répétées pour chacun de leurs enfants. Ce post en particulier à 4 commentaires, alors il est répété 4 fois.

    Le problème est que ceci va impacter Rails assez fortement parce qu’il va devoir faire avec pas mal de petits objets à courte durée de vie. La douleur est surtout ressentie du côté de Rails, pas vraiment du côté de MySQL. Maintenant, comparons avec les petites requêtes :

    mysql> SELECT `posts`.`id`, `posts`.`title`, `posts`.`body` FROM `posts`;
    +-----------+-----------------+--------+
    | id        | title           | body   |
    +-----------+-----------------+--------+
    | 130049073 | Hello RailsConf | MyText |
    | 226779025 | Hello Brazil    | MyText |
    | 269986261 | Hello World     | MyText |
    | 921194568 | Rails 2.1       | NULL   |
    | 972244995 | AkitaOnRails    | NULL   |
    +-----------+-----------------+--------+
    5 rows in set (0.00 sec)
     
    mysql> SELECT `comments`.`id`, `comments`.`body` FROM `comments` WHERE (`comments`.post_id IN (130049073,226779025,269986261,921194568,972244995));
    +-----------+---------+
    | id        | body    |
    +-----------+---------+
    |  61594165 | MyText3 |
    | 734198955 | MyText1 |
    | 765025994 | MyText4 |
    | 777406191 | MyText2 |
    | 816076421 | MyText5 |
    +-----------+---------+
    5 rows in set (0.00 sec)

    En fait, j’ai triché un peu, j’ai enlevé les champs created_at et updated_at manuellement des requêtes ci-dessus pour que vous comprenniez mieux ce qui se passe. Voilà ce que nous avons : l’ensemble de données des posts, séparé et non doublonné et celui des commentaires dont le nombre est identique à celui de la requête précédente. Plus l’ensemble de données est long et complexe, plus c’est important parce que Rails doit gérer plus d’objets. Allouer et libérer plusieurs centaines ou milliers de petits objets dupliqués n’est jamais une bonne chose.

    Mais cette nouvelle fonctionnalité est élégante. Disons que vous vouliez quelque chose comme ça :

    >> Post.find(:all, :include => [:comments], :conditions => ["comments.created_at > ?", 1.week.ago.to_s(:db)])

    Dans Rails 2.1, il comprendra qu’il y a une condition de filtrage sur la table “comments” alors il ne coupera pas la requête, à la place, il préférera générer l’ancienne version avec la jointure externe :

    SELECT `posts`.`id` AS t0_r0, `posts`.`title` AS t0_r1, `posts`.`body` AS t0_r2, `posts`.`created_at` AS t0_r3, `posts`.`updated_at` AS t0_r4, `comments`.`id` AS t1_r0, `comments`.`post_id` AS t1_r1, `comments`.`body` AS t1_r2, `comments`.`created_at` AS t1_r3, `comments`.`updated_at` AS t1_r4 FROM `posts` LEFT OUTER JOIN `comments` ON comments.post_id = posts.id WHERE (comments.created_at > '2008-05-18 18:06:34')

    De cette manière les jointures embarquées, les conditions et toutes les autres opérations sur les jointures de tables devraient continuer à marcher correctement. Dans l’ensemble ça devrait même accélérer vos requêtes. Plusieurs personnes ont déjà rapporté qu’en recevant plus de requêtes individuelles, MySQL semblait recevoir plus de CPU d’un coup. Faites vos exercies et vos tests de stress, analysez et voyez ce qu’il se passe.

    Mises à jour partielles et objets altérés (Dirty Objects)

    Dans les anciens temps, si vous aviez voulu mettre à jour seulement un attribut, vous auriez fait ceci :

    p = Post.find(:first)
    p.title = "New Title"
    p.save

    Voyons quelle tête a le vieux SQL qui été généré :

    UPDATE `posts` SET `title` = 'New Title', `body` = "MyText", `created_at` = "2008-05-25 14:59:21", `updated_at` = '2008-05-25 18:14:55' WHERE `id` = 130049073

    Vous voyez le ‘problème’ ? Il réécrit chacun de tous les attributs du modèle, même s’il n’ont pas changé d’un poil. Ca a deux effets pratiques :

  • Augmenter la charge de la base de données en cas de mises à jour massives
  • Garantir que le modèle reste cohérent (souvenez-vous de ça, nous y reviendrons !)

    Pour résoudre le premier problème une nouvelle fonctionnalité a été ajoutée. Regardons à quoi ressemble le SQL généré par Rails 2.1 :

    UPDATE `posts` SET `title` = 'New Title', `updated_at` = '2008-05-25 18:14:55' WHERE `id` = 130049073

    Vous voyez la différence ? Il ne met à jour que les attributs qui ont réellement changé ainsi que la colonne d’horodatage updated_at, et laisse les autres tranquilles. Sans rien toucher, c’est comme ça que se comporteront vos mises à jour. Nous avons même quelques nouvelles méthodes pour ‘demander’ au modèle ce qu’il se passe avant d’envoyer réellement les données dans la base de données :

    >> p = Post.find(:first)
    => ...
    >> p.title = "Another Title"
    => "Another Title"
    >> p.changed?
    => true
    >> p.changed
    => ["title"]
    >> p.changes
    => {"title"=>["New Title", "Another Title"]}
    >> p.title_changed?
    => true
    >> p.title_was
    => "New Title"
    >> p.title_change
    => ["New Title", "Another Title"]
    >> p.title_will_change!
    => "Another Title"

    Je pense que ce que fait chaque méthode doit vous paraître évident. Il y a maintenant un module ActiveRecord::Dirty qui a été ajouté à la classe ActiveRecord::Base qui offre des possibilités d’interroger le modèle lui même (#changed?, #changes) ou d’interroger chaque attribut individuellement simplement en ajoutant les suffixes _changed?, _was et ainsi de suite.

    Tout ceci semble bien sympa, mais souvenez vous : nous n’avons traité que la première puce de ma liste. Il reste un deuxième problème : la cohérence des données.

    Le problème advient lorsqu’on considère que deux utilisateurs peuvent être en train de mettre à jour le même modèle au même moment. Dans l’ancienne version ça pouvait aussi se produire et la stratégie était : “le dernier qui met à jour gagne.” Ce n’est peut-être pas la meilleure solution mais au moins on est assurés que tout ce qui est attribué dans le modèle est correct du point de vue d’au moins un des utilisateurs.

    Avec cette nouvelle fonctionnalité, l’utilisateur A change un attribut, l’utilisateur B en change deux autres du même enregistrement et là nous avons potentiellement 3 colonnes incohérentes fusionnées dans la même ligne. Ca peut vite devenir incontrôlable.

    Le problème de la concurrence dans les bases de données n’est pas nouveau. On connait au moins deux trucs pour le règler, le premier est Le Verrouillage Pessimiste, où le premier utilisateur pose un verrou qui interdit aux autres utilisateurs de l’éditer en même temps. Mais dans une application Web, ce n’est évidemment pas recommandé. La deuxième option c’est Le Verrouillage Optimiste.

    Pour l’activer il faut ajouter une colonne nommée ‘lock_version’ de type entier dont la valeur initiale par défaut est ‘0’ puis :

  • Les utilisateurs A et B chargent le même enregistrement.
  • A termine son travail et met à jour l’enregistrement.
  • La colonne ‘lock_version’ est incrémentée.
  • B termine lui aussi et tente de mettre à jour l’enregistrement. L’opération va échouer parce que la commande SQL générée va chercher à mettre à jour l’enregistrement dont le champ ‘lock_version’ vaut ‘0’ mais il a déjà été incrémenté et il vaut ‘1’.

    De cette manière nous avons réellement bloqué les mises à jour indésirables tout en donnant à l’utilisateur B une chance de relire l’enregistrement contenant les modifications de A et de réévaluer ses propres modifications.

    C’est une technique à utiliser avec discernement, uniquement lorsqu’elle est nécessaire. Ne commencez pas à ajouter des colonnes ‘lock_version’ dans toutes les tables comme des dingues. Autre chose, si pour de bonnes raisons personnelles vous n’aimiez pas ces Mises à Jour Partielles, vous pourriez les désactiver dans le fichier environment.rb comme ceci :

    ActiveRecord::Base.partial_updates = false # the default is true

    Il y a eu d’autres améliorations de performances, lisez Nimble. Mais aussi Ryan Bates et Ryan Daigle à ce sujet.

    D’autres gadgets pour ActiveRecord

    Pour les exemples suivants, lançons script/console pour une autre nouvelle fonctionnalité bien pratique :

    >> Post.first # 1
    => ...
    >> Post.last  # 2
    => ...
    >> Post.empty_body.first       # 3
    => ...
    >> Post.empty_body.recent.last # 4
    => ...
    >> Post.all            # 5
    => ...
    >> Post.empty_body     # 6
    => ...
    >> Post.empty_body.all # 7
    => ...

    ActiveRecord a 3 nouveaux raccourcis, #first, #last et #all, qui évitent de taper find(:first), find(:last) ou find(:all). La bonne nouvelle : ils marchent aussi avec les named_scopes ! Regardons le SQL généré pour comprendre ce qu’ils font :

    // 1
    SELECT * FROM `posts` LIMIT 1
    // 2
    SELECT * FROM `posts` ORDER BY posts.id DESC LIMIT 1
    // 3
    SELECT * FROM `posts` WHERE ((body = '' OR body IS NULL)) LIMIT 1
    // 4
    SELECT * FROM `posts` WHERE ((created_at > '2008-05-11 19:55:33') AND ((body = '' OR body IS NULL))) ORDER BY posts.id DESC LIMIT 1
    // 5
    SELECT * FROM `posts`
    // 6
    SELECT * FROM `posts` WHERE ((body = '' OR body IS NULL))
    // 7
    SELECT * FROM `posts` WHERE ((body = '' OR body IS NULL))

    J’ai numéroté pour vous, à la fois les appels de méthodes Rails et les requêtes SQL générées ci-dessus pour vous permettre de suivre. C’est assez clair. La nouveauté ici c’est l’option :last. En fait elle trie les résultats en utilisant la clé primaire et ne récupère que le dernier. Faites bien attention à la signification de “dernier”, dans ce cas, il signifie “la plus grande clé primaire” et pas quelque chose comme “le dernier enregistrement mis à jour”. Pour ça, il vaut mieux créer un named_scope ‘last_updated’, par example.

    Autre nouvelle fonctionnalité dans Rails 2.1, le support des associations has_one :through. Je suis désolé de ne pas avoir de bon cas d’utilisation de cette fonctionnalité dans cette démo, mais je vais essayer quelque chose à titre d’exemple pour la tester.

    Commençons par créer deux nouveaux échaffaudages :

    ./script/generate scaffold Company name:string
    ./script/generate scaffold Role

    Puis éditions le fichier db/migrate/20080526004326_create_roles.rb (encore une fois, vos horodatages seront différents) comme ça :

    class CreateRoles < ActiveRecord::Migration
      def self.up
        change_table :roles do |t|
          t.belongs_to :company
        end
      end
     
      def self.down
        change_table :roles do |t|
          t.remove_belongs_to :company
        end
      end
    end

    Le nom de la migration est un peu trompeur parce qu’on a déjà une autre migration pour créer la table ‘roles’. Cette fois nous nous contentons d’ajouter une nouvelle colonne. Créons maintenant les associations :

    # app/models/company.rb
    class Company
      has_many :roles
      has_many :users, :through => :roles
    end
     
    # app/models/role.rb
    class Role
      belongs_to :user
      belongs_to :company
    end
     
    # app/models/user.rb
    class User
      has_many :roles
      has_one :company, :through => :roles, :order => 'created_at DESC'
      ...
    end

    Ouvrons maintenant un autre script/console pour tester ces nouvelles associations :

    >> admin = Role.create(:name => "Administrator")
    => ...
    >> acme = Company.create(:name => "Acme LLC")
    => #
    >> admin.company = acme
    > ...

    Nous avons commencé par créer un nouveau Role et une nouvelle Company puis nous les avons associés.

    >> user = User.find(:first)
    => ...
    >> user.roles << admin
    => ...
    >> user.save
    => true

    Puis nous avons cherché un utilisateur dans notre base de données et nous lui avons associé le nouveau rôle.

    >> admin.reload
    => ...
    >> admin.user
    => ...
    >> admin.company
    => #
    >> user.company
    => #

    Et voilà les choses intéressantes. Premièrement nous avons rechargé le rôle, pour être sûrs. Nous nous assurons qu’il est correctement associé à l’utilisateur et à la nouvelle entreprise.

    Ce qui est intéressant c’est que l’utilisateur est associé directement à une et une seule entreprise par l’intermédiaire de son rôle. Ce à quoi il faut faire attention c’est que l’utilisateur peut avoir plusieurs autres (has_many) rôles. C’est pourquoi la méthode de classe has_one supporte la plupart des options des finders comme :conditions et, comme dans l’exemple précédent, :order. Elle limitera toujours l’ensemble de résulats à 1 en ne récupérant que la première ligne même s’il y en a plusieurs, alors faites attentions aux incohérences de votre modèle métier si vous ne faites pas assez de tests.

    Améliorations d’ActiveResource

    Nous nous amusons avec le projet ‘blog’ que nous avons commencé dans Rolling with Rails 2. Mais il y en avait un autre très petit que nous avions créé dans la partie 2 qui s’appelle ‘blog_remote’. On va maintenant le réutiliser pour découvrir les nouvelles fonctionnalités d’ActiveResource.

    Avant tout, si vous êtes arrivés jusqu’ici, vous devriez vous trouver dans le répertoire du projet ‘blog’. Démarrez le serveur, comme d’habitude avec ’./script/server’. Il devrait se charger et écouter le port 3000 par défaut (si vous n’avez pas changé vos options d’environement.)

    Depuis un autre termina, ouvrez le projet ‘blog_remote’ depuis lequel nous allons tester de nouvelles fonctionnalités de Rails 2.1 :

    $ cd ../blog_remote

    Vous devez suivre les étapes décrites dans la première partie de ce tutoriel pour autoriser ce projet à utiliser la dernière version de Rails installée au niveau système. Sinon, gelez simplement la dernière version du dépôt de Rails ou, éventuellement copiez la version de Rails utilisée par le projet ‘blog’.

    Souvenez-vous que nous avons un app/models/post.rb comme celui-là :

    class Post < ActiveResource::Base
      self.site = 'http://akita:akita@localhost:3000/admin'
    end

    Ca servait à mettre en avant le fait que l’authentification HTTP était inclue dans Rails 2.0. Mais, si vous êtes comme moi, vous n’aimez peut-être pas plus que ça le fait de devoir concaténer le nom d’utilisateur et le mot de passe dans l’URL. Il y a eu quelques solutions pour régler ça, mais dans Rails 2.1, on peut finalement faire comme ça :

    class Post < ActiveResource::Base
      self.site = 'http://localhost:3000/admin'
      self.user = 'akita'
      self.password = 'akita'
      self.timeout = 5 # c'est IMPORTANT!
    end

    Ca devrait rendre les choses un peu plus simple à maintenir. Mais la plus intéressante est l’option de configuration #timeout. C’est même très important parce que le timeout (délai de garde) HTTP par défaut est de 60 secondes. Alors, si pour une raison ou une autre, l’application distante est indisponible, votre application devra attendre jusqu’à une minute avant de lever une exception.

    Vous pouvez imaginer que ce n’est pas très efficace, parce que ca peut potentiellement bloquer toutes vos instances de Rails pour au moins une minute. Testez et analysez vos requêtes distantes, faites en sorte qu’elles soient petites et essayez de trouver le plus petit timeout possible pour votre application. Plus d’informations ici et, bien sûr, nous vous recommandons de faire le moins d’appels distants possible dans un site web public. Mettez en cache ce que vous pouvez au lieu de devoir sortir tout le temps pour la moindre requête.

    Pour vérifier que tout est en ordre et fonctionne (le serveur du projet ‘blog’ doit fonctionner,) depuis ‘blog_remote’, ouvrez ‘script/console’ :

    >> Post.find(:all)
    => [...]

    Vous devriez voir les objets arriver dans le tuyau depuis le projet de blog ‘distant’. Référez-vous à mon tutoriel sur Rails 2.0 pour comprendre comment tout ça a été assemblé.

    Et bien plus encore

    Il y a eu des centaines de changements dans Rails 2.1, et pas mal d’améliorations. Une chose que j’aime en particulier c’est l’optimisation du routage. Oleg Andreev a le scoop. Ca doit être pratiquement transparent pour nous.
    Beaucoup de tests d’ActiveRecord ont été nettoyés par John Barnette, la documentation a été sérieusement revue grâce à Pratik Naik. Les attributs d’ActiveRecord sont maintenant moins coûteux…

    Redemtion in a Blog a aussi suivi le moindre petit changement dans Rails Edge et je vais lui emprunter les morceaux les plus intéressants.

    Un morceau intéressant est l’aliasage des ressources. Vous pouvez maintenant faire ceci dans votre config/routes.rb :

    map.resources :comments, :as => 'comentarios'

    De cette manière http://your_site/comentarios doit se comporter exactement de la même manière que http://your_site/comments. C’est probablement une bonne chose pour les développeurs web qui aiment l’optimisation pour les moteurs de recherche (SEO). Vous trouverez encore plus de trucs sur l’alisage des ressources dans le plugin de Carlos Brando Custom Resource Name qui devrait même fonctionner avec d’anciennes versions de Rails. Et puisqu’on parle de mapping, maintenant vous pouvez aussi faire ça :

    map.new_session :controller => 'sessions', :action => 'new'
    map.root :new_session

    ‘map.root’ accepte maintenant un symbole comme argument pour définir le chemin racine de l’application.

    Ryan Bates s’est plaint (et a écrit un correctif) pour cette utilisation particulière de fields_for :

    <% fields_for "project[task_attributes][]", task do |f| %>
      <%= f.text_field :name, :index => nil %>
      <%= f.hidden_field :id, :index => nil %>
    <% end %>

    Notez le :index => nil qui fait un peu ‘tache’. Maintenant vous pouvez écrire simplement :

    <% fields_for "project[task_attributes][]", task, :index => nil do |f| %>
      <%= f.text_field :name %>
      <%= f.hidden_field :id %>
    <% end %>

    Et nous avons un autre raccourci bien pratique pour les vues. Au lieu de faire ceci :

    <% form_for(:person) do |f| %>
      <%= render :partial => 'form', :locals => { :form => f } %>
    <% end %>

    Vous pouvez utiliser cette version courte :

    <% form_for(:person) do |f| %>
      <%= render :partial => f %>
    <% end %>

    Ce qui est significativement plus lisible et maintenable que la version précédente. Néanmoins, les deux devraient continuer à fonctionner dans Rails 2.1.

    Et puis, pour les projets qui font un usage intensif des téléchargements de fichiers, nous avons maintenant un meilleur moyen de gérer le transfert des gros fichiers qui évite de bloquer complètement une instance de Mongrel :

    send_file '/path/to.png', :x_sendfile => true, :type => 'image/png'

    L’ancienne méthode ‘send_file’ accepte maintenant la configuration :x_sendfile des entêtes. De ce que j’en ai compris, elle demande aux serveurs HTTP comme Apache et LightTPD de gérer le transfert du fichier tout en laissant l’instance de Rails transquile pour gérer les autres requêtes. Cette technique a déjà été expliquée par John Guenin et il était nécessaire d’installer un plugin. Mais dans Rails 2.1 c’est par défaut.

    Conclusion

    Rails 2.1 apporte son lot d’améliorations et d’optimisations et elles sont vraiment bienvenues. Si vous utilisez déjà Rails 2, cette mise à jour devrait fonctionner pratiquement sans rien faire. Encore une fois, si vous utilisez une version Rails 1.2 ou plus ancienne, vous n’avez pas de raison de ne pas tenter de faire la mise à jour.

    Au lieu de demander “Puis-je/Dois-je mettre à jour mes projets vers Rails 2.1?” vous devez plutôt vous demander “Est-ce que j’ai un jeu de tests complet ?” Si c’est le cas, tant mieux, vous devriez pouvoir faire la mise à jour sans problème et voir ce que vos tests vous racontent. S’il n’y a que quelques bugs, vous devriez pouvoir les corriger en moins d’une journée. Mais si vous n’avez pas une couverture complète de tests, alors vous devrez investir le temps nécessaire pour écrire les tests qui couvriront toute votre application, et seulement après, songer à faire la mise à jour.

    Les optimisations de performances à elles seules suffisent à justifier la mise à jour vers Rails 2.1.

    Be Sociable, Share!

Tags: , ,

  1. AkitaOnRails’s avatar

    Awesome Pierre, great work with the translation. I wish I could understand a little bit of french.

  2. Pierre’s avatar

    Hi Fabio ! Thank you too, you did the hard work !

  3. Trackback from elchinas on 5 janvier 2009 at 9:49

  4. rivsc’s avatar

    A quand le tutoriel pour Rails 3.1 ? :p

Reply

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *