Ceci est une traduction de l’article The skinny on scopes publié sur Edge Rails.

Je me souviens de mon cœur faisant des bonds lorsque le plugin has_finder de Nick Kallen a été implémenté dans rails core sous le nom de named_scope.
named_scope a rapidement rejoint la liste de mes outils préférés de par sa merveilleuse manière de créer des requêtes logiques encapsulées et réutilisables.
Alors qu’il avait ses points faibles (pour ne pas le nommer, le manque du support de :joins et :include), il a redéfini ma manière de penser la logique de mes modèles.
Une fois que vous avez gouté au plaisir des named_scopes, vous ne pouvez jamais revenir en arrière.

Et maintenant Rails 3 arrive avec son refactoring complet de ActiveRecord. Que deviennent nos cher named_scope ? Pour faire simple, cela a été renommé en scope et vous pouvez les utiliser comme vous le faisiez déjà … Mais de manière plus aisée.
Voyons un peu ce que l’on peut faire avec ces scopes dans Rails 3.

Usage basique

Supposons un modèle Post avec des champs published_at, title et content.
Dans rails 2.x, nous devrions définir les scopes published et recent.

class Post < ActiveRecord::Base
    named_scope :published, lambda {
        { :conditions =>
            ["posts.published_at IS NOT NULL AND posts.published_at <= ?", Time.zone.now]
        }
    }
    named_scope :recent, : order => "posts.published_at DESC"
end
La raison pour laquelle nous utilisons un lambda ici est que cela permet de reporter l’exécution de Time.zone.now au moment ou la scope est effectivement invoquée.
Sans ce lambda, le moment retourné serait celui ou la classé est évaluée. Pas la scope elle même.

Avec Rails 3, l’architecture d’ActiveRecord est maintenant basée sur une classe Relation. Classe que vous pouvez voir comme une sorte de « named_scope on steroids », permettant de de chainer chaque requête directement dans ActiveRecord.

Vous pouvez voir comment utiliser les méthodes where, ordre etc dans l’article de Pratik sur cette nouvelle interface ou bien encore dans ce Railscast.
Comprendre ceci est important étant donné que les nouvelles scopes sont construites autour de cela.

Voyons comment. Voici nos deux différentes scopes portées sous Rails 3.

class Post < ActiveRecord::Base
    scope :published, lambda {
        where("posts.published_at IS NOT NULL AND posts.published_at <= ?", Time.zone.now)
    }
    scope :recent, order("posts.published_at DESC")
end

Alors que la logique reste la même (les portions SQL), vous pouvez commencer à voir comment les scopes utilisent la nouvelle interface de requêtage pour construire directement la requête au lieu de construire un hash d’options comme c’était fait dans Rails 2.
Ceci est le tout premier aperçu de la flexibilité que la nouvelle interface apporte à nos scopes.
Elles ne sont plus construites différemment que n’importe quelle requête. Elles sont construites au dessus des mêmes méthodes que vous pouvez utiliser lorsque vous construisez directement vos requêtes.
Cette consistance est maintenant présent partout dans ActiveRecord.

Mais ce n’est pas tout …

Réutilisation des scopes

Supposons que nous désirions modifier notre scope « recent » pour n’inclure que les posts publiés. Nous avons déjà défini ce que published signifie et nous ne devrions donc pas avoir à le redéfinir pour créer une nouvelle scope.
Aucun problème ! Nous pouvons chainer les scopes elles mêmes et c’est ce que nous allons faire ici.

class Post < ActiveRecord::Base
    scope :published, lambda {
        where("posts.published_at IS NOT NULL AND posts.published_at <= ?", Time.zone.now)
    }
    scope :published_since, lambda { |ago|
        published.where("posts.published_at >= ?", ago)
    }
    scope :recent, published.order("posts.published_at DESC")
end

Ca commence à devenir intéressant.

Construction dynamique

Déjà dans Rails 2.3, vous pouvez créer des scopes anonymes afin d’obtenir dynamiquement des scopes chainables selon vos besoins.
Un cas typique d’utilisation est lorsque vous désirez créer une méthode de recherche à laquelle vous pouvez toujours ajouter d’autres manipulations.

Par exemple pour chercher parmi nos posts, nous créons cette méthode qui retournera un scope que vous pourrez par la suite filtrer (notez l’utilisation de « scoped » pour démarrer la chaine avec une scope vide à laquelle d’autres peuvent être ajoutées).

class Post < ActiveRecord::Base
    class << self

        # Cherche dans les champs title et body la chaine donnée.
        # Démarre avec une scope vide et la construit pour chaque attribut.
        # Facilitera l'extraction des définitions des champs de recherche pour le futur.
        def search(q)
            [:title, :body].inject(scoped) do |combined_scope, attr|
                combined_scope.where("posts.#{attr} LIKE ?", "%#{q}%")
            end
        end
    end
end

L’utilisation de inject ici masque l’intérêt de cette méthode si vous n’avez pas l’habitude de voir de telles itérations. Voici une version plus aisée à comprendre contenant les champs de recherche en dur (et qui n’utilise même pas de scope anonyme).

class Post < ActiveRecord::Base
    class << self

        # Moins dynamique mais probablement plus lisible
        def search(q)
            query = "%#{q}%"
            where("posts.title LIKE ?", query).where("posts.body LIKE ?", query)
        end
    end
end

Vu que nous construisons nos scopes autour de la nouvelle interface qui est fortement chainable, nous pouvons faire la chose suivante avec notre méthode de recherche :

# What's in the db, titles ~= publish date
Post.all.collect(&:title) #=> ["1 week from now", "Now", "1 week ago", "2 weeks ago"]
Post.published.collect(&:title) #=> ["Now", "1 week ago", "2 weeks ago"]

