1. CouchDB n'est pas votre base de données traditionnelle
Une base de données sans tables, sans SQL, taillée pour les applications parallèles (le cloud !), avec des transactions atomiques... CouchDB n'est pas votre base de données traditionnelle. Elle est conçue avant tout pour les applications web, même si son champ d'utilisation va probablement s'élargir rapidement. Dans CouchDB, on a un seul type d'objet : les documents. Un document est un simple conteneur d'informations. Chaque document est composé de propriétés. Il n'y a aucune contrainte sur le nombre de propriétés d'un document, sur le type de ces propriétés... Le protocole permettant d'accéder à un serveur CouchDB est basé sur HTTP, avec une interface REST.
Le plus dur, lorsqu'on commence à travailler avec CouchDB, est d'oublier nos vieux réflexes d'utilisation de bases de données classiques !
1.1 Des documents
L'élément fondamental qui constitue toute base de données classique est la table. Une table contient des champs (colonnes), définis à l'avance.
L'élément fondamental qui constitue une base de données CouchDB est le document. Un document est un conteneur de données. Il possède un identifiant unique, qui permet de le référencer de manière unique dans la base. Il n'y a aucune autre contrainte. Pour remplir les documents, on y ajoute des propriétés. Chaque propriété est constituée d'une clé, qui est une simple chaîne de caractères, et d'une valeur, qui peut être tout objet JSON valide.
Il y a trois clés qui sont imposées par CouchDB :
- _id : l'identifiant unique du document ;
- _rev: le numéro de révision du document, géré automatiquement par le moteur CouchDB ;
- _attachments : les éventuels fichiers attachés à ce document.
1.2 Un peu de JSON
JSON signifie Javascript Standard Object Notation. C'est un format permettant d'exprimer en format texte un objet Javascript. On peut le comparer à la sérialisation d'un objet.
Exemple d'objet JSON :
{
"type":"blog",
"title":"Mon premier post",
"body":"Pour une fois que je bloggue, ...",
"tags": [ "blog","premier","web" ]
}
Cet article ne détaille pas l'implémentation de JSON. Voyez pour cela http://json.org/. En résumé, les types JSON de base sont :
- une chaîne de caractères doit être mise entre quotes : "ceci est un string".
- un objet (qui supporte des données de type "clé" => valeur") est représenté par des accolades : { "key1": "value1", "key2":"value2" }.
- une liste de valeurs (un tableau) est représenté par des crochets : [ "value1","value2"].
- JSON supporte les booléens : true et false ainsi que null.
- JSON supporte les nombres flottants : 21.34576.
1.3 Protocole d'accès à CouchDB : RESTons simples
Comment interroger une base de données CouchDB ? Les développeurs du projet n'ont pas réinventé la roue : pas de protocole dédié comme pour MySQL ou PostgreSQL par exemple, mais simplement une interface basée sur le protocole HTTP : REST.
REST, pour Representational State Transfer, est connu comme une alternative simple au protocole SOAP, pour interfacer des web services. Rest n'est pas un protocole, c'est un style d'architecture. Ainsi, de nombreuses variantes REST sont implémentées selon les projets, et, malgré les querelles d'expert, on peut dire que les principales caractéristiques sont :
- REST est basé sur le protocole HTTP.
- Pour des requêtes de lecture, on utilise la méthode HTTP GET.
- Pour créer de nouveaux éléments, lorsqu'on ne spécifie pas d'identifiant unique, on utilise la méthode POST.
- Pour mettre à jour des éléments existants ou créer des éléments en spécifiant leur identifiant unique, on utilise la méthode PUT.
- Pour effacer des documents, on utilise la méthode DELETE.
En conséquence, tout langage informatique qui sait travailler avec des sockets HTTP, c'est-à-dire à peu près tous les langages, peut s'interfacer simplement avec CouchDB.
2. Installation de CouchDB
La version disponible, à l'heure où cet article est écrit, est la 0.9.
2.1 Installation sous Fedora 10
CouchDB est codé avec le langage Erlang et a besoin de l'interpréteur JavaScript. De plus, il nous faut le compilateur GCC et les outils make. Un simple :
% yum install erlang icu libicu-devel js js-devel libcurl-devel gcc make
doit mettre en place l'environnement nécessaire. Ensuite, un classique wget / configure/ make / make install :
% wget http://apache.crihan.fr/dist/couchdb/0.9.0/apache-couchdb-0.9.0.tar.gz
% tar xvzf apache-couchdb-0.9.0.tar.gz
% cd apache-couchdb-0.9.0
% ./configure --prefix=/usr/local/couchdb
% make
% make install
On installe CouchDB dans un répertoire dédié /usr/local/couchdb afin de pouvoir le mettre à jour ou l'effacer facilement au besoin.
Afin de ne pas compromettre la sécurité du système, on crée un utilisateur dédié :
% adduser -r -d /usr/local/couchdb/var/lib/couchdb couchdb
% chown -R couchdb /usr/local/couchdb/var/lib/couchdb
% chown -R couchdb /usr/local/couchdb/var/log/couchdb
% chown -R couchdb /usr/local/couchdb/var/run
Il ne nous reste plus qu'à lancer la bête :
% sudo -u couchdb /usr/local/couchdb/bin/couchdb
Si tout se passe bien, on doit voir les lignes suivantes :
Apache CouchDB 0.9.0 (LogLevel=info) is starting.
Apache CouchDB has started. Time to relax.
2.2 Installation sur Debian Lenny
CouchDB 0.9 n'est pas encore disponible pour Debian Lenny. Il faut par conséquent utiliser les dépôts « squeeze ». Dans le fichiers des dépôts Debian /etc/apt/sources.list, il faut rajouter la ligne suivante :
deb http://ftp.fr.debian.org/debian/ squeeze main contrib non-free
Ensuite, rechargez la liste des packages :
% apt-get update
Puis, installez couchdb :
% apt-get install couchdb
Enfin, n’ oubliez pas d'enlever ou de commenter la ligne qu'on a rajoutée dans /etc/apt/sources.list !
2.3 Test de disponibilité du serveur
Par défaut, CouchDB écoute sur le port 5984. On peut facilement tester l'accès à l'application en utilisant curl :
% curl http://localhost:5984/
{"couchdb":"Welcome","version":"0.9.0"}
CouchDB nous renvoie un objet JSON composé de deux propriétés, une première dont la clé est couchdb et qui a la valeur "Welcome", et une seconde propriété version dont la valeur est "0.9.0". Maintenant que CouchDB est fonctionnel, on va pouvoir s'amuser un peu !
3. Premiers pas avec CouchDB
3.1 Créer une base de données
La première étape va consister à créer une nouvelle base de données afin de faire nos tests. On spécifie le nom unique de la base de données lors de sa création. Il nous faut donc utiliser la méthode HTTP PUT. Ainsi, pour créer une base de données mydb, on envoie une requête PUT sur l'URL http://localhost:5984/mydb :
% curl -X PUT http://localhost:5984/mydb
{"ok":true}
La réponse du serveur est un objet JSON, qui nous précise que l'opération a abouti : cet objet comporte une clé ok égale au booléen TRUE.
Nous pouvons maintenant interroger notre base de données :
% curl -X GET http://localhost:5984/mydb
{"db_name":"mydb","doc_count":0,"doc_del_count":0,"update_seq":0,"purge_seq":0,"compact_running":false,"disk_size":4096,"instance_start_time":"1240577519746194"}
qui renvoie une liste d'informations utiles, dont le nombre de documents stockés dans notre base de données (0 pour l'instant).
3.2 Créer un document
Maintenant que notre base de données existe, nous allons créer un premier document. Pour illustrer cet article, nous allons prendre l'exemple bien connu d'une application de blogging. La structure d'un document qui représente une entrée de blog comportera les champs suivants :
- un champ type dont la valeur sera blog post.
- un champ date dont la valeur sera le timestamp Unix de création du blog.
- un champ title contenant le titre du post.
- un champ tags qui contiendra les labels représentant le contenu de mon post.
L'objet JSON représentant un post aura donc la forme :
{
"type": "blog post",
"date": "1240584533",
"title": "Mon premier post",
"tags": [ "couchdb","test","exemple" ]
}
Pour enregistrer ce document sur CouchDB, il faut employer la méthode HTTP POST, car nous ne spécifions pas l'identifiant unique de notre document :
% curl -X POST -d '{"type": "blog post","date": "1240584533","title": "Mon premier post","tags": [ "couchdb","test","exemple" ]}' http://localhost:5984/mydb
{"ok":true,"id":"ec06f2bb78b41d7275c0ea8a33ce214a","rev":"1-2098454642"}
On remarque que CouchDB nous renvoie un objet JSON contenant la réponse à la requête ("ok" : true), l'identifiant unique du document (id), ainsi que son numéro de révision (rev).
Pour récupérer le document, on utilise la méthode HTTP GET, en pointant sur l'URL composée du nom de la base (mydb) le séparateur /, puis l'identifiant unique du document :
% curl -X GET http://localhost:5984/mydb/ec06f2bb78b41d7275c0ea8a33ce214a
{
"_id":"ec06f2bb78b41d7275c0ea8a33ce214a",
"_rev":"1-2098454642",
"type":"blog post",
"date":"1240584533",
"title":"Mon premier post",
"tags":["couchdb","test","exemple"]
}
Le serveur CouchDB a rajouté au document initial une clé _id qui contient l'identifiant unique de ce document, ainsi qu'une clé _rev qui contient le numéro de révision de la dernière version du document.
Il est possible de spécifier l'identifiant unique souhaité lorsqu'on crée un document. Dans ce cas, il faut utiliser la méthode HTTP PUT et spécifier l'identifiant souhaité dans l'URL. Par exemple :
% curl -X PUT -d '{"type": "blog post","date": "1240584533","title": "Mon premier post","tags": [ "couchdb","test","exemple" ]}' http://localhost:5984/mydb/premier_post
{"ok":true,"id":"premier_post","rev":"1-954474519"}
Les numéros de révision avec CouchDB
CouchDB associe à chaque document un numéro de révision. Lorsqu'un document est créé, un numéro de révision lui est assigné. Lorsque ce document est mis à jour, son numéro de révision change. Ceci permet d'être certain, lorsqu'on met à jour un document, qu'il n'y a pas eu d'autres mises à jour entre le moment ou on a lu le document et le moment où on met à jour. Par conséquent, lorsqu'on met à jour ou qu'on efface un document, on doit spécifier le numéro de révision sur lequel on travaille.
3.3 Mettre à jour un document
Pour mettre à jour un document, il convient d'utiliser la méthode PUT. De plus, il faut rajouter dans le document le numéro de la dernière révision.
Nous allons modifier le titre de notre document premier_post en "Mon tout premier post" :
% curl -X PUT -d '{"_rev":"1-954474519","type":"blog post","date": "1240584533","title":"Mon tout premier post","tags": [ "couchdb","test","exemple" ]}' http://localhost:5984/mydb/premier_post
{"ok":true,"id":"premier_post","rev":"2-914774123"}
La mise à jour s'est bien déroulée, et CouchDB spécifie le nouveau numéro de révision de notre document. Si, entre le moment où nous avons lu le document, et le moment où nous l'avons mis à jour, une autre application l'avait mis à jour, le serveur nous aurait répondu :
{"error":"conflict","reason":"Document update conflict."}
3.4 Effacer un document
Il convient d'utiliser la méthode DELETE pour effacer un document. Là encore, on doit spécifier le numéro de révision sur lequel on travaille, afin de pouvoir gérer les éventuels conflits :
% curl -X DELETE 'http://localhost:5984/mydb/premier_post?rev=2-914774123'
{"ok":true,"id":"premier_post","rev":"3-207179598"}
Là encore, CouchDB renvoie un objet JSON qui indique si l'opération a abouti. Il renvoie aussi un nouveau numéro de révision : en effet, avec CouchDB, le document ne sera effectivement effacé de la base de données que lorsqu'on effectuera sur celle-ci une opération de maintenance dite « compact ».
3.5 Compacter la base de données
Au niveau du disque, une base de données CouchDB est un fichier ouvert en mode « append only » : les écritures des différentes modifications de la base sont ajoutées au fichier. Ceci permet au serveur CouchDB d'assurer des transactions atomiques, et de redémarrer instantanément en cas de crash du serveur. Par contre, l'administrateur de bases de données CouchDB doit, de temps à autre, lancer l'opération « compact » sur les bases de données. Lors de cette opération, la base de données est toujours accessible en lecture/écriture, mais les performances peuvent être dégradées.
L'opération « compact » effectue deux purges principales sur la base de données : il efface effectivement du disque les documents effacés via la méthode DELETE, et il oublie aussi les anciennes révisions de chaque document, ne gardant que la dernière. Il ne faut donc pas utiliser les révisions de document CouchDB pour créer une application de type contrôle de version (CVS, subversion) !
Pour compacter une base de données, on utilise la méthode POST sur l'URL de la base, suivi du mot-clé _compact :
% curl -X POST http://localhost:5984/mydb/_compact
{"ok":true}
4. Les vues
CouchDB permet donc de stocker des documents, de les lire, de les mettre à jour... mais comment effectuer des requêtes ? En effet, si on a besoin de connaître l'identifiant unique de chaque document pour le retrouver, ça ne sert pas à grand chose. CouchDB répond à ce besoin avec un outil extrêmement puissant : les vues.
Les vues permettent de réaliser les opérations de sélections, de classement, etc. sur les documents contenus dans la base de données. Le concept fondamental derrière les vues se nomme map/reduce (http://fr.wikipedia.org/wiki/MapReduce). Les vues peuvent être codées en plusieurs langages. Nous nous bornerons ici au langage prévu par défaut : Javascript !
L'opération map itère sur tous les documents de la base, et permet d'y appliquer un algorithme, puis d'exporter la réponse sous forme de valeurs associées à des clés. L'opération reduce permet d'itérer sur les ensembles clés-valeurs créées par l'opération map, pour en ressortir une valeur unique (par exemple des sommes, des moyennes). La théorie est un peu compliquée, continuons donc par des exemples !
4.1 Création d'une vue : les design documents
Les vues sont stockées dans des documents spéciaux, appelés « design documents ». On peut déclarer plusieurs vues dans un design document, permettant de regrouper les vues par famille. Un design document a les spécificités suivantes :
- Son nom est précédé de _design/, par exemple _design/blog.
- Il contient une clé language qui indique l'interpréteur à utiliser pour parser les vues (exemple "javascript").
- Il contient un objet views, objet dont chaque clé est le nom d'une vue, et dont la valeur est elle-même un objet, devant comporter la clé map, qui contient la fonction qui réalise l'opération de map, et pouvant comporter la clé reduce, qui contient la fonction qui réalise l'opération reduce.
La fonction déclarée dans map prend un unique argument : le document (au format JSON). À chaque fois que l'on veut exporter, dans la réponse de la vue, une clé (et sa valeur), on utilise la fonction emit (clé à exporter, valeurs à exporter).
4.2 Sélection des posts
Pour commencer simplement, nous allons créer une vue qui liste tous les documents de la base de données qui sont des posts de blog. Pour cela, nous testons si le document contient une clé type dont la valeur est "blog post". La fonction javascript à utiliser est :
function (doc) {
// test sur la clé type du document
if ( doc.type && doc.type == 'blog post' ) {
// le document courant est bien un blog post:
// on utilise la fonction map pour le rajouter
// dans les résultats de la vue
emit ( null , doc );
}
}
Il faut maintenant créer le design document. Dans cet exemple, le document est appelé blog et la vue posts. L'objet JSON qui représente le document est de la forme :
{
"language": "javascript",
"views" : {
"posts": {
"map": "function (doc) {
if ( doc.type && doc.type == \"blog post\" ) { emit ( null , doc ); }
}"
}
}
}
Comme pour tout document CouchDB, on l'enregistre en utilisant la méthode HTTP PUT. On spécifie son nom dans l'URL.
% curl -X PUT -d "{\"language\":\"javascript\",\"views\":{\"posts\":{\"map\":\"function (doc) {if ( doc.type && doc.type == \\\"blog post\\\" ) { emit ( null , doc ); }}\"}}}" http://localhost:5984/mydb/_design/blog
{"ok":true,"id":"_design/blog","rev":"1-3765146518"}
Pour interroger une vue, l'URL CouchDB doit être composée de la façon suivante :
http://localhost:5984/[database]/_design/[nom du design document]/_view/[nom de la vue]
ce qui donne dans cet exemple : http://localhost:5984/mydb/_design/blog/_view/posts.
Une requête GET vers le serveur CouchDB donne les résultats suivants :
% curl -X GET http://localhost:5984/mydb/_design/blog/_view/posts
{"total_rows":0,"rows":[]}
En effet, la base de données ne contient plus aucun post. Nous repeuplons donc la base avec un premier_post :
% curl -X PUT -d '{"type": "blog post","date": "1240584533","title": "Mon premier post","tags": [ "couchdb","test","exemple" ]}' http://localhost:5984/mydb/premier_post
{"ok":true,"id":"premier_post","rev":"1-2260723687"}
L'interrogation de la vue donne alors le résultat suivant :
% curl -X GET http://localhost:5984/mydb/_design/blog/_view/posts
{"total_rows":1,"offset":0,"rows":[
{"id":"premier_post","key":null,"value":{"_id":"premier_post","_rev":"1-2260723687","type":"blog post","date":"1240584533","title":"Mon premier post","tags":["couchdb","test","exemple"]}}
]}
Afin de rendre l'exemple plus parlant, on ajoute un second post :
% curl -X PUT -d '{"type": "blog post","date": "1240835988","title": "Les vues CouchDB","tags": [ "couchdb","vues" ]}' http://localhost:5984/mydb/second_post
{"ok":true,"id":"second_post","rev":"1-164584844"}
Et on interroge de nouveau la vue :
% curl -X GET http://localhost:5984/mydb/_design/blog/_view/posts
{"total_rows":2,"offset":0,"rows":[
{"id":"premier_post","key":null,"value":{"_id":"premier_post","_rev":"1-2260723687","type":"blog post","date":"1240584533","title":"Mon premier post","tags":["couchdb","test","exemple"]}},
{"id":"second_post","key":null,"value":{"_id":"second_post","_rev":"1-164584844","type":"blog post","date":"1240835988","title":"Les vues CouchDB","tags":["couchdb","vues"]}}
]}
4.3 Grouper les posts par date
L'utilisation d'une vue peut être basique, comme dans l'exemple précédent. Mais, en utilisant plus puissamment les possibilités de la fonction emit(), CouchDB peut effectuer des pré-traitements forts utiles dans le cadre d'applications web. Dans l'exemple suivant, nous allons grouper les posts par jour. De plus, au lieu d'exporter, dans les valeurs, le document entier, nous n'exporterons que son titre (clé title).
4.3.1 Un peu de Javascript : Date
L'objet Javascript Date permet de manipuler... les dates. On obtient un nouvel objet Date facilement, mais, pour le positionner à la date que l'on souhaite, il faut lui passer le nombre de millisecondes depuis le premier janvier 1970. Il se trouve que dans les enregistrements de posts contenus dans la base de données mydb, les dates sont spécifiées en Unix Timestamp, c'est-à-dire en nombre de secondes depuis le premier janvier 1970. Nous devons donc multiplier ce nombre par 1000 :
var postDate = new Date();
postDate.setTime(doc.date * 1000);
Nous pouvons ensuite exporter l'année du post avec postDate.getFullYear(), le mois avec postDate.getMonth()+1 (les mois Javascript vont de 0 à 11), le jour du mois avec postDate.getDate().
On obtient la fonction :
function (doc) {
if ( doc.type && doc.type == 'blog post' ) {
var postDate = new Date();
postDate.setTime(doc.date * 1000);
var postKey= [ postDate.getFullYear() , postDate.getMonth()+1 , postDate.getDate() ];
emit ( postKey , doc.title );
}
}
4.3.2 Création de la vue posts_by_date
Il reste à modifier le design document blog pour lui ajouter la vue posts_by_date :
% curl -X PUT -d "{\"language\":\"javascript\",\"views\":{\"posts\":{\"map\":\"function (doc) {if ( doc.type && doc.type == \\\"blog post\\\" ) { emit ( null , doc ); }}\"},\"posts_by_date\":{\"map\":\"function (doc) {\\n if ( doc.type && doc.type == \\\"blog post\\\" ) {\\n var postDate = new Date();\\n postDate.setTime(doc.date * 1000);\\n var postKey= [ postDate.getFullYear() , postDate.getMonth()+1 , postDate.getDate() ];\\n emit ( postKey , doc.title ); \\n }\\n}\"}},\"_rev\":\"1-3765146518\"}" http://localhost:5984/mydb/_design/blog
{"ok":true,"id":"_design/blog","rev":"2-3892991535"}
L'appel de cette nouvelle vue donne comme résultat :
% curl -X GET http://localhost:5984/mydb/_design/blog/_view/posts_by_date
{"total_rows":2,"offset":0,"rows":[
{"id":"premier_post","key":[2009,4,24],"value":"Mon premier post"},
{"id":"second_post","key":[2009,4,27],"value":"Les vues CouchDB"}
]}
Cette vue classe bien les posts par date : la variable key est composée d'un tableau contenant l'année, le mois et le jour de chaque post. De plus, la valeur associée à chaque clé est bien le titre de chaque post.
L'appel des vues CouchDB supporte des paramètres de requête qui permettent de sélectionner uniquement un échantillon des résultats. Les paramètres les plus utiles sont key, startkey et endkey.
Par exemple, pour obtenir les posts datés du 24 avril 2009, nous utiliserons le paramètre key=[2009,4,24]. Ce paramètre étant passé comme variable dans l'URL, il faut l'encoder au préalable, ce qui donne %5B2009%2C4%2C24%5D.
% curl -X GET http://localhost:5984/mydb/_design/blog/_view/posts_by_date?key=%5B2009%2C4%2C24%5D
{"total_rows":2,"offset":0,"rows":[
{"id":"premier_post","key":[2009,4,24],"value":"Mon premier post"}
]}
Pour sélectionner un intervalle, on utilise les paramètres startkey et endkey. Pour sélectionner tous les posts d'avril 2009, on peut utiliser :
startkey : [2009,4,1]
endkey : [2009,4,32]
% curl -X GET 'http://localhost:5984/mydb/_design/blog/_view/posts_by_date?startkey=%5B2009%2C4%2C1%5D&endkey=%5B2009%2C4%2C32%5D'
{"total_rows":2,"offset":0,"rows":[
{"id":"premier_post","key":[2009,4,24],"value":"Mon premier post"},
{"id":"second_post","key":[2009,4,27],"value":"Les vues CouchDB"}
]}
4.3.3 Contrainte sur l'algorithme de la fonction map
Quel que soit le code contenu dans la fonction map, il doit respecter une unique règle : en ayant en entrée le même document, la fonction doit toujours générer le même résultat. On appelle cela « l'intégrité référentielle ». Par exemple, une fonction map, qui teste la date d'un document par rapport à « aujourd'hui » et exporte une clé si la date est la même, n'est pas une fonction map correcte. En effet, si on rejoue la fonction demain, pour le même document en entrée, le résultat sera différent.
4.4 L'opération reduce
Plus compliquée que map, la fonction complémentaire reduce prend une liste créée par la fonction map, et y applique un algorithme. La sortie d'une fonction reduce doit donc être un élément unique.
La fonction reduce prend trois arguments en entrée : key, values et rereduce.
Dans le cas le plus simple, l'argument rereduce est égal à false. Alors :
- key contient un tableau des clés exportées dans la fonction map, et des identifiants uniques ayant généré ces clés. Si on reprend l'exemple précédent de la vue posts_by_date, key sera alors :
[ [ [2009,4,24] , "premier_post" ] , [ [2009,4,27], "second_post" ] ]
- values contient un tableau des valeurs associées aux clés exportées, soit dans notre exemple :
[ "Mon premier post" , "Les vues CouchDB" ]
Il se trouve que la fonction reduce doit aussi être capable de prendre comme argument en entrée... le résultat qu'elle même a généré ! On appelle cet évènement rereduce. Dans ce cas :
- L'argument rereduce est le booléen true.
- L'argument key est null.
- L'argument values est un tableau comportant le résultat d'un, ou plusieurs, précédent(s) appel(s) de la fonction reduce.
Dans beaucoup de cas cependant, il n'est pas utile de traiter différemment ces deux cas de figure.
4.4.1 Nombre de posts par jour
On peut modifier notre vue posts_by_date pour afficher le nombre de posts par jour.
On modifie la fonction map pour que la valeur exportée ne soit plus le titre du post, mais l'entier 1. On obtient la fonction :
function (doc) {
if ( doc.type && doc.type == 'blog post' ) {
var postDate = new Date();
postDate.setTime(doc.date * 1000);
var postKey= [ postDate.getFullYear() , postDate.getMonth()+1 , postDate.getDate() ];
emit ( postKey , 1 );
}
}
Puis, on crée la fonction reduce, dont le rôle est d'additionner tous les éléments contenus dans le tableau values :
function (key,values,rereduce) {
return sum(values);
}
Reste à mettre à jour le design document blog :
% curl -X PUT -d "{\"_rev\":\"2-3892991535\",\"language\":\"javascript\",\"views\":{\"posts\":{\"map\":\"function (doc) {if ( doc.type && doc.type == \\\"blog post\\\" ) { emit ( null , doc ); }}\"},\"posts_by_date\":{\"map\":\"function (doc) {\\n if ( doc.type && doc.type == \\\"blog post\\\" ) {\\n var postDate = new Date();\\n postDate.setTime(doc.date * 1000);\\n var postKey= [ postDate.getFullYear() , postDate.getMonth()+1 , postDate.getDate() ];\\n emit ( postKey , 1 ); \\n }\\n}\",\"reduce\":\"function(key,values,rereduce) {\\n\\treturn sum(values);\\n}\"}}}" http://localhost:5984/mydb/_design/blog
{"ok":true,"id":"_design/blog","rev":"3-3373016540"}
Afin de rendre l'exemple plus parlant, on rajoute un troisième post, qui a la même date que le second :
% curl -X PUT -d '{"type": "blog post","date": "1240835988","title": "map-reduce me","tags": [ "couchdb","vues" ]}' http://localhost:5984/mydb/troisieme_post
{"ok":true,"id":"troisieme_post","rev":"1-1904998968"}
Si on appelle la vue posts_by_date, on obtient 3, soit le nombre total de posts dans la base :
% curl -X GET http://localhost:5984/mydb/_design/blog/_view/posts_by_date{"rows":[
{"key":null,"value":3}
]}
Afin d'avoir le nombre de posts par jour, il faut demander au serveur CouchDB de grouper les résultats par clé. Pour cela, on utilise le paramètre de requête group que l'on positionne à true :
% curl -X GET 'http://localhost:5984/mydb/_design/blog/_view/posts_by_date?group=true'
{"rows":[
{"key":[2009,4,24],"value":1},
{"key":[2009,4,27],"value":2}
]}
4.4.2 Contraintes sur l'algorithme de la fonction reduce
Outre l'intégrité référentielle, que la fonction reduce doit respecter, elle doit aussi être :
- commutative, c'est-à-dire avoir le même résultat quel que soit l'ordre du tableau values :
f(Key, [ A , B ] ) = f(Key, [ B , A ] )
- associative, c'est-à-dire avoir le même résultat quelle que soit la manière dont on regroupe partiellement les values :
f(Key, [ f(Key, [ A , B ]), f(Key, [ C ]) ] ) = f(Key, [ f(Key, [ A , C ]), f(Key, [ B ]) ] )
- idempotent, c'est-à-dire capable de prendre en entrée (dans le paramètre values) sa propre sortie :
f(Key, Values) == f(Key, [ f(Key, Values) ] )
Et pourquoi toutes ces contraintes ? Pour pouvoir exécuter ces traitements sur plusieurs serveurs en parallèle ! C'est, selon la légende, la signification de C.O.U.C.H : Cluster Of Unreliable Commodity Hardware.
5. Il n'y a plus de joint ?
Il n'y a pas de SQL dans CouchDB, donc impossible d'utiliser les jointures. Reprenons l'exemple précédent, en ajoutant à un blog post des commentaires. Nous décidons qu'un document commentaire de post aura une propriété type avec la valeur blog comment, une propriété post dont la valeur sera l'identifiant unique du post auquel ce commentaire est lié et un champ body qui contient le commentaire. Nous allons insérer deux commentaires à notre post premier_post :
{
"type": "blog comment",
"post": "premier_post",
"body": "un premier exemple de commentaire"
}
{
"type": "blog comment",
"post": "premier_post",
"body": "et encore un commentaire"
}
Nous utilisons la méthode HTTP POST pour enregistrer ces commentaires, car nous ne spécifions pas l'identifiant unique :
% curl -X POST -d '{"type":"blog comment","post":"premier_post","body":"un premier exemple de commentaire"}' http://localhost:5984/mydb
{"ok":true,"id":"7d2455d233d5a927d6b3e55db25f9144","rev":"1-1507876865"}
% curl -X POST -d '{"type":"blog comment","post":"premier_post","body":"et encore un commentaire"}' http://localhost:5984/mydb
{"ok":true,"id":"d453c2c87d74d8cab9367e674a78c0bc","rev":"1-88910194"}
5.1 Récupérer les commentaires : la méthode triviale
Pour récupérer les commentaires associés au post premier_post, la première idée qui vient à l'esprit est de créer une vue commentaires, qui se comporte comme une hash-table, en émettant l'identifiant unique du post dans la clé, et l'identifiant du commentaire en valeur. On obtient alors une fonction map du type :
function (doc) {
if ( doc.type && doc.type == "blog comment" ) {
emit(doc.post,doc._id);
}
}
L'interrogation de cette vue, en utilisant la variable key égale à l'identifiant unique du post, donne :
% curl -X GET 'http://localhost:5984/mydb/_design/blog/_view/commentaires?key="premier_post"'
{"total_rows":2,"offset":0,"rows":[
{"id":"7d2455d233d5a927d6b3e55db25f9144","key":"premier_post","value":"7d2455d233d5a927d6b3e55db25f9144"},
{"id":"d453c2c87d74d8cab9367e674a78c0bc","key":"premier_post","value":"d453c2c87d74d8cab9367e674a78c0bc"}
]}
Bien que cela fonctionne, ce n'est pas optimal ; il nous faut au minimum deux requêtes à la base : une première pour récupérer les informations sur le post, une seconde pour les informations sur les commentaires.
5.2 La puissance des clés : view collation
Une méthode plus optimale, pour obtenir toutes les informations en une requête, est d'utiliser une caractéristique des clés exportées dans les vues : les clés peuvent être des tableaux. On peut donc créer une vue qui contienne non seulement le post, mais aussi ses commentaires associés
function (doc) {
if ( doc.type && doc.type == "blog post" ) {
// le doc est un post, je crée une clé tableau avec comme
// premier élément l'id du post et comme deuxième élément
// le mot-clé post
var mykey = [ doc._id , "post" ];
emit(mykey,null);
} else if ( doc.type && doc.type == "blog comment" ) {
// le doc est un commentaire, ma clé tableau comporte alors comme
// premier élément l'id du post, et comme deuxième le mot-clé comment
var mykey = [ doc.post , "comment" ];
emit(mykey,null);
}
}
et enregistrer cette vue, par exemple, sous le nom post_complet. Pour récupérer toutes les données en une seule requête, on utilise les paramètres de requête startkey = [ "premier_post" , null ] et endkey = [ "premier_post" , "zzzzz" ]. On obtient alors :
% curl -X GET 'http://localhost:5984/mydb/_design/blog/_view/post_complet?startkey=%5B+%22premier_post%22+%2C+null+%5D&endkey=%5B+%22premier_post%22+%2C+%22zzzz%22+%5D'
{"total_rows":5,"offset":0,"rows":[
{"id":"7d2455d233d5a927d6b3e55db25f9144","key":["premier_post","comment"],"value":null},
{"id":"d453c2c87d74d8cab9367e674a78c0bc","key":["premier_post","comment"],"value":null},
{"id":"premier_post","key":["premier_post","post"],"value":null}
]}
C'est pas mal, mais nous obtenons les identifiants des documents qui nous intéressent, et non les documents entiers. Heureusement, un paramètre de requête permet de spécifier au serveur CouchDB d'inclure les documents dans la réponse, include_docs=true :
% curl -X GET 'http://localhost:5984/mydb/_design/blog/_view/post_complet?startkey=%5B+%22premier_post%22+%2C+null+%5D&endkey=%5B+%22premier_post%22+%2C+%22zzzz%22+%5D&include_docs=true'
{"total_rows":5,"offset":0,"rows":[
{"id":"7d2455d233d5a927d6b3e55db25f9144","key":["premier_post","comment"],"value":null,
"doc":{"_id":"7d2455d233d5a927d6b3e55db25f9144","_rev":"1-1507876865","type":"blog comment","post":"premier_post","body":"un premier exemple de commentaire"}},
{"id":"d453c2c87d74d8cab9367e674a78c0bc","key":["premier_post","comment"],"value":null,
"doc":{"_id":"d453c2c87d74d8cab9367e674a78c0bc","_rev":"1-88910194","type":"blog comment","post":"premier_post","body":"et encore un commentaire"}},
{"id":"premier_post","key":["premier_post","post"],"value":null,
"doc":{"_id":"premier_post","_rev":"1-2260723687","type":"blog post","date":"1240584533","title":"Mon premier post","tags":["couchdb","test","exemple"]}}
]}
Voilà comment, en une seule vue, et donc, une seule requête, une application est capable de récupérer un élément, et tous ses éléments associés !
6. Futon : l'interface d'administration embarquée
Intégré à CouchDB, Futon est une interface d'administration web. Elle permet de faire quasiment toutes les opérations sur le serveur CouchDB. On y accède en allant à l'URL http://localhost:5984/_utils.
Conclusion
De nombreux projets s'appuient déjà sur CouchDB, depuis les systèmes embarqués, jusqu'aux nuages de serveurs. En utilisant des protocoles connus, ouverts et éprouvés, les concepteurs ont donné à leur création de solides bases. En choisissant le modèle de développement open source, ils disposent d'une large audience, d'un développement rapide et de plein de bonnes idées qu'ils n'auraient pas eues eux-mêmes. Enfin, en repensant entièrement le concept de stockage de données, ils permettent aux développeurs d'imaginer des applications nouvelles, plus souples et au moins aussi puissantes.
Michaël Bailly
Références :
- CouchDB : http://couchdb.apache.org : site officiel, http://wiki.apache.org/couchdb/ : là où ça se passe.
- REST : http://fr.wikipedia.org/wiki/Representational_state_transfer
- JSON : http://json.org/
- Javascript : http://fr.wikipedia.org/wiki/JavaScript
- Map/Reduce : http://fr.wikipedia.org/wiki/MapReduce, http://labs.google.com/papers/mapreduce.html
Entité | URL |
Serveur CouchDB | Http://[serveur]:5984/ |
Base de données | Http://[serveur]:5984/[base] |
Document | Http://[serveur]:5984/[base]/[document id] |
Design document | Http://[serveur]:5984/[base]/_design/[document id] |
Vue | Http://[serveur]:5984/[base]/_design/[document id]/_view/[nom de la vue] |
Résumé des URL REST CouchDB
Paramètre | Type | Description |
key | Objet json | Restreint la sélection à la clé spécifiée |
startkey | Objet json | Restreint le début de la sélection à la clé spécifiée |
startkey_docid | String | Restreint le début de la sélection au document spécifié |
endkey | Objet json | Restreint la fin de la sélection à la clé spécifiée |
endkey_docid | String | Restreint la fin de la sélection au document spécifié |
limit | integer | Restreint le nombre d'éléments retournés au nombre spécifié |
stale | Mot-clé « ok » | Utilise la vue en mémoire si disponible, en ne voyant peut-être pas les dernières données insérées |
descending | boolean | Ordonne les résultats en classant les clés par ordre descendant |
skip | integer | Omettre les X premiers résultats |
group | boolean | Grouper les résultats des vues ayant une fonction reduce par clé |
reduce | boolean | Si « false », dans le cadre d'une vue avec une fonction reduce définie, ne pas appliquer cette fonction |
include_docs | boolean | Si « true », inclut les documents en plus des valeurs spécifiées par la fonction emit() |
Résumé des paramètres de requête des vues