Fork me on GitHub

Démarrage avec les bases NoSQL

Introduction

Ça faisait un moment que je voulais me lancer dans un projet utilisant une DB NoSQL.

Pour les projets web actuel nous n'en ressentions pas encore besoin.

Au cours du dernier TP que j'ai animé à l'UTBM en LO52, j'ai compris qu'il était temps de faire le pas. (Voir la vidéo).

Couchdbkit

La première chose que j'ai essayé de faire c'est de lancer le getting started de la documentation de couchdbkit

Vous pouvez regarder le gist

ça permet de comprendre l'intégralité du fonctionnement, saisie, et lecture de la base. On écrit les views en javascript.

L'arborescence est importante, selon ce que vous mettez, vous n'accèderez pas forcément à votre view.

On peut voir une interface de navigation et d'administration de couchdb ici : http://localhost:5984/_utils/

DjangoApp

Ensuite, j'ai décidé de tester l'intégration à Django et là je n'ai pas eu de problème.

Vous pouvez lancer l'example : https://github.com/benoitc/couchdbkit/tree/master/examples/djangoapp

$ virtualenv apps
$ source apps/bin/activate
$ pip install -r requirements.txt
(apps)$ python manage.py syncdb
(apps)$ python manage.py runserver

Ensuite vous testez ici : http://localhost:8000/ et c'est tout.

Démarrer un projet Django CouchDB

Pour démarrer "from scratch", il faut configurer votre projet :

COUCHDB_DATABASES = (
    ('djangoapp.mesh’, 'http://127.0.0.1:5984/mesh'),
)

# ...

INSTALLED_APPS = (
     ....
     'couchdbkit.ext.django’,
     'captor_mesh.mesh’,
     ....
 )

Dans le fichier models.py on va ensuite créer notre modèle de document :

from couchdbkit.ext.django.schema import *

class Captor(Document):
    arduino_id = IntegerProperty()
    pin_id = IntegerProperty()
    value = IntegerProperty()
    date_time = DateTimeProperty(auto_now_add=True)

    def __unicode__(self):
        return u'[%s] Arduino : %s - PIN : %s - Value : %d' % (
            self.date_time.strftime('%H:%M:%S'),
            self.arduino_id,
            self.pin_id,
            self.value)

Pour créer automatiquement un formulaire à partir du modèle :

from couchdbkit.ext.django.schema import *

class CaptorForm(DocumentForm):
     class Meta:
         document = Captor

Nous avons des vues simples qui affiche simplement les templates :

def gauge(request, arduino_id):
    return render_to_response('mesh/gauge.html', {'arduino': arduino_id})

def line(request, arduino_id):
    return render_to_response('mesh/line.html',
                              {'arduino': arduino_id})

Nous avons d'autres vues plus compliquées où nous souhaitons afficher des informations de la base :

def index(request):
        if request.method == "POST":
        return HttpResponseRedirect(
              reverse(request.POST['view'],
                      args=[request.POST['arduino']]))

    arduinos = list(Captor.view('mesh/arduino'))

    return render_to_response('mesh/index.html', {'arduinos': arduinos},
                              RequestContext(request))
<html>
  <head>
    <title>Mesh network</title>
  </head>

  <body>
    <h1>Mesh network</h1>

    <form action="" method="post">
      <select name="arduino">
                {% csrf_token %}
            {% for arduino in arduinos %}
            <option value="{{ arduino }}">Arduino #{{ arduino }}</option>
            {% endfor %}
      </select>
      <select name="view">
            <option value="gauge">Gauge</option>
            <option value="line">Line</option>
      </select>
      <p><input type="submit" /></p>
    </form>
  </body>
</html>

Nous allons donc réaliser un fichier _design/views/arduino/map.js :

function(doc) {
     if (doc.doc_type == "Captor")
          emit(doc.arduino_id);
}

Et je ne suis pas allé plus loin avec CouchDB car la magic de l'accès à mes views décrites dans un fichier js séparé ne me convenait pas.

PyMongo

Ce qui est plaisant avec MongoDB, c'est de suivre le tutoriel dans le Try it out (un shell javascript dans une page web).

Intégration avec Django

Il y a un module nommé django-mongodb-engine qui permet d'ajouter le backend mongodb à l'ORM de Django.

Du coup, il n'y a plus de foreignkey mais des ListField() et il y a donc quelques modification à l'utilisation.

from django.db import models
from djangotoolbox.fields import ListField

class Post(models.Model):
    title = models.CharField()
    text = models.TextField()
    tags = ListField()
    comments = ListField()

Pour mon réseau de capteur, il n'y a pas véritablement de changement.

Comme c'était un peu trop simple de simplement installer django-mongodb-engine, j'ai décidé de recoder la chose en utilisant Flask et PyMongo.

Un simple pip install flask pymongo est on est parti !

N'hésitez pas à aller voir le code intégral sur le GitHub

En conclusion, ce qui est compliqué dans le NoSQL ce sont les requêtes. Bien sur, nous avons les fameux map_reduce mais ils ne sont pas aussi fast-forward que nos requêtes SQL.

Voici les deux requêtes sur lesquelles je me suis un peu casser les dents :

Récupérer la liste des arduinos

La première, je me suis lancé dans un map_reduce au lieu de lire la doc en profondeur :

On aurait voulu écrire :

SELECT DISTINCT arduino
FROM mesh_captors

C'est a peu prêt la même chose :

from pymongo import Connection
    db = Connection().mesh
arduinos = db.mesh_captors.distinct('arduino')

On se connecte à la database nommée mesh et on fait la requête sur la collection mesh_captors pour récupérer les valeurs distinctes de arduino.

Récupérer la dernière valeur de chaque pin d'un arduino

Là on aurait souhaité écrire cela :

SELECT DISTINCT pin, value
FROM mesh_captors
WHERE arduino = 203
GROUP_BY pin
ORDER BY date DESC

Il y a donc la selection du arduino, la selection des pins avec un group_by, puis la selection par date.

from bson.code import Code

reducer = Code("""
              function (doc, out) {
                  if(out.date == 0 || out.date < doc.date) {
                       out.date = doc.date;
                       out.value = doc.value;
                  }
              }
              """)

# We group lines by pin of the arduino
captors_values = db.mesh_captors.group(
    key=['pin'],
    condition={'arduino': int(arduino_id)},
    reduce=reducer, initial={'date': 0})

Une fois écrit ça parait logique, mais ça m'a quand même prit un certain temps.

La gestion des index

Avec mon premier script j'ai réussi à faire 200000 entrées dans la base en moins de 10 minutes et mongodb ne pouvait plus faire les requêtes. J'ai donc dû rajouter quelques index.

from pymongo import ASCENDING, DESCENDING
db.mesh_captors.ensure_index([('arduino', ASCENDING), ('pin', ASCENDING), ('date', DESCENDING)])

Conclusion

Le NoSQL est vraiment pratique pour ce genre d'application, pas de database lock et une rapidité appréciable.

Comments !

blogroll

social