# Combinaisons de recherche
Post.search('1').collect(&:title) #=> ["1 week from now", "1 week ago"]
Post.search('1').published.collect(&:title) #=> ["1 week ago"]
Post.search('w').published_since(10.days.ago).collect(&:title) #=> ["Now", "1 week ago"]
Post.search('w').order('created_at DESC').limit(2).collect(&:title) #=> ["2 weeks ago", "1 week ago"]

Vous pouvez imaginer un scénario ou des requêtes bien plus complexes pourront être construites en utilisant des scopes anonymes.
Cool non ?

Scopes multi modèles

Les scopes sont parfaites pour manipuler uniquement les colonnes d’un modèle unique. Mais elles peuvent également être utilisées pour construire des requêtes multi modèles (qui requièrent un join).
Ajoutons à nos posts des utilisateurs (qui peuvent être auteur ou commentateur) et écrivons quelques scopes sur le modèle User qui nous permettront de récupérer uniquement ceux ayant publié des billets et ceux qui ont commenté.

class User < ActiveRecord::Base
    has_many :posts, :foreign_key => :author_id
    has_many :comments

    # Récupération de tous les utilisateurs ayant publié un billet
    scope :published, lambda {
        joins("join posts on posts.author_id = users.id").
        where("posts.published_at IS NOT NULL AND posts.published_at <= ?", Time.zone.now).
        group("users.id")
    }

    # Récupération de tous les utilisateurs qui ont commenté un billet
    scope :commented, joins("join comments on comments.user_id = users.id").group("users.id")
end

Notons également que ActiveRelation est également suffisamment intelligent pour savoir comment faire un join sur la définition de l’association, nous autorisant à placer cette relation en référence.

class User < ActiveRecord::Base
# Récupération de tous les utilisateurs ayant publié un billet
scope :published, lambda {
joins(:posts). # Inutile de rédiger le SQL
where("posts.published_at IS NOT NULL AND posts.published_at <= ?", Time.zone.now).
group("users.id")
}

# Récupération de tous les utilisateurs qui ont commenté un billet
scope :commented, joins(:comments).group("users.id") # Just reference :comments
end

C’est une bonne pratique de référencer le nom complet de la colonne, avec sa table (posts.published_at au lieu de published_at).
Cela permet d’éviter des ambiguïtés de noms de colonnes. Particulièrement important lorsque vous construisez des scopes multi modèles ou des colonnes venant de plus d’une table peuvent être jointes.

