Développement

Pourquoi et comment nous avons migré de CouchDB vers MongoDB

Après quelques hésitations et suite à de nombreuses discussions, nous avons décidé de migrer l'application sur laquelle je travaille de CouchDB vers MongoDB.

Je vais tenter, dans cet article, de vous expliquer pourquoi et comment nous avons décidé de faire cela.

Le pourquoi du comment

L'image ci-contre est l'interface de recherche de l'application sur laquelle je travaille.

Dans cette application, nous avons divers enregistrements (chacun représentant un forage). Chacun de ces enregistrements possède divers attributs tels que ceux qui sont affichés ici (nom; longueur et date de forage).

La partie haute de l'image correspond aux filtres. On peut ajouter un ou plusieurs filtres sur les attributs. Ici par exemple, nous avons deux filtres : un sur le numéro de série de l'appareil qui a analysé le forage et un sur une intervalle de dates.

Les enregistrements affichés sont ceux qui correspondent à tous les filtres présents. La raison principale nous ayant poussé à migrer vers MongoDB est ici !

Fonctionnement sous CouchDB

Afin de rechercher l'enregistrement approprié avec CouchDB, nous créons dynamiquement diverses vues.

Par exemple la recherche ci-dessus fait appel à la vue by_boreholename_creation_date. Vue qui nous retourne "simplement" tous les enregistrements avec, comme clé, un tableau de tous les attributs présentés. A l'appel de cette vue, nous faisons une recherche avec startkey et endkey, qui nous permet alors de filtrer suivant les valeurs fournies.

Cependant l'article startkey et endkey dont le lien est plus haut dit :

CouchDB fait en effet une comparaison d'égalité pour chacun des éléments du tableau, sauf le dernier.

Du coup supposons la recherche suivante :

  • Sur une intervalle de date de début.
  • Puis sur un nom de forage.

Le second élément de la clé sera cherché correctement. Cependant pour l'intervale, la recherche faite sera en ==. En conséquent nous n'aurons absolument pas les résultats escomptés. Nous n'aurons que les enregistrements correspondant à la date appropriée.

Lors du WebWorkersCamp, j'ai abordé ce problème à Benoit Chesneau qui est contributeur CouchDB. Bien que nous n'ayons pas vraiment eu le temps d'en parler en détail, la solution qu'il semblait me proposer était de créer une vue pour chaque filtre et de faire autant d'appels que de filtres.

A l'heure actuelle, certains de nos clients ont plus de 700 enregistrements en base et cela va en grimpant de manière exponentielle. Nos requêtes sont déjà (trop) lentes. Il n'est pas envisageable de faire une requête pour chaque filtre.

Fonctionnement sous MongoDB

Du coup nous avons décidé de migrer sous MongoDB. J'ai commencé à travailler sur cette migration le 26 juillet et celle-ci a été déployée en production cette semaine même (30 aout).

Les requêtes sous MongoDB sont beaucoup plus simples. Déjà car nous n'avons pas à nous préoccuper de créer des vues en javascript. Elles sont générées automatiquement par le moteur. Les opérateurs permettent de gérer tous nos besoins.

Nous utilisons mongoid (sous rails 2.3). Et un simple appel à la méthode where() avec un hash d'arguments correspondant à la recherche suffit pour récupérer la liste de nos enregistrements.

Nous n'avons même plus de problématique d'ordre des filtres. Nos recherches sont toujours effectuées sur un intervalle lorsqu'il y en a un ou une expression régulière lorsqu'il s'agit d'une chaine.

Le comment du pourquoi

Avec CouchDB, nous utilisions CouchRest. Avec MongoDB, nous utilisons Mongoid.

La principale probématique du "comment" fût la migration des données. Je ne souhaitais pas vraiment faire du trop bas niveau.

J'ai donc migré les modèles CouchDB vers le dossier lib/ (afin qu'ils ne soient pas chargés automatiquement en production) et je leur ai mis le contenu suivant :

module Couchdb  
    class Record < CouchRest::ExtendedDocument
        def self.to_s
            "Record"
        end
    end
end

Ainsi mon modèle récupère les données de CouchDB correspondant au namespace "Record" et non pas "Couchdb::Record" comme cela aurait été le cas originellement.

Puis dans une tâche rake, je parcours tous les documents CouchDB et les ajoute dans MongoDB.

