====== L'exemple d'un blog ======
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.
===== Modèle de document =====
Voici comment on va organiser les documents qui contiendront les billets :
* **_id** : Champs obligatoire. Il doit être unique dans toute la base. On choisit que ce sera le permalink du billet, ici une version URL du sujet.
* **Type** : Tous les documents sont à plats dans la base, il faut pouvoir distinguer les billets des commentaires...
* **Subject** : Le sujet du billet (le titre).
* **Author** : Le nom de l'auteur.
* **PostedDate** : La date de publication du billet, au format AAAA-MM-JJ hh:mm:ss
* **Tags** : Un tableau contenant les tags du billet.
* **Body** : Le corps du billet.
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...).
===== Les vues =====
À ce stade, le terme de [[documentation/api/view|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.
==== Liste des billets ====
=== Liste des billets par permalien ===
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.
=== Liste des billets par autre chose ===
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).
==== Activité sur le blog ====
==== Liste des tags ====
On veut récupérer la liste des tags utilisés dans les billets, avec le nombre de billet par tag.
=== Première approche ===
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.
FIXME à finir
===== Les commentaires =====
On va essayer d'aller plus loin, modifions tout ça pour une prise en charge des commentaires.
==== Le modèle de donnée ====
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.
==== Les vues ====
Voyons ce qu'on peut faire au niveau des vues avec ces commentaires.
=== Les commentaires par billet ===
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.
== C'est pas idiot ? Remarque ==
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 ] :
* Appel de la vue sans paramètres : on a le nombre de commentaires total
* Appel avec **group=true** : rien de bien intéressant
* Appel avec **group=true** et **group_level=1** : nombre de commentaire par billet
* Appel avec **group=true** et **group_level=2** : nombre de commentaire par billet et par auteur
=== Les derniers billets commentés ===
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.