Pour êtres super flexible, vous pouvez toujours invoquer table_name au lieu de mettre cela en dur.
Pour être franc, c’est quelque chose que je ne fais que rarement. where(« #{table_name}.published_at IS NOT NULL »)

Vu que nous avons l’arsenal complet des opérateurs ActiveRecord à notre disposition dans les scopes, nous pouvons faire des join et des group by dans les scopes qui seront chainées dans les requêtes complexes. Chose que named_scope n’arrivait que rarement à faire.

# Récupération de tous les utilisateurs ayant publié un billet
User.published.collect(&:username) #=> ["tim", "dave"]
User.published.to_sql
  #=> SELECT "users".* FROM "users" join posts on posts.author_id = users.id
  #   WHERE (posts.published_at IS NOT NULL AND posts.published_at <= '2010-02-22 21:33:00.892308')
  #   GROUP BY users.id

# Récupération de tous les utilisateurs ayant posté un commentaire
User.commented.collect(&:username) #=> ["ryan", "john", "tim", "dave"]
User.commented.to_sql
  #=> SELECT "users".* FROM "users" join comments on comments.user_id = users.id
  #   GROUP BY users.id

# Combinaison pour récupérer tous les auteurs qui ont posté un commentaire
User.published.commented.collect(&:username) #=> ["tim", "dave"]
User.published.commented.to_sql
  #=> SELECT "users".* FROM "users"
  #   join posts on posts.author_id = users.id
  #   join comments on comments.user_id = users.id
  #   WHERE (posts.published_at IS NOT NULL AND posts.published_at <= '2010-02-22 21:33:00.892308')
  #   GROUP BY users.id
Comme fait ici, vous pouvez utiliser to_sql afin de visualiser la requête SQL qui sera exécutée. C’est très pratique lors de débogages.

Opérations CRUD sur les scopes

Vu que ActiveRelation vous permet d’invoquer toutes les méthodes builder/update/destroy, celles-ci sont également accessibles pour les scopes.
Jouons un peu à faire des modifications sur nos scopes au lieu de se contenter de les récupérer.

# Incrémente le nombre de vues pour tous les billets publiés
Post.published.collect(&:views_count) #=> [59, 71, 42]
Post.published.update_all("views_count = views_count + 1")
Post.published.collect(&:views_count) #=> [60, 72, 43]

# On se moque des billets non publiés
Post.unpublished.size #=> 1
Post.unpublished.destroy_all
Post.unpublished.size #=> 0

Vous pouvez également créer un nouvel uplet avec des scopes existantes. Supposons que nous avons une scope (très peu utile) qui ne récupère que les posts avec un certain titre

class Post < ActiveRecord::Base
    scope :titled_luda, where(:title => 'Luda')
end

Nous pouvons utiliser cette scope pour créer de nouvelles instances (tout comme new, create, …)

Post.titled_luda.size #=> 0
Post.titled_luda.build
  #=> #<Post id: nil, title: "Luda", ...>
Afin de pouvoir utiliser les méthodes de création dans une scope, celle-ci doit définir l’égalité de l’attribut de manière directe dans le where avec un hash comme fait précédemment.

Si nous avions fait un where(« title = ‘Luda’ »), le titre n’aurait pas été propagé dans le nouvel objet.

Pour les grands fou

Une chose qui m’a toujours choqué est la manière dont la logique pour ce qui fait qu’un billet est publié est séparée entre les scopes de la classe Post et celles de la classe User.
Afin de se rafraichir la mémoire

class Post < ActiveRecord::Base
    scope :published, lambda {
        where("posts.published_at IS NOT NULL AND posts.published_at <= ?", Time.zone.now)
    }
end

Ainsi que

class User < ActiveRecord::Base

    scope :published, lambda {
        joins(:posts).
        where("posts.published_at IS NOT NULL AND posts.published_at <= ?", Time.zone.now).
        group("users.id")
    }
end

Tout bon développeur va instantanément noter la duplication du where(« posts.published_at IS NOT NULL AND posts.published_at <= ?", Time.zone.now).

Heureusemnt il existe une méthode pour éviter cette répétition : un merge dont l’alias est ‘&’.
Voyons comment nous pouvons utiliser scope#& afin de faire référence à Post.published dans la scope User.published.

class User < ActiveRecord::Base

    scope :published, lambda {
        joins(:posts).group("users.id") & Post.published
    }
end

Et voici la requête SQL qui sera générée :

User.published.to_sql
  #=> SELECT users.* FROM "users"
  #   INNER JOIN "posts" ON "posts"."author_id" = "users"."id"
  #   WHERE (posts.published_at IS NOT NULL AND posts.published_at <= '2010-02-27 02:55:45.063181')
  #   GROUP BY users.id

Notez comment les conditions définies dans Post.published sont mergées dans les relations join et group de la scope User.published ?
Et ceci fonctionne avec toutes les relations mergables.

Conclusion

Ce billet dérive un petit peu dans une explication de la nouvelle interface de requêtage SQL avec ActiveRecord dans Rails 3 afin d’entrer dans les détails des scopes.
Cependant aucune des nouvelles fonctionnalités des scopes n’aurait pu être mise en place sans ces nouveautés d’ActiveRecord.
Donc si vous êtes encore un petit confus à propos de cela, n’hésitez pas à vous documenter encore un petit peu plus à propos de la nouvelle API ActiveRecord avant de vous penchez sur les scopes.
Une fois que vous aurez ces bases, vous ne pourrez plus vous passer des scopes !

Vous êtes probablement déjà tombé sur des symboles en Ruby, souvent utilisés pour les clés des hash.
Exemple :

{:foo => 'bar'}

:foo est un symbole. ‘bar’ est une string. Mais nous pourrions faire

{'foo' => 'bar'}

Alors pourquoi utiliser les symboles ? Supposons le cas suivant :

x = :sym
y = :sym

(x.__id__ == y.__id__ ) && ( :sym.__id__ == x.__id__)

La comparaison ici retournera vrai.
En effet, un même symbole sera toujours le même objet en mémoire, dans toute votre application.
Cela ne pose pas de problèmes car un symbole est également non mutable.

Et le même avec des strings :

x = "string"
y = "string"

(x.__id__ == y.__id__ ) || ( "string".__id__ == x.__id__)

Ici, x, y et « string » sont tous les trois des objets différents avec des __id__ différents. 3 objets ont donc été initiés en mémoire !

Maintenant reprenons notre hash de tout à l’heure avec la comparaison string/symbole. Mais en créant deux hashes !

Avec la version symboles :

{:foo => 'bar'}
{:foo => 'doe'}

Nous créons 5 objets en mémoire : deux hashes; un symbole et deux string.

Dans la version strings :

{'foo' => 'bar'}
{'foo' => 'doe'}

Ici, nous créons 6 objets en mémoire : deux hashes et 4 strings.

Dans un cas d’école comme celui-ci, cela ne change pas grand chose. Mais dans un cas réel d’application, si vous remplacez tous vos symboles par des strings, votre consommation mémoire va rapidement s’élever.
Alors commencez dès maintenant à remplacer toutes vos strings qui sont non mutables (en gros, toutes vos clés de hashes et les options dans les méthodes) par des symboles :)

Pour compenser le fait qu’il y ait eu deux articles la semaine passée, celui de cette semaine sera light ;)
Il existe plusieurs systèmes d’identifications pour les applications Rails. Mes deux préférés sont authlogic et devise. Mais clearance est pas mal non plus.

Je vais parler du premier. Lorsque vous avez implémenté votre système d’identification avec ce gem, vous vous rendrez rapidement compte qu’un même utilisateur peut être connecté plusieurs fois depuis des machines ou des navigateurs différents.
Ce base, cela peut être intéressant afin d’éviter d’avoir à redemander à vos utilisateurs de se reconnecter trop régulièrement.

Mais dans d’autres cas (ou vous désirez éviter le partage de comptes par exemple), ce n’est pas l’effet désiré.
Après avoir cherche un petit peu, j’ai posté la solution à ceci sur stackoverflow.

Dans votre modèle de session (par défaut UserSession), ajoutez le code suivant :

before_destroy :reset_persistence_token
before_create  :reset_persistence_token

def reset_persistence_token
    record.reset_persistence_token
end

Nous créons deux callbacks.
Ainsi, notre méthode reset_persistence_token sera exécutée à chaque fois que nous créons ou supprimons une session utilisateur (à chaque fois que l’utilisateur se connecte ou se déconnecte).

Cette méthode réinitialise un token qui est également situé dans la session de l’utilisateur et qui nous permet de l’identifier.
Nous sommes obligés de la définir car elle l’est à l’origine, uniquement dans le modèle User. C’est dans la session que nous désirons réinitialiser ce token.
D’ou le record.. record retourne l’utilisateur auquel nous réinitialisons le token.