Couchdb::Record.all.each do |record|
    record.delete '_rev'
    record.delete '_attachments'
    record.delete 'couchrest-type'
    r = Record.new record     
    puts "Record saved"
  end
  puts "There are now #{Record.count} records in the mongo db"

CouchRest, faisant étendre les modèles de Hashes (ce qui est une décision fortement discutable) rends la chose particulièrement aisée puisqu'il nous suffit de transmettre le document sans les paramètres métiers.

Vous noterez que nous ne supprimons pas le _id de CouchRest. Ainsi, notre document conserve le même id.

Cette tâche rake est exécutée manuellement lors du déploiement en production.

Conclusion

N'abandonnez pas CouchDB à cause de cet article ! Bien que ce moteur ne convienne, in fine, pas à nos besoins de recherche multi attributs, il n'en reste pas moins quelque chose d'excellent que je n'hésiterai pas à recommander.

Envie de travailler sur l'application que je viens de décrire ? Nous cherchons un développeur javascript/ruby pour me remplacer ! contactez moi pour plus d'informations

Par ailleurs la bonne nouvelle est que, MongoDB stockant ses données en json binaire, l'accès à celles-ci est beaucoup plus rapide. Nous n'avons pas encore de réelles statistiques en production. Mais le temps d'exécution de nos tests a été réduit de 20% !

Drag and Drop HTML 5

Après les WebSockets, dans cet article, nous allons voir une autre nouveauté apportée par HTML 5 : le déplacement d'éléments dans la page et le suivi de ceux-ci en javacript.

Une demonstration de ce que nous allons faire ici est disponible en ligne.

Pourquoi HTML5 ?

En effet ! On pourrait très bien utiliser divers frameworks tels que jQuery permettent assez simplement d'implémenter du drag & drop d'éléments dans une page. Plusieurs problèmes à cela cependant :

  • Vous devenez dépendant du framework jQuery
  • L'ergonomie de la chose est assez discutable dès que l'on a des objets un peu complexes (dans mon cas, des tableaux imbriqués, chaque élément pouvant être déplacé dans le tableau de base ou l'un des tableaux fils (ces tableaux fils compris)).
  • La lourdeur de la chose
  • L'accessibilité de la chose pour les personnes n'ayant pas de souris est également très discutable

C'est pourquoi nous allons chercher à implémenter une fonctionnalité de drag & drop simple en HTML5. Vous pouvez déjà tester ce drag & drop de manière fonctionnelle. Et nous allons voir comment le créer !

Le tag HTML

Nous allons créer dans notre page deux éléments simples qui seront les conteneurs.

<div class="box"></div>
<div class="box></div>

Puis dans le premier conteneur nous allons ajouter un élément qui pourra être déplacé vers le second.

<div class="box"><div class="element" id="first">Hey !</div></div>

Afin de visualiser un peu mieux ceux-ci, nous pouvons ajouter un peu de style à cela :

<style type="text/css">
    .box {
        border: 1px solid #CCC;
        width: 150px;
        height: 150px;
        float: left;
        margin: 10px;
    }
    .element {
        width: 50px;
        height: 50px;
        background-color: #CCCCCC;
        margin: 5px;
        padding: 3px
    }
</style>

Nous avons maintenant tous les éléments nécessaires pour faire du drag & drop simple dans notre page. Créons un petit peu de dynamisme dans la chose afin de pouvoir les déplacer.

Les évènements

Nous avons accès à plusieurs nouveaux évènements nous permettant de mettre en place notre drag & drop.

Déplacement de l'élément

Un seul d'entre eux sera utile pour le moment cependant : ondragstart. Cet évènement est déclenché dès que vous débutez le drag & drop sur un objet qui le permet.

Pour rendre un élément draggable, nous devons lui ajouter l'attribut draggable="true". Puis dans la boite qui le contient, nous ajoutons l'évènement approprié.

<script type="text/javascript">
    function dragStart(event) {
        event.dataTransfer.effectAllowed = 'move';
        event.dataTransfer.setData("Text", event.target.getAttribute('id'));
    }
</script>

<div class="box" ondragstart="dragStart(event);">
    <div id="first" class="element" draggable="true">Hey !</div>
</div>

Analysons un peu ce code.

L'objet dataTransfer

Chacune des deux méthodes utilisées fait partie de dataTransfer. dataTransfer est un nouvel objet de HTML5. Il permet de transférer des données entre les évènements. Ainsi, lorsque dans un évènement, nous modifions une donnée dans dataTransfer, celle-ci sera accessible à tous les autres évènements suivants. Nous pouvons donc sauvegarder des données et les récupérer dans un autre évènement. Ce qui va s'avérer particulièrement pratique pour déplacer le bon objet.

effectAllowed nous permet de définir le type de déplacement que nous autorisons pour ce drag and drop. Les divers déplacements autorisés sont les suivants :

  • all - L'élément peut être copié, déplacé et lié.
  • copy - L'élément peut être copié.
  • copyLink - L'élément peut être copié et lié.
  • copyMove - L'élément peut être copié et déplacé.
  • link - L'élément peut être lié.
  • linkMove - L'élément peut être déplacé et lié.
  • move - L'élément peut être déplacé.
  • none - L'élément ne peut pas être draggé.
  • uninitialized - Valeur par défaut. Le comportement est "move" pour les éléments éditables, "link" pour les ancres et "copy" pour les autres.

setData quant à lui nous permet de définir une valeur à notre dataTransfer. Ici, nous définissons l'identifiant de l'élément. Mais vous pourriez définir ce que vous désirez et qui vous permettra de retrouver cet élément par la suite.

La clé "Text" n'est pas une clé que vous pouvez définir comme vous le désirez. Cela correspond au format de la chaine que nous transmettons. Ici, du texte.
Certains navigateurs, comme Firefox acceptent n'importe quelle valeur. D'autres, comme Chrome en revanche, exigeront que vous utilisiez la bonne.

Notre objet est maintenant déplaçable dans la page. Il nous faut maintenant le dropper dans la seconde boite.

Droppage de l'élément

Se contenter de déplacer l'élément ne va pas nous emmener très loin. Afin de pouvoir dropper l'élément, nous allons ajouter deux évènements : ondragover et ondrop.

ondragover est est exécuté lorsque l'on passe au dessus de l'élément. Il va nous permettre ici d'autoriser ou non le droppage de l'élément dans la seconde boite. Par défaut, javascript n'accepte pas que l'on droppe l'élément. En conséquent on doit retourner false afin d'arrêter la propagation de l'évènement et de ne pas avoir le comportement par défaut (et donc de pouvoir dropper l'élément).

