Vous allez découvrir comment on pourrait se servir de CouchDB comme base de donnée pour un blog.
Il y a mieux, il y a pire, mais c'est une manière de faire.
Voici comment on va organiser les documents qui contiendront les billets :
Regardons ça :
{
"_id": "hello-world",
"Type": "billet",
"Subject": "Hello, World !",
"Author": "Arthur",
"PostedDate": "2009-10-26 13:54:32",
"Tags": ["blog", "cuisine"],
"Body": "Bienvenue sur mon nouveau blog consacré à la cuisine."
}
On en montre deux autres pour les exemples qui vont suivre :
{
"_id": "j-aime-le-chocolat",
"Type": "billet",
"Subject": "J'aime le chocolat",
"Author": "Arthur",
"PostedDate": "2009-10-28 15:05:23",
"Tags": ["chocolat", "cuisine", "gâteau"],
"Body": "Je me suis rendu compte aujourd'hui que j'aimais le chocolat, et surtout les gâteaux au chocolat."
}
{
"_id": "mousse-au-chocolat-la-recette",
"Type": "billet",
"Subject": "Mousse au chocolat, la recette",
"Author": "Arthur",
"PostedDate": "2009-10-28 17:55:19",
"Tags": ["chocolat", "cuisine", "dessert", "recette"],
"Body": "Bla bla chocolat, bla bla blanc en neige, bla bla mousse au chocolat. Régalez-vous !"
}
Remarquez que chaque champ du document est un objet JSON. Par exemple les champs Type, PostedDate, Subject… sont des chaînes de caractères (entre guillemet donc). Le champs Tags est lui un tableau (de chaînes de caractères, mais ça aurait pu être un tableau d'objets JSON quelconques : tableau de tableau…).
À ce stade, le terme de vues n'est pas très parlant.
Une vue sert à récupérer des couples (clé, valeur) en rapport avec les documents. Voyons ça par la pratique.
Commençons par quelque chose de simple. On veut avoir la liste des billets, accessible (et donc classée) par le permalink (qui est notre champ spécial _id).
On va définir une vue avec la fonction map suivante :
map = function(doc) {
if (doc.Type=="billet") {
emit(doc._id, null);
}
}
Elle prend en paramètre un document. On attend d'elle qu'elle émette des couples (clé, valeur) via la fonction emit(). Lors de la construction de la vue, la fonction map définie va être appelée sur chaque document, 1 par 1, et CouchDB va enregistrer ce qui est émis pour ne pas avoir à tout refaire lors d'un second appel.
Remarquez déjà qu'avant toute émission, on teste si le document est bien un billet via doc.Type qui doit valoir “billet”.
On fait donc pour chaque billet emit(doc._id, null) : on émet le permalien (le champ _id ici) et la valeur null, car pour ce cas, on a que faire d'une valeur émise.
Ce qui est émis sera donc, au final ce qui suit. C'est aussi ce qu'on obtiendra en appelant la vue en question.
[
{ "key": "hello-world", "value": null },
{ "key": "j-aime-le-chocolat", "value": null },
{ "key": "mousse-au-chocolat-la-recette", "value": null }
]
Il faut savoir que ces résultats sont triés par clés croissantes. Chaque ligne retournée inclue aussi un champ id avec l'id du document, que j'ai volontairement ignoré ici.
Vous faites pareil, sauf que vous changez le champ émis.
Par exemple pour la liste des billets par sujet :
map = function(doc) {
if (doc.Type=="billet") {
emit(doc.Subject, null);
}
}
Cela retournera lors de l'appel de la vue :
[
{ "key": "Hello, World !", "value": null, "id": "hello-world" },
{ "key": "J'aime le chocolat", "value": null, "id": "j-aime-le-chocolat" },
{ "key": "Mousse au chocolat, la recette", "value": null, "id": "mousse-au-chocolat-la-recette" }
]
Cette fois, je n'ai pas omis le champs id. Il s'ajoute sur chaque ligne pour permettre de remonter au document.
Petite note : si vous appelez la vue en ajoutant le paramètre include_docs=true, vous aurez un champs de plus par ligne, nommé doc, qui contiendra l'intégralité du document en question (pour éviter de les appeler 1 par 1 pour les afficher par exemple).
On veut récupérer la liste des tags utilisés dans les billets, avec le nombre de billet par tag.
On va définir une vue avec les fonctions map et reduce suivantes :
map = function(doc) {
if (doc.Type=="billet") {
for(i in doc.Tags) {
emit(doc.Tags[i], 1);
}
}
}
reduce = function(key, values, rereduce) {
return sum(values);
}
Simulons un appel de la vue sans l'application de la fonction reduce avec le paramètre reduce=false, ce qui nous retourne :
[
{ "key": "blog", "value": 1, "id": "hello-world" },
{ "key": "chocolat", "value": 1, "id": "j-aime-le-chocolat" },
{ "key": "chocolat", "value": 1, "id": "mousse-au-chocolat-la-recette" },
{ "key": "cuisine", "value": 1, "id": "hello-world" },
{ "key": "cuisine", "value": 1, "id": "j-aime-le-chocolat" },
{ "key": "cuisine", "value": 1, "id": "mousse-au-chocolat-la-recette" },
{ "key": "dessert", "value": 1, "id": "mousse-au-chocolat-la-recette" },
{ "key": "gâteau", "value": 1, "id": "j-aime-le-chocolat" },
{ "key": "recette", "value": 1, "id": "mousse-au-chocolat-la-recette" }
]
Les résultats sont bien triés par clé. On a bien tout ce qui a été émis (toujours avec un champ id qui contient l'id du document à l'origine de l'émission).
CouchDB donne alors cette liste à manger à la fonction reduce, pour chaque clé différente, il donne à la fonction reduce en paramètres la clé, et un tableau des valeurs prises par cette clé. Par exemple pour la clé “chocolat”, il y a deux fois la valeur 1, la fonction reduce est donc appelée avec comme valeurs le tableau [1, 1].
Voyons ce que l'application de reduce nous donne : on appelle la vue avec le paramètre group=true :
[
{ "key": "blog", "value": 1 },
{ "key": "chocolat", "value": 2 },
{ "key": "cuisine", "value": 3 },
{ "key": "dessert", "value": 1 },
{ "key": "gâteau", "value": 1 },
{ "key": "recette", "value": 1 }
]
Regardez les valeurs. C'est beau.
à finir
On va essayer d'aller plus loin, modifions tout ça pour une prise en charge des commentaires.
Si vous avez l'habitude des bases de données relationnelles, vous pensez immédiatement à une nouvelle table commentaires liées aux billets, puis aux jointures pour récupérer le tout.
On pourrait faire ainsi, un nouveau type de document. Mais on va faire autrement, pour vous montrer la force du document unique (comme d'habitude, cette méthode n'est pas forcement la panacée).
On va inclure les commentaires dans le billet lui même, étant donné qu'un commentaire ne concerne qu'un seul billet.
Voici un exemple :
{
"_id": "hello-world",
"Type": "billet",
"Subject": "Hello, World !",
"Author": "Arthur",
"PostedDate": "2009-10-26 13:54:32",
"Tags": ["blog", "cuisine"],
"Body": "Bienvenue sur mon nouveau blog consacré à la cuisine.",
"Comments": [
{ "Author": "Marie", "PostedDate": "2009-10-26 14:15:22", "Body": "Enfin un blog de cuisine !" },
{ "Author": "Arthur", "PostedDate": "2009-10-26 14:17:14", "Body": "Merci Marie." }
]
}
On a ajouté un champ Comments qui est un tableau d'Object (au sens tableau associatif) JSON qui contient trois champs : Author, PostedDate et Body.
Quand on ajoute un commentaire, on le met à la suite des autres dans le document du billet lui même. L'avantage, quand on récupère le document, on a directement les commentaires.
Voyons ce qu'on peut faire au niveau des vues avec ces commentaires.
On veut compter les commentaires par billet.
map = function(doc) {
if (doc.Type=="billet") {
for(i in doc.Comments) {
emit( [ doc._id , i ] , 1 );
}
}
}
Dans cette fonction map, on va émettre pour chaque commentaire du billet une clé contenant un tableau. Dans ce tableau clé, on y place en premier l'id du billet, et l'indice du commentaire dans son tableau.
Sans fonction reduce, ça nous donnerait le résultat suivant :
[
{ "key": [ "hello-world", 0 ], "value": 1, "id": "hello-world" },
{ "key": [ "hello-world", 1 ], "value": 1, "id": "hello-world" },
{ "key": [ "j-aime-le-chocolat", 0 ], "value": 1 "id": "j-aime-le-chocolat" }
]
Ajoutons la fonction reduce qui va bien :
reduce = function(key, values, rereduce) {
return sum(values);
}
Si on appelle la vue sans paramètres, on obtient :
[
{ "key": null, "value": 3 }
]
On a ainsi le nombre total de commentaires sur le blog.
Si on appelle la vue avec le paramètre group=true, on demande de grouper par clé :
[
{ "key": [ "hello-world", 0 ], "value": 1 },
{ "key": [ "hello-world", 1 ], "value": 1 },
{ "key": [ "j-aime-le-chocolat", 0 ], "value": 1 }
]
On obtient la même chose que sans fonction reduce, car les clés données sont toutes différentes.
Maintenant, on appelle la vue avec les paramètres group=true et group_level=1 :
[
{ "key": [ "hello-world" ], "value": 2 },
{ "key": [ "j-aime-le-chocolat" ], "value": 1 }
]
Le niveau de groupement demandé étant de 1, CouchDB ne va pas aller plus loin que le premier élément du tableau clé pour effectuer ses regroupements et donner ça à manger à la fonction reduce.
On obtient ainsi le nombre de commentaires par billet.
Vous allez me dire, c'est idiot, si on avait émis directement l'id du billet en clé et le nombre de commentaires en valeur (doc.Comments.length) dans la fonction map, on aurait directement eu ça. Oui, mais c'est pour l'exemple, et puis vous pourriez avoir besoin d'émettre des clés plus complexes pour faire des regroupements.
Si on avait émis, par exemple [ doc._id, doc.Comments[i].Author, i ] :
On veut les commentaires par billet.
map = function(doc) {
if (doc.Type=="billet") {
if (doc.Comments.length > 0) {
last_i = doc.Comments.length - 1;
emit( doc.Comments[last_i].PostedDate , null );
}
}
}
Dans cette fonction map, on va émettre pour chaque billet possédant au moins un commentaire une clé contenant la date du commentaire le plus récent (on a fais l'hypothèse qu'ils sont toujours triés par date) et la valeur null.
Ce qui donne :
[
{ "key": "2009-10-26 14:17:14", "value": null, "id": "hello-world" },
{ "key": "2009-10-28 18:23:51", "value": null, "id": "j-aime-le-chocolat" }
]
On peut appeler la vue avec le paramètre descending=true pour trier les clés dans l'ordre décroissant et le paramètre limit=10 pour limiter le nombre retourné à 10. On aura les 10 derniers billets différents commentés.