Ainsi, à chaque fois que notre utilisateur se connectera ou se déconnectera de l’application, le token sera modifié.
Et nous n’aurons donc pas la possibilité d’avoir deux machines connectées avec la même session :)

Je vois beaucoup de développeurs anglophones qui ont des blogs techniques. Mais cela ne me semble pas si répandu que cela chez les francophones.
Du coup j’ai rédigé une petite liste de 10 raisons (en fait 11) pour lesquelles, toi, développeur, tu devrais avoir un blog technique.

  1. Le partage des connaissance

    Ne soyez pas égoïste, pensez aux autres développeurs qui vont affronter des problèmes similaires aux vôtres !
    Partager ces problèmes (en les sortant de leur contexte au besoin) vous permet ainsi de faciliter le travail de ceux qui tenteront de faire la même chose que vous après.

  2. La création d’une base personnelle de connaissances

    Parce que penser aux autres c’est gentil. Mais qui pensera à vous ! ;)
    Avec un blog, vous vous créez une base de connaissances personnelles voir un aide mémoire.
    Il m’est par exemple arrivé plusieurs fois de me rendre sur mon article Installer le gem MySQL sous mac.

  3. La veille technologique

    Avec un blog que vous tentez réellement de mettre à jour régulièrement, vous allez tenter de vous tenir toujours au courant des dernières nouveautés afin de faire de nouveaux articles.
    Ainsi j’ai écrit quelques articles sur Rails 3 dernièrement. Cela m’a forcé à me pencher sur cette version bêta. Chose que je n’aurai probablement pas fait tout de suite si ce n’est pour rédiger ces articles.

  4. Le dépassement de soi

    Lorsque vous allez tenter de découvrir ces nouvelles technologies, vous allez devoir vous plonger dans la documentation de l’application (personne n’a encore réellement fait d’article puisque la chose est toute neuve) voir dans le code même de celle-ci.
    C’est une excellente manière de vous dépasser personnellement et au passage d’améliorer vos compétences en développement.

  5. La reconnaissance des compétences

    En discutant avec Sarah il y a quelques semaines, celle-ci me disait ne pas rédiger d’articles techniques car beaucoup de monde le fait déjà en PHP. En même temps elle ajoutait avoir eu quelques personnes lui ayant dit ne pas savoir comment elle développe.

    Lorsque vous cherchez un emploi en tant que développeur, votre futur employeur (et/ou vos futur collègues) va/vont vouloir savoir comment vous développez. S’il faudra repasser derrière vous après chaque commit ou si vous allez devenir le nouveau cerveau de l’équipe.
    Deux manières existent afin de montrer du code : participer à des projets open source ou écrire des articles sur un blog :)

    Attention, ceci est également à double tranchant. En publiant du code, vous courez également le risque que tout le monde se rende compte qu’en fait, votre code n’est pas aussi propre que vous voudriez le faire croire ;)

  6. Remercier les gens qui ont pris du temps pour vous apprendre

    Bah oui, on a tous débuté un jour. Moi c’était en seconde avec Jordan et un livre sur PHP (qui est une référence en matière de mauvaises pratiques).

    Et à ce moment la, vous avez bien du demander de l’aide sur divers forums ou à des amis qui avaient déjà de la bouteille. Quelle meilleure manière de les remercier que de partager ensuite ce que vous avez appris ?

  7. La visibilité/e-réputation

    Ouais, c’est quand même l’une des raisons premières de créer un blog hein. Vous allez gagner en visibilité, en renommée (à condition de faire des articles de qualité), devenir un expert dans votre secteur (ou au moins donner l’impression que vous en êtes un :mrgreen: ).

    Les conséquences se font rapidement ressentir : vous recevrez beaucoup plus d’offres d’emploi (et serez donc mieux payé), de demandes de contact (j’ai beaucoup d’étudiants qui me contactent pour des « interviews » sur le métier de développeur web).

  8. Se la péter

    :mrgreen:

    Il ne se passe pas une semaine sans qu’un collègue, en cherchant quelque chose à propos de ruby, ne tombe sur mon blog. Et quelle meilleure manière de se « la péter » que de ressortir sur tous les résultats Google relatifs à un domaine technologique précis.

  9. Amour du prochain

    C’est pas moi qui ai dit « Aimez vous les uns les autres ». Mais ca s’applique bien ici. Et même si c’est un petit peu de la répétition avec la seconde raison, ça fonctionne tout de même.
    On ne vis pas dans un monde de bisounours. Mais en travaillant pour, on peut arriver à quelque chose de similaire ;)

  10. Se payer des bières avec les adsense

    On peut toujours rêver ! Mais rien ne vous empêche d’afficher quelques publicités sur votre blog.
    Pour info avec les revenus générés par ce blog, je peux me payer environ 3 bières par mois.

  11. Le fun

    Et oui, ça ne fait pas 10 mais 11. Et pourtant cette dernière raison est la plus importante.
    Il y a quelques jours en réfléchissant à cet article, j’ai sondé sur twitter en demandant « qu’est-ce qui vous motive à maintenir un blog technique ? ».
    S’en est suivi une discussion avec Raphael qui m’expliquait rapidement pourquoi il a fermé son blog.

    C’est la raison primordiale pour maintenir un blog technique : il faut que cela vous amuse. Si ce n’est pas le cas, autant ne rien faire.
    Vous n’arriveriez pas à vous motiver, vos articles perdraient fortement en qualité et votre blog n’aurait pas réellement d’intérêt.