ondrop est exécuté lorsque l'on lache l'élément dans la seconde boite. C'est à ce moment la que nous déplaçons effectivement l'élément dans le dom.

Voici donc à quoi ressemblent nos deux boites maintenant.

<script type="text/javascript">
    function dragStart(event) {
        event.dataTransfer.effectAllowed = 'move';
        event.dataTransfer.setData("Text", event.target.getAttribute('id'));
    }

    function dragOver(event) {
        return false;
    }

    function drop(event) {
        var element = event.dataTransfer.getData("Text");
        event.target.appendChild(document.getElementById(element));
        event.stopPropagation();

        return false;
    }
</script>

<div class="box" ondragstart="dragStart(event);" ondragover="return dragOver(event);" ondrop="return drop(event);">
    <div id="first" class="element" draggable="true">Hey !</div>
</div>
<div class="box" ondragstart="dragStart(event);" ondragover="return dragOver(event);" ondrop="return drop(event);">

Essayez par vous même. Vous pouvez maintenant drag/dropper l'élément d'une boite à l'autre ! :)

Vous pouvez tester directement sur la demonstration en ligne de ce drag and drop.

Tous les évènements disponibles

Nous avons vu ici comment faire quelque chose de très simple. Cependant il vous est possible de complexifier la chose en fonction de vos besoins. Plusieurs évènements sont pour cela à votre disposition.

  • dragstart - Evènement représentant le début du déplacement. Est exécuté dès que vous commencez à bouger un élément.
  • drag - Evènement exécuté à intervales réguliers lors du déplacement (le référentiel HTML5 indique toutes les 350ms). Si il retourne faux, le déplacement sera arrêté et annulé.
  • dragenter - Evènement exécuté lorsque l'élément que vous déplacez arrive dans un autre élément.
  • dragleave - Evènement exécuté lorsque l'élément que vous déplacez sors d'un autre élément.
  • dragover - Evènement exécuté lorsque vous passez au dessus d'un élément. Si vous laissez cet évènement continuer sa propagation (en ne retournant pas "faux"), il ne sera pas possible de dropper l'objet que vous déplacez dans cet élément.
  • drop - Evènement exécuté lorsque vous dropper l'objet que vous êtes en train de déplacer dans un autre élément.
  • dragend - Evènement représentant la fin du déplacement. Exécuté lorsque celui-ci s'achève.

