mardi 20 novembre 2012

Push database -> browser avec Mongo et Play

Aujourd'hui je vous propose une expérience amusante à réaliser avec Play Framework 2.1 et la base de données NoSQL MongoDB.
Vous avez peut être entendu parler de ReactiveMongo, un driver Scala "asynchrone et réactif" pour Mongo. Il existe un plugin Play pour utiliser ce driver plus facilement au sein d'une webapp. Nous allons voir comment utiliser ces technologies pour récupérer un ensemble de messages de manière asynchrone et pour les pousser vers le navigateur en temps réel, dans une application de type "Twitter lite". Le but de notre prototype : en laissant une page de recherche ouverte, on veut voir les nouveaux messages correspondant à cette recherche s'afficher dès leur enregistrement en base de données, sans effectuer de "pull" vers le serveur.

Côté browser nous allons utiliser la technologie SSE (Server-Sent Events).
Si on reprend la définition du W3C, SSE est le fait "qu'une page reçoive automatiquement les mises à jour d'un serveur". C'est donc une technique de push qui permet de s'abonner à des mises à jour et à afficher les nouvelles données sans effectuer de requête de rechargement.

Note : certains auront remarqué que le principe de SSE est proche de celui des WebSockets. Si ces 2 techniques font partie d' HTML5, SSE est plus simple car il ne gère que la transmission de données que dans le sens serveur -> client, alors que WebSocket est bidirectionnel.

Assez parlé, un peu de code (Scala) :
 def search(filter: String) = Action { 
    val query = QueryBuilder().query(
                    BSONDocument("message" -> BSONRegex(filter, "")))

    //query results asynchronous cursor
    val cursor = 
        collection.find[JsValue](query, QueryOpts().tailable.awaitData)

    //create the enumerator
    val dataProducer = cursor.enumerate

    //stream the results
    Ok.stream(dataProducer through EventSource())
        .as("text/event-stream")
}

L'objet "collection" est notre lien avec la base de données. Dans notre contrôleur Play, on crée un curseur qui renverra les nouveaux messages contenant notre filtre de recherche, au fur et à mesure de leur arrivée dans la base grâce à l'option "tailable.awaitData".
On utilise ensuite la méthode "Ok.stream" qui permet d'envoyer des résultats partiels au client dès leur disponibilité. L'objet "EventSource" fait partie de l'API Iteratee de Play (voir cette page pour en savoir plus) et fournit un pont vers la fonction SSE définie côté client :
<script type="text/javascript" charset="utf-8">
    var feed = new EventSource("/news/search?filter=@filter");
    feed.addEventListener('message', function(e) {
    var result  = jQuery.parseJSON(event.data);
    //[...]
</script>

On récupère enfin chaque nouveau message dans un objet javascript "result" afin de l'afficher dynamiquement.

Note : lors de la commande "collection.find[JsValue](...)" les objets obtenus via la requête Mongo ont été automatiquement convertis au format JSON (ou JsValue) avant de les pousser sur la socket SSE.
Cela fonctionne grâce à un paramètre implicite (les fameux "implicits" de Scala) définissant une méthode de transformation. Pour profiter de ces implicits il suffit d'ajouter la ligne "import play.modules.reactivemongo.PlayBsonImplicits._".

Note 2 : "dataProducer through EventSource()" peut aussi s'écrire "dataProducer &> EventSource()", selon les goûts...

Vous pouvez récupérer le code complet de l'application ici.