Pour finir je ne donnerai que deux conseils à ceux que cela aurait (par le plus grand des hasards) motivé à créer leur blog :

  • Soyez constant.
    Évitez de publier trois articles le premier mois, puis plus rien pendant 6 mois.
    Je me force personnellement à faire un article par semaine (voir plus. La preuve, cet article est le second e la semaine). Mais cela peut être plus ou moins.
  • Relisez vous. Lors du Wordcamp Parisien de Février 2009, on m’a demandé « si j’avais un seul conseil pour un futur blogueur à donner, quel serait-il ? »
    Ma réponse fut : rédige tes articles le lundi, publie les le mardi.
    Et relis ton article avant de le publier.

Et vous, qu’est-ce qui vous pousse à maintenir (ou pas) un blog technique ?
Certains m’ont déjà répondu (merci Xavier et Bruno) et cela a fait les 6e et 9e raisons.

J’ai déjà plusieurs fois, dans des articles précédents, parlé de couchdb.
Notamment les bases et les vues.

Dans le second article notamment, nous avons vu la fonction emit, qui permet, dans une vue, de renvoyer un enregistrement.
Cette fonction prends deux arguments. Le premier étant la clé de l’enregistrement, à considérer comme son identifiant. Et le second étant les données de l’entrée.

Supposons une vue quelconque qui me retournerait les données suivantes :

{"total_rows":5,"offset":0,"rows":[
    {"id":"cef5872e7d32bfe45f4a0b766544878c","key":"Aptitude","value":5},
    {"id":"c1a25474f3bb36ab517945c24f3a6c34","key":"Rubinius","value":3},
    {"id":"234331917ad63b46a155d4482df350eb","key":"Rubygems","value":2},
    {"id":"317fd0be2eb4133528af6cf6e4fba4aa","key":"Rubygems","value":8},
    {"id":"5040d8ea60f951fe6a44b0b0f7eee439","key":"Rubygems","value":7},
]}

La clé est un identifiant permettant de reconnaitre chaque entrée. La valeur n’a aucun sens ici. Mais contiendra les données de votre enregistrement dans un contexte réel.

CouchDB vous permet de récupérer tous les enregistrements. Mais pour n’en récupérer que certains, cela risque d’être un petit peu compliqué.
Créer une vue pour chaque requête est à rejeter d’avance car overkill.
Deux options vont cependant venir à notre secours : startkey et endkey.

Supposons que je ne sois intéressé que par les entrées ayant pour clé « Rubinius ».
En appellant notre vue avec les paramètres ?startkey= »Rubinius »&endkey= »Rubinius », nous obtenons uniquement l’entrée correspondant à Rubinius.
Les paramètres startkey et endkey doivent être des éléments json valides. Nous devons donc y ajouter des «  » afin de spécifier une string.

Si nous voulions Rubinius et Rubygems, nous pourrions faire une recherche sur le terme Rub qui est commun aux deux.
Donc en mettant un ?startkey= »Rub »&endkey= »RubZ », nous aurons tous les enregistrements, qu’ils aient pour clé Rubinius ou Rubygems.

Que font startkey et endkey ?

Pour comprendre à quoi servent ces deux paramètres, il faut prendre les résultats de manière itérative.
CouchDB les parcours tous les uns après les autres. Lorsque startkey est présent, il supprimera tous les enregistrements jusqu’à ce que l’un d’entre eux ne matche la valeur du paramètre.

CouchDB conservera ensuite tous les enregistrements.

Si endkey est présent, il ne les conservera que jusqu’à ce que l’un d’eux ne matche la valeur de ce paramètre. Et supprimera tous ceux situés après que le premier ait matché cette valeur.

Dans notre exemple précédent, si nous avions mis « Rub » à endkey, le premier enregistrement aurait matché non seulement startkey mais également endkey.
Par conséquent nous n’aurions vu strictement aucun enregistrement. Tous auraient été enlevés des résultats.
En mettant endkey à RubZ, tous nos enregistrements Rubinius et Rubygems matchent cette valeur.
Mais si nous avions un enregistrement Python à la suite, il ne matcherait absolument pas et ne serait donc pas inclus.

Recherche dans une clé simple

Par clé simple, nous entendons un string (à l’opposé d’un tableau ou d’un hash json).
Dans le cas d’un string, CouchDB fera une comparaison > ou < entre la clé et la valeur de startkey et endkey.

Ainsi si startkey a pour valeur 3, toute clé étant un entier supérieur ou égal à 3 sera inclus dans les résultats.
Si startkey a pour valeur "Aaa", des clés telles que "Aaabbb", "Aaaaaa" seront inclues. Mais "Abaa" ne sera pas inclue.

Afin de faire une recherche sur une terme précis, je vous conseille d'ajouter un caractère très éloigné dans votre table de caractères à la fin de endkey.
En ruby le mieux pour cela est de faire :

FAR_CHAR = [0x9999].pack(‘U’)

Vous n'avez plus qu'à ajouter ce caractère, le dernier que vous pourrez trouver, à la fin de votre chaine endkey.

Recherche dans une clé multiple

Les recherches dans les clés multiples sont un petit peu plus compliquées.
Supposons une vue qui vous retournera l'élément JSON suivant :

{"total_rows":5,"offset":0,"rows":[
    {"id":"cef5872e7d32bfe45f4a0b766544878c","key":["Aptitude", "Rubinius"],"value":5},
    {"id":"c1a25474f3bb36ab517945c24f3a6c34","key":["Rubinius", "Aptitude"],"value":3},
    {"id":"234331917ad63b46a155d4482df350eb","key":["Rubinius", "Aptitude"],"value":2},
    {"id":"317fd0be2eb4133528af6cf6e4fba4aa","key":["Rubygems", "Aptitude"],"value":8},
    {"id":"5040d8ea60f951fe6a44b0b0f7eee439","key":["Rubygems", "Rubygems"],"value":7},
]}