Conclusion

A l'heure actuelle, seul Opéra ne supporte pas le drag and drop. Toutes les versions récentes des navigateurs le supportent. Cette solution est bien évidemment à préférer à toute solution intégralement javascript qui ne fera que simuler ce qui est ici geré nativement par le navigateur.

10 raisons pour lesquelles tout développeur devrait avoir un blog technique

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.

CouchDB: recherche avec startkey et endkey

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

Read Eval Print Loop

Ou repl ! Le dernier projet open source de Chris Wanstrath. Il est plutôt productif. Donc je ne parle pas de tous ses projets, même si plusieurs sont particulièrement intéressants (jetez donc un coup d'oeil à resque et à fakefs (j'ai fait deux commits sur ce dernier)).

Mais revenons en à repl. Supposons que vous soyez en train de travailler sur un projet utilisant GIT. En bon barbu que vous êtes, vous n'utilisez évidemment pas d'interface graphique (c'est tout à fait faisable, j'en suis la preuve vivante). Cependant passer son temps à taper "git add", "git rm", "git commit", ... est parfois assez rébarbatif (j'éviterai le terme chiant). Pouvoir avoir un terminal spécific à git et avoir uniquement à taper "add", "rm", "commit" etc serait plus intéressant. Et c'est justement là que repl arrive. Pas seulement pour git, mais pour n'importe quelle commande linux !

Commençons pas installer la bête.

sudo gem install repl
Vous suivez toujours ? :mrgreen:

Maintenant utilisons notre nouveau jouet avec git justement.

repl git
Et la, la magie opère et vous êtes un nouvel homme (ou une femme. Pas de machisme). Vous avez un shell spécifiquement pour git.

Et bien évidemment, ça fonctionne avec n'importe quoi. Git, Apt-get, Gem, ... Et même votre propre exécutable fait maison :)

Oui mais comment ça fonctionne ?

Petit curieux va ! Bonne question ! Comme quoi la taille d'une application n'est pas proportionnelle à son utilisé, repl tient en tout et pour tout en 80 lignes de Ruby ! Et encore c'est parce qu'il a une fonctionnalité d'autocomplétion. On pourrait conserver ta fonctionnalité principale uniquement et le réduire à 20 lignes de code (on s'en tape les ...).

Voyons ici uniquement l'indispensable.

loop do
  print ENV['REPL_PROMPT'] || '>> '</p>

<p>  begin
    line = $stdin.gets.chomp
  rescue NoMethodError, Interrupt
    exit
  end</p>

<p>  puts "$ #{command} #{line}" if debug
  system "#{command} #{line}"
  warn "Use Ctrl-D (i.e. EOF) to exit" if line =~ /^(exit|quit)$/
end

Nous créons une boucle infinie (car elle ne sera rompue que par un Ctrl-D) afin d'avoir l'impression d'être dans une interface shell. Puis au début de chaque ligne, nous affichons les caractères ">>", toujours dans un souci d'esthétique.

Le $stdin.gets.chomp permet de récupérer toute commande tapée par l'utilisateur.

Puis, une fois que la commande a été tapée, on l'exécute et on affiche ce qu'elle retourne (fonction "system"). Si c'est "exit" qui est entrée, on invite la personne à taper Ctrl-D pour quitter repl.

Cool, simple, efficace et utile :)

CouchDB et requêtes avancées: les vues

Dans deux articles précédents, nous avons vu une introduction à CouchDB et l'utilisation de CouchDB avec Ruby. Nous sommes donc maintenant capable d'ajouter des données dans notre base et de récupérer une liste de ceux-ci. Mais il peut parfois arriver d'avoir besoin de faire des requêtes plus complexes.

Prenons par exemple une table CouchDB générée par CouchREST. Vous y verrez, pour chacun de vos modèles, un document nommé "_design/Model". Par exemple, "_design/File". Il ne s'agit en réalité par d'un document. Mais d'une vue. Regardons l'une des vues générées par CouchREST.

{
    "all": {
        "map": "function(doc) {
            if (doc['couchrest-type'] == 'Image') {
                emit(doc['_id'],1);
            }
        }"
   }
}
Vous lisez bien, les vues CouchDB sont du javascript ! :)

Après avoir chargé cette vue, tentez de visiter la page votre_base/_design/votre_vue/_view/all. Vous constaterez que seuls les éléments de type "Image" sont retournés. Notre fonction javascript est exécutée sur chacun des éléments. Et seuls ceux que vous désirez (en les insérant avec "emit") sont retournés.

Prenons le cas d'une plateforme de blog (oui cas bidon). Supposons que nous désirions visualiser uniquement les documents ayant l'attribut "published" à true.

{
    "all": {
        "map": "function(doc) {
            if (doc['published'] == true) {
                emit(doc['_id'], doc);
            }
        }"
    }
}
De la même manière que tout à l'heure, nous ne retournons que les billets publiés dans notre blog. Voici ainsi le contenu qui m'est retourné pour la vue précédente

{
    "total_rows":2,
    "offset":0,
    "rows":[
        {
            "id":"7c46156162a59d145cf9cf7850e6b677",
            "key":"7c46156162a59d145cf9cf7850e6b677",
            "value":{
                "_id":"7c46156162a59d145cf9cf7850e6b677",
                "_rev":"5-f726b6d7f469b686079fbe4c5f50726b",
                "title":"My First Blog Post",
                "content":"My Post Content",
                "published":true,
                "comments":[
                    {"author":"John","content":"First Comment"},
                    {"author":"James","content":"Second Comment"}
                ]
            }
        },
        {
            "id":"f14fde843b20e18561ea5e8055dbc3b3",
            "key":"f14fde843b20e18561ea5e8055dbc3b3",
            "value":{
                "_id":"f14fde843b20e18561ea5e8055dbc3b3",
                "_rev":"1-5eef22da217f5858542d175c41d2ef3d",
                "title":"My Second Post",
                "content":"Second Content",
                "published":true
            }
        }
    ]
}

Vous constatez que j'ai inséré plusieurs commentaires sur l'un de mes billets. Il ne s'agit pas de documents différents mais bien du même document. L'attribut "comments" est un tableau de commentaires, chacun pouvant contenir les éléments que je désire.

Et la, la problématique à laquelle on pense rapidement, c'est : "Mais comment je fais pour récupérer la liste de tous mes documents ?" Comme précédemment, avec une vue bien évidemment !

{
    "all": {
        "map": "function(doc) {
            for (var i in doc.comments) {
                emit(doc, doc.comments[i]);
            }
        }"
    }
}

Nous parcourons chacun de nos documents. Puis dans chacun de ceux-ci, nous parcourons tous les commentaires. Et insérons au resultat tous les commentaires. Le résultat obtenu est le suivant.

{
    "total_rows":2,
    "offset":0,
    "rows":[
        {
            "id":"7c46156162a59d145cf9cf7850e6b677",
            "key":"My First Blog Post",
            "value":{
                "author":"James",
                "content":"Second Comment"
            }
        },
        {
            "id":"7c46156162a59d145cf9cf7850e6b677",
            "key":"My First Blog Post",
            "value":{
                "author":"John",
                "content":"First Comment"
            }
        }
    ]
}
Nous avons bien la liste de tous nos commentaires. Et (vu que nous l'avons ajoutés au résultat), nous avons même le titre billet qui va avec ! :)

Et les performances dans tout ça ? Comme c'est de l'HTTP, il y a du cache natif, que CouchDB gère parfaitement bien. Si vous chargez plusieurs fois votre vue, vous verrez donc que la page retournée est bien un 304 not modified. Je n'ai pas testé avec énormément d'enregistrements. Mais j'ai discuté de cela avec Mirsal lundi soir lors de l'apéro Web Event Lyon. Et apparemment, même avec des milliers de billets, la chose ne posera pas trop de problèmes. Je reviendrai là-dessus dans quelques mois si il y a matière à en dire quelque chose ;)