Comment chercher dans un tableau ... :)
Simplement en y passant un tableau ! Ainsi, nous pourrions faire ?startkey=["Rubinius", "Aptitude"]&endkey=["Rubinius", "Aptitude"]
Qui nous retournera tous les enregistrements correspondant à la clé ["Rubinius", "Aptitude"] (soit 2 enregistrements).

La recherche fonctionne de la même manière que précédemment sauf que en plus de parcourir tous les enregistrements, CouchDB parcours chacunes des entrées du tableau.
Mais ne fait pas de comparaison pour chacun des éléments !

CouchDB fait en effet une comparaison d'égalité pour chacun des éléments du tableau, sauf le dernier.
Ainsi, dans notre dernière requête, une comparaison == sera faite pour l'élément "Rubinius".
Et des comparaisons < et > seront faites pour l'élément "Aptitude".

Du coup si vous placez, dans votre requête : ?startkey=["Rub", "Aptitude"]&endkey=["RubZ", "Aptitude"], vous n'aurez non pas trois enregistrements ... Mais aucun car aucun de vos enregistrements n'a pour première valeur de sa clé "Rub".

En revanche sur le dernier élément, la recherche se fait avec les mêmes comparaisons que pour une string.
Vous pouvez donc faire la recherche suivante : ?startkey=["Rubinius", "Apt"]&endkey=["Rubinius", "AptZ"], qui vous retournera correctement vos deux enregistrements ayant pour clé ["Rubinius", "Aptitude"].

Conclusion

Si vous en êtes ici et que vous avez tout compris, félicitations ! J'ai mis deux bonnes heures avant de réellement comprendre le concept ;)
Comme vous le constatez, la recherche via startkey et endkey est assez atypique comparé à une recherche SQL habituelle et dicterera généralement une partie de l'architecture de votre application et de son fonctionnement.

Mais une fois que ceci est compris, la puissance de ce genre de recherche est assez impressionnant (dans mon cas, chez LIM, nous faisons des recherches dans des tableaux allant jusqu'à 12 entrées).
Et vous, comment l'implémentez-vous ? :p

L’une des grandes mises à jour de Rails 3 est la nouvelle API Action Mailer.
Petit rappel : dans les versions précédentes de Rails, nous pouvons transmettre des emails en créant, dans le répertoire app/models un modèle ActionMailer.
Celui-ci pourrait ressembler (dans rails 2.3) à ceci :

class UserMailer < ActionMailer::Base
    def welcome_email(user)
        recipients user.email
        from "I'm nobody <42@unknown>"
        subject "Hello World"
        body {:user => user }
    end
end

Et dans le répertoire app/views/user_mailer, nous pourrons créer un fichier welcome_email.text.erb qui sera le contenu de notre email.
Maintenant supposons que nous désirions attacher une pièce jointe à notre email.
Nous allons devoir ajouter dans notre méthode welcome_email cette pièce jointe.

attachment "application/pdf" do |a|
    a.body = contenu_du_pdf()
end

un « beurk » suffira !

Dans Rails 3, notre méthode d’envoi d’email devient la suivante :

class UserMailer < ActionMailer::Base
    default :from => "I'm nobody <42@unknown>"

    def welcome_email(user)
        @user = user
        mail(:to => user.email,  :subject => "Hello World")
    end
end

Et lorsque nous souhaitons ajouter une pièce jointe, nous n’avons qu’à faire :

attachments['terms.pdf'] = {:content => contenu_du_pdf() }

C’est déjà plus sympa.

Mais ce n’est pas tout ! Par défaut, les fichiers welcome_mail.text.erb et welcome_mail.html.erb sont inclus dans le mail.
Ainsi la personne recevant l’email peut le lire en html ou en texte.
Mais tout comme vous le faites dans vos contrôleurs pour l’html, le json, l’xml ou tout autre format, vous pouvez vouloir rendre quelque chose de différent en fonction du format html ou texte du mail.

Go ! :)

mail(:to => user.email,  :subject => "Hello World") do |format|
    format.text { render :text => "Mon email est en texte" }
    format.html { render :html => "Mon email est en <strong>HTML</strong>" }
end

Pour continuer lorsque vous envoyiez votre email, vous faisiez cela de la manière suivante :

UserMailer.deliver_welcome_email(@user)

Vous devrez maintenant faire :

UserMailer.welcome_email(@user).deliver

Le welcome_email vous renvoyant un objet Mail que vous pouvez ainsi modifier comme bon vous semble.
Voir le stocker pour l’envoyer plus tard par exemple.
Si vous désirez plus d’informations concernant cette nouvelle API, je vous recommande l’article sur guides.rails.info (encore en cours de rédaction).
Et le gist qui a servi de spécification pour cette nouvelle API.

Demain soir aura lieu le 4e apéro Ruby à Lyon.
Bien évidemment, je ne peux que vous conseiller de vous y rendre !
Je serai présent et j’entrainerai un jeune padawan avec moi afin de le trainer du côté clair de la force :)

C’est à l’Antre Autre, un petit bar sympa à côté des Terreaux.
Vous pouvez venir à partir de 19h. Et nous serons probablement plusieurs à manger sur place par la suite (mais bien évidemment, vous partez quand bon vous semble).

Que vous veniez ou pas, vous avez entendu parler de rails 3 !
Et peut-être de bundler, dont je ferai une présentation d’une dizaine de minutes (« lightning talk »).