Quoi qu'il en soit, je trouve les vues particulièrement intéressantes. Je me retrouve régulièrement frustré par les limitations de SQL. En développant celles-ci en Javascript, la chose devient virtuellement illimitée.

Mirsal disait également hier: "Les bases de données orientées document sont généralement ce qu'il y a de plus adapté. C'est SQL qui devrait être utilisé uniquement dans des cas atypiques." Je n'irai pas jusqu'à m'avancer comme cela. Mais les possibilités sont tellement impressionnantes que cela laisse rêveur.

Première introduction à CouchDB

Pour une raison que j'expliquerai dans un prochain billet, je m'intéresse actuellement à CouchDB. Il s'agit d'un serveur de base de données orienté document.

Plus clairement, chaque serveur va avoir plusieurs bases. Et chacunes de ces bases va avoir plusieurs documents. Chaque document pourra avoir un ou plusieurs champs et ces champs pourront varier en fonction du document.

Du coup pour une base "fichiers" par exemple, vous pourrez avoir un document qui représente une image et qui aura les champs "nom", "tumbnail" et "size". Et un document représentant un fichier texte et qui aura uniquement les champs "nom" et "size". Chaque document ne possède que les champs dont il a besoin.

Pour l'installation du serveur, je vous invite à vous référer à la documentation officielle. Une fois que cela est fait, passons à la suite de cet article.

CouchDB implémente un serveur HTTP acceptant des requêtes JSON. Mettons en place une librairie Ruby simple permettant de faire ces appels (et honteusement récupérée de la documentation CouchDB officielle).

require 'net/http'
module Couch
    class Server
        def initialize(host, port, options = nil)
            @host = host;
            @port = port;
            @options = options;
        end</p>

<p>        def delete(uri)
            request(Net::HTTP::Delete.new(uri))
        end</p>

<p>        def get(uri)
            request(Net::HTTP::Get.new(uri))
        end</p>

<p>        def put(uri, json)
            req = Net::HTTP::Put.new(uri)
            req["content-type"] = "application/json"
            req.body = json
            request(req)
        end</p>

<p>        def post(uri, json)
            req = Net::HTTP::Post.new(uri)
            req["content-type"] = "application/json"
            req.body = json
            request(req)
        end</p>

<p>        def request(req)
            res = Net::HTTP.start(@host, @port) {|http|
                http.request(req)
            }
            handle_error(req, res) if (not res.kind_of?(Net::HTTPSuccess))
            res
        end</p>

<p>        private
        def handle_error(req, res)
            e = RuntimeError.new("#{res.code}:#{res.message}\nMETHOD:#{req.method}\nURI:#{req.path}\n#{res.body}")
            raise e
        end
    end
end

Nous implémentons une méthode pour chacune des actions faisables avec tout serveur de base de données : listage des données, ajout d'un nouveau document, modification d'un document et suppression de celui-ci.

Utilisons cette librairie.

Détaillons ce que nous faisons.

server = Couch::Server.new("localhost", "5984")
Pour commencer, nous instancions la librairie CouchDB en précisant l'adresse du serveur et son port.

server.put("/foo/", "")
Puis nous créons une base de données nommée "foo". Une requête "PUT" sur l'url de la nouvelle base crée celle-ci.

doc = Hash.new(:name => 'Document Name', :content => 'The Content')
id = Digest::MD5.hexdigest(doc[:name] + '/' + doc[:content])
server.put("/foo/#{id}", doc.to_json)
Ici cela devient plus intéressant.

Nous commençons par créer un hash contenant tous les champs qui constitueront le document. Puis nous définissons l'identifiant de ce document.

Nous définissons celui-ci en une chaine md5 et non pas un simple entier. En effet si vous faites de la réplication de bases, vous risquez d'avoir des duplicats d'identifiants en se contentant d'un identifiant. Ici avec une chaine md5 contenant le contenu originel du document, il y a extrêmement peu de chances que nous ayons des duplicats.

Puis nous transmettons une requête permettant de créer le document ayant l'identifiant que nous venons d'attribuer avec le contenu donné.

p JSON.Parse(server.get("/foo/#{id}").body)
Pour finir nous lisons le contenu du document que nous venons de créer. Une requête GET sur l'url du document permet d'en retourner le contenu au format JSON, que nous parsons grâce à la librairie JSON Ruby. Il ne nous reste plus qu'à afficher le contenu de la manière dont nous le désirons.