Du coup, que vous veniez ou pas, vous pouvez visualiser en ligne cette introduction à bundler.
Enjoy! comme on dit. Si vous avez des remarques à faire dessus, n’hésitez pas à me les faire remonter (ou alors à forker le projet et à faire les modifictions).
Et je vous attends demain.

Parlons un peu de rack :)
Utilisé par rails et de nombreuses autres applications ruby afin de pouvoir être lancées par une large majorité de serveurs web (mongrel, unicorn, …) rack est particulièrement puissant.

La création d’une première application est on ne peut plus simple.
Dans un fichier test.rb :

class Test
    def call(env)
        [200, {"Content-Type" => "text/html"}, ["<html><head></head><body>Hello world!</body></html>"]]
    end
end

Exécutez votre application avec la ligne de commande suivante :

rackup test.rb

Simple mais efficace :)
Bien évidemment il faut ajouter des librairies à cela afin d’avoir quelque chose de plus puissant.

Maintenant découvronr autre chose : les middlewares.
En effet rack peut embriquer les méthodes call et ainsi vous permettre d’exécuter du code juste après l’initialisation de rack. Ou bien juste avant l’exécution du code de votre page.

Le développement d’un rack ressemble fortement à celui d’une tâche rack « normale ».
Voici un exemple de middleware vous permettant d’ajouter, juste avant la balise , le temps d’exécution de la page.

Dans un fichier response_timer.rb

class ResponseTimer
    attr_reader :message, :app

    def initialize(app, message = "Response Time")
        @message = message
        @app = app
    end

    def call(env)
        start = Time.now
        status, headers, response = app.call(env)
        stop = Time.now

        body = ''
        response.each { |part| body << part }
        index = body.rindex('</body>')
        if index
            body.insert(index, "<!-- #{message}: #{stop - start} -->\n")
            headers["Content-Length"] = body.length.to_s
            response = [body]
        end
        [status, headers, response]
    end
end

En revanche, nous allons maintenant avoir besoin d’un fichier de configuration pour rack, afin de pouvoir définir quel middleware nous incluons.
Nous devons pour cela créer un fichier nommé config.ru

require 'test'
require 'response_timer'

use ResponseTimer
run Test.new

Et nous n’avons plus qu’à exécuter le serveur

rackup

Vous noterez trois choses :

  • Nous incluons les deux librairies que nous venons de créer. Notre application et le middleware
  • Avec la méthode « use », nous ajoutons le middle à ceux natif à rack
  • Nous exécutons notre application

Comment ce middleware fonctionne-t-il ?
Comme votre application ! :)
Vous constaterez que les deux ont une méthode call. Et que les deux retournent un tableau contenant status, headers et réponse.

La seule différence entre les deux étant cette ligne :

status, headers, response = app.call(env)

Dans le middleware :
Grace au app.call, nous appellons le middleware suivant. Et ce, jusqu’à ce que nous arrivions au bout de la chaine : notre classe « test ».
Nous calculons donc assez aisément le temps d’exécution de notre application. Il nous suffit de faire la différence entre le timestamp avant l’exécution de cette méthode et après.
Et comme nous avons accès au contenu de la page retournée même après l’exécution de la méthode, nous pouvons y ajouter un commentaire juste avant la balise (si elle existe) contenant le temps d’exécution de la page.

Et dans rails ?

Comme dit plus haut, toute application rails tourne avec rack. Il est donc tout à fait possible d’ajouter notre middleware dans votre application.
Dans rails, les middlewares sont à ajouter dans le fichier config/environment.rb.

Voici comment j’ai inclu le rack ResponseTimer :

Rails::Initializer.run do |config|
    config.middleware.insert_before Rack::Lock, "ResponseTimer", "Load Time"
end

Le middleware Rack::Lock est le tout premier à être exécuté (vous pouvez avoir la liste de tous vos middlewares avec rake middleware).
Nous exécutons donc, juste avant celui-ci, le calcul du temps d’exécution de notre page :)

Trois méthodes vous permettent d’ajouter de nouveaux middlewares :

  • config.middleware.use – Le middleware sera ajouté à la fin de la pile
  • config.middleware.insert_before – Le middleware sera ajouté avant celui passé en premier paramètre
  • config.middleware.insert_after – Le middleware sera ajouté après celui passé en premier paramètre

Vous pouvez également supprimer un middleware précédemment ajouté : config.middleware.delete
Et remplacer un middleware par un autre : config.middleware.swap

Par ailleurs de nombreux middlewares sont disponibles en Open Source. Vous en trouverez une liste sur le wiki de rack.
Ainsi que dans le projet rack-contrib.

Rails 3 étant proche de sa première beta, j’en profite pour multiplier les articles à son propos !
Qui plus est il y a matière à écrire. Je ne m’en prive pas donc :)

Hier matin je suis tombé sur un article particulièrement intéressant The path to Rails3: Introduction, qui explique plutôt bien le maitre mot de cette nouvelle version du framework : découplage.
Ce découplage a pour but de faciliter l’utilisation de rails par blocs uniquement lorsque son utilisation en entier n’est pas forcément nécessaire.

Vous pouvez ainsi valider vos modèles sans forcément être dans une application rails, ni utiliser Active Record.
Avec arel, vous pouvez générer des requêtes SQL sans dépendre de rails (à terme du moins. Pour le moment, vous dépendez toujours d’ActiveRecord).

Avec bundler, vous gérez les dépendances de votre projet, qu’il utilise rails ou pas.
Regardez comment je fais pour jesus !
Je ferai une présentation de bundler au prochain apéro Ruby à Lyon. Venez donc y assister !