Bien évidemment il existe de multiples librairies bien plus évoluées que ce que nous venons de faire. Par exemple CouchREST, ActiveCouch ou RelaxDB. Mais nous avons vu dans cet article les bases de l'utilisation de CouchDB et son fonctionnement. A vous de lire la documentation de ces projets si vous désirez les utiliser :)

Développer un widget Netvibes simple

Je me suis un peu amusé cette semaine en développant rapidement un widget Netvibes pourRefStats (annonce). Vous pouvez voir celui-ci et l'installer sur votre page.

Et comme c'est un composant de l'application web refstats, il est sur Github. Analysons rapidement la chose.

Nous avons tout d'abord des balises xml. Nous définissons l'auteur du plugin, la version de l'API utilisée et le fait qu'on est pas en debug.

<meta name="author" content="Damien MATHIEU" />
<meta name="description" content="Shows the evolution of your positions on Google and any other search engine via refstats.net." />
<meta name="apiVersion" content="1.0" />
<meta name="debugMode" content="false" />

Jusqu'à maintenant, pas grand chose d'intéressant. Mais si on descends un peu en dessous de l'inclusion des CSS de netvibes, on a :

<widget:preferences>
    <preference name="apikey" type="text" label="API Key" defaultValue="69dc57d333617ffcaa136109a0ff5e3d" />
    <preference name="engine" type="list" label="Engine" defaultValue="1" >
        <option value="0" label="all" />
        <option value="1" label="google.com" />
        <option value="2" label="google.fr" />
        <option value="4" label="google.ch" />
        <option value="5" label="google.be" />
        <option value="6" label="google.ca" />
        <option value="3" label="Yahoo!" />
    </preference>
</widget:preferences>

Pour comprendre ce que nous faisons, regardez ce que donne le widget (voir le lien "voir celui-ci" plus haut). Et cliquez sur le bouton "Editer". Vous constatez que nous avons les champs tels que définis dans les préférences ici.

Nous définissons ici un champ texte pour la clé d'API et une liste pour le moteur à utiliser. Mais il existe d'autres types : text, boolean, hidden, password, range, list. Ces options sont sauvegardées automatiquement lorsque vous validez et accessibles par la suite en javascript avec la méthode

widget.getValue('option_name');

Puisque nous parlons de javascript, attaquons-nous y. Vous pouvez voir un

widget.onLoad = function() {
[...]
}
Cette méthode est exécutée lors du chargement du widget. Et à chaque modification des paramètres de celui-ci. Les autres méthodes utilisées ici sont des méthodes internes au script. Je vous laisse les analyser vous même si cela vous amuse.

Vous constaterez cependant deux choses : Les appels ajax. L'API RefStats retourne du json. Je fais donc appel à la méthode UWA.Data.getJson, qui prends deux arguments : l'url de la page à appeller. Et la méthode à appeller avec les données une fois celles-ci reçues. A vous après de traiter vos données, que cela soit du json avec getJson, du xml avec getXml ou du texte avec getText.

Vous constaterez également l'utilisation de la méthode widget.setBody();. Cette méthode permet de définir le contenu du widget de manière dynamique. Ici, je redéfinis entièrement le contenu à chaque fois en me basant sur les données que j'ai en variables. Rien ne vous empêche cependant de manipuler le dom de manière plus fine. L'objet widget possède une méthode createElement.

Bon, cet article est particulièrement concis. Mais il deviendrait trop long si je commencais à détailler entièrement ce qui est déjà indiqué dans la documentation Netvibes. Je vous laisse donc vous y référer. Mais je pense que cela fait déjà un bon début. Rien qu'avec ça, il est déjà possible de faire deux ou trois trucs sympa :)

Symfony et Zend Framework : veuillez exécuter nos tests correctement

S'il vous plait ! Et je m'explique.

J'utilise, comme outil d'intégration continue, Integrity. Avec un "post-receive" sur le repository github, à chacun de mes commits, Integrity est mis au courant et il exécute les tests.