Je découvre donc cet article. Et le trouvant intéressant, je le partage avec Julien qui bosse dans le même bureau que moi.
Sa réaction a été « c’est vraiment en train de devenir inaccessible pour les débutants, rails. Trop compliqué » (je répète pas les choses mot pour mot, désolé).

Ce à quoi je réponds : NON !.
Pour un débutant découvrant le framework, celui-ci reste toujours aussi simple. Le screencast créer un blog avec rails en 15 minutes est parfaitement adaptable pour rails 3.

Et ce parce que de base, rien ne change !
L’idéologie de rails est toujours convention over configuration et les API ne changent pas fondamentalement (sauf peut-être pour ActionMailer. Mais ce n’est pas encore implémenté).

Ainsi votre contrôleur ressemblera toujours à ceci :

class PostController < ApplicationController
  def index
    @posts = Post.all
  end
end

Votre modèle ressemblera toujours à cela

class Post < ActiveRecord::Base
  validates_presence_of :title, :content
end

Et votre vue ressemblera toujours à cela :

<% @posts.each do |post|  %>
    <p>
        <%= post.title %>
        <%= post.content %>
    </p>
<% end %>

Vos routes quant à elles, au lieu de ressembler à cela :

ActionController::Routing::Routes.draw do |map|
  map.resource :post
end

Ressembleront à cela :

ApplicationRails3::Application.routes.draw do |map|
    resources :post
end

Et rien qu’avec ça, vous avez le début de votre blog vous permettant déjà de visualiser la liste de tous vos articles.

« seul » le fonctionnement en interne change (et ce radicalement). Les API utilisées dans votre application ne changent, pour la plupart pas.
Et si elles changent, la retro compatibilité devrait évidemment être assurée pendant pendant une version.

Moralité : non, rails ne se dirige pas vers une usine à gaz tel que J2EE. Oui, rails conserve sa simplicité. Et oui rails prends énormément en puissance.

Il y a une semaine de cela, Yehuda Katz a fait un article expliquant comment, dans Rails 3, obtenir les fonctionnalités de ActiveModel dans un modèle n’utilisant pas ActiveRecord.
Cela permet notamment de pouvoir utiliser les méthodes de sérialisation de Rails (export json et xml) et les validations.
C’est ce second point que je vais détailler aujourd’hui parce que c’est celui que je trouve le plus intéressant.

Notez que vous devez tout d’abord avoir le gem ActiveModel 3.0.pre installé sur votre machine.
Il est disponible sur gemcutter. Il vous suffit donc de faire

sudo gem install activemodel -v=3.0.pre -source=http://gemcutter.org

Prenons un fichier « test.rb », totalement intépendant de Rails. C’est juste du ruby pur et dur.
Mais parce qu’on y définit une classe, on souhaite pouvoir valider le format des données passées à celle-ci.

Voici donc le contenu de notre fichier test.rb

#
# Nous ne sommes pas dans Rails. Rubygems n'est donc pas inclus par défaut.
# ActiveModel ne l'est pas non plus. On doit donc les appeller.
#
require 'rubygems'
require 'active_model'

class Test
  #
  # On charge les validations de ActiveModel
  #
  include ActiveModel::Validations

  #
  # First Name et Last Name doivent absolument être présent
  #
  validates_presence_of :first_name, :last_name

  #
  # On définit les attributs First Name et Last Name
  # Et on les initialise dans le constructeur
  #
  attr_accessor :first_name, :last_name
  def initialize(first_name, last_name)
    @first_name, @last_name = first_name, last_name
  end
end

Vous noterez qu’il nous suffit d’inclure ActiveModel::Validations afin d’avoir accès à toutes ces méthodes de validations. Plutôt cool ! :)

Dès à présent, nous pouvons tester si nos validations passent !

test = Test.new('Jane', 'Doe')
p test.valid?

Inévitablement, nos validations passent puisque nous vérifions simplement la présence de First Name et de Last Name et que sans ceux-ci, nous ne pourrions même pas instancier la classe.

Mais … Comme nous avons accès à toutes les méthodes de validation d’ActiveModel, nous pouvons créer notre propre validateur !

class Test
  # ... Contenu de la classe définit précédemment dans l'article

  #
  # Et on ajoute une validation, empêchant quelqu'un de prendre le nom de famille "Doe"
  #
  validate do |t|
    t.errors[:last_name] << 'You must know your own name. Doe is only for anonymous guys' if t.last_name == 'Doe'
  end
end

Constatez le « validate » que nous avons ajouté et qui empêche quelqu’un de prendre le nom de famille « Doe ».

Maintenant, réexecutons notre code précédent. Notre méthode « valid? » nous retournera bien évidemment false.
Et si nous faisons un print de test.errors, nous constaterons que celui-ci est un Hash contenant chacune de nos erreurs.

#<OrderedHash {:last_name=>["You must know your own name. Doe is only for anonymous guys"]}>

N’oubliez pas bien évidemment de vérifier que la validation de vos données est correcte avant de sauvegarder celles-ci. Sans quoi l’utilité est particulièrement restreinte :)

Vous pouvez du coup, d’une manière similaire à ce que vous faites déjà avec vos modèles ActiveRecord, valider les données de tous vos objets Ruby en utilisant ActiveModel.
Par exemple si vous utilisez une solution NoSQL telle que CouchDB ou MongoDB.
Ou encore sur des modèles vous permettant de faire le lien avec une API distante (XML ou JSON par exemple).

Tous vos modèles utilisent ainsi la même API, celle d’ActiveModel. Ils sont tous homogènes et vous pouvez d’autant plus aisément manipuler ceux-ci.

 
Fork me on GitHub