Ca fonctionne à merveille avec mes projets Rails (l'API RefStats et mon portfolio); mes projets Django (la documentation de RefStats). Mais pour les projets Zend Framework (l'appli web RefStats) et un autre sur lequel je vais bosser en aout et qui sera fait avec Symfony, c'est pas cool. Les tests, bien qu'ils ne passent pas tous, sont considérés par Integrity, comme valides.

La raison de cela est très simple : lorsqu'un processus console est exécuté, il peut retourner un code de status. Un peu comme le code HTTP sur les pages web. Ce code doit être à 0 pour que les tests soient valides et à une autre valeur, quelconque, si les tests ne passent pas. Voir notamment la documentation Integrity. En PHP, un simple exit(0) ou exit(1) à la fin des tests suffirait à exécuter la chose correctement.

Et vraissemblablement, Symfony tout comme le Zend Framework ne renvoient pas le bon code de status. Alors s'il vous plait, PHP a déjà pas mal de retard au niveau des tests automatisés d'applications, qu'il serait cool de rattraper. Faire ceci serait déjà un premier pas :)

Librairies globales ou pour chaque application ?

Le cas "général"

Lorsque l'on utilise un framework tel que Zend, Symfony ou encore n'importe quelle librairie, il y a deux écoles :

  • Installer une fois le framework dans un répertoire spécifique. Et toutes les applications situées sur la machine utilisent cette version.
  • Installer une version du framework pour chaque application.
A première vue, la première solution pourrait paraitre intéressante. On y gagne en place disque et on a pas des copies de fichiers similaires partout sur sa machine.

Cependant il s'agit d'une très mauvaise idée. Vous allez en effet vous retrouver bloqué sur une version de la librairie à utiliser pour toutes vos applications. Et lorsque une nouvelle version majeure sortira et que la rétrocompatibilité avec la version précédente ne sera pas forcément très poussée, soit vous ne mettrez pas à jour. Soit vous devrez mettre à jour toutes vos applications en même temps pour faire cette mise à jour. C'est un petit peu lourd. Autant donc dire que vous ne mettrez pas à jour. Et si vous le faites, vous ne prendrez probablement pas le temps de tester chaque application correctement et vous retrouverez avec des bugs.

En revanche si vous avez une version spécifique de la librairie spécialement pour l'application, vous pouvez mettre la librairie à jour pour une application, tester celle-ci proprement et puis passer à l'application suivante.

Et en versionnant la librairie globale

Avec Rails

Rails propose quelque chose d'assez sympatique. Dans le fichier config/environment.rb, vous avez RAILS_GEM_VERSION = '2.3.2' unless defined? RAILS_GEM_VERSION

Vous définissez la version de la librairie utilisée dans l'application. A côté de cela, vous avez donc plusieurs versions de rails installées dans le répertoire des librairies globales. Et vous pouvez choisir celle que vous désirez en fonction de l'application.

Plus de librairie inclue directement dans l'application (ce qui est tout de même assez crade). Plus de problèmes de changement de version multi applications. Vous pouvez installer Rails 2.3.2 et l'utiliser dans l'une de vos applications tout en ayant toutes les autres qui tournent encore avec Rails 2.2.

Symfony

Symfony possède un fichier config/ProjectConfiguration.class.php Dans lequel vous trouverez require_once dirname(__FILE__).'/../lib/symfony/autoload/sfCoreAutoload.class.php'; Remplacez le chemin par celui vers la version de Symfony de votre choix. Et le tour est joué !

Zend Framework

Avec Zend Framework, c'est en définissant le include_path que vous pouvez définir la version de la librairie que vous utilisez. Si vous avez, dans votre document public/index.php set_include_path( APPLICATION_PATH . '/../library' . PATH_SEPARATOR . APPLICATION_PATH . PATH_SEPARATOR . get_include_path() );

require_once 'Zend/Loader.php'; Zend_Loader::registerAutoload(); Il vous suffit de remplacer '/../library' par le chemin vers le dossier library versionné pour votre application.

Alors après entre utiliser des librairies versionnées et implémenter directement la version de votre choix dans l'application, c'est à vous de faire la part des choses. Je pense que cela dépends surtout des gouts. La seule chose à éviter est bien évidemment la librairie installée une seule fois, utilisée par toutes les applications et impossible à mettre à jour. Et ceci est bien évidemment faisable avec d'autres frameworks et librairies que les 3 mentionnés ici. Documentez vous ! ;)