Lors d’un des nos derniers projets, afin de synchroniser un catalogue de produits avec une volumétrie élevée (56000 unités) et d’assurer un fonctionnement entièrement offline de l’application, nous nous sommes intéressés à Couchbase Mobile. Cette solution propose, depuis 2014, de répliquer une base de données distante sur un device de façon transparente. Avec l’avènement de Realm Mobile Platform et le besoin de revoir notre processus de synchronisation, notre regard s’est porté sur Couchbase Mobile. Cet article propose de synthétiser l’étude que nous avons faite de Couchbase Mobile : comment l’utiliser, dans quel cadre et quels sont ses points forts (et ses faiblesses).
Tour d’horizon
Couchbase Mobile est une base de données mobile orientée document. La couche de persistance mobile, appelée Couchbase Lite, a longuement reposé sur SQLite, qui reste le choix par défaut. Couchbase a cependant récemment développé son propre moteur de stockage, ForestDB, plus rapide et moins coûteux en RAM. Dans les deux cas, les documents (les entités que l’on manipule avec Couchbase lite) sont stockés sous forme de JSON.
Couchbase lite est disponible sur plusieurs plateformes: Android et iOS mais aussi .Net ou bien encore PhoneGap.
Installation
Il existe plusieurs façons d’installer CouchBase Lite :
- CocoaPods
pod 'couchbase-lite-ios' pod 'couchbase-lite-ios/SQLCipher'
la deuxième dépendance est optionnelle, elle n’a d’utilité que si vous souhaitez chiffrer votre base de données
- Carthage
github "couchbase/couchbase-lite-ios"
Si vous voulez utiliser la bibliothèque statique ForestDB, il faudra télécharger le zip la contenant grâce au téléchargement direct, celle-ci n’étant pas disponible via CocoaPods ou Carthage.
Créer et configurer la base de donnée
Pour créer une référence sur une base de données, il faudra utiliser un objet de type CBLDatabase
et lui préciser des options lors de son instantiation :
let dbName = "db" let options = CBLDatabaseOptions() options.create = true options.storageType = kCBLForestDBStorage let database = try! CBLManager.sharedInstance().openDatabaseNamed(dbName, with: options)
L’option create
n’efface pas la base de données existante si jamais elle est re-précisée. Si par contre la base de données n’existe pas et que cette option n’est pas déclarée, alors l’appel à openDatabaseNamed
va échouer. Il est par ailleurs possible de préciser un moteur de stockage en utilisant la constante kCBLForestDBStorage
. Si celle-ci n’est pas précisée, alors c’est SQLite qui sera utilisé.
Vous trouverez plus d’informations sur le repository GitHub de forestDB
Faire un CRUD
Avant de s’attaquer à la réplication, voyons comment manipuler localement les données :
La création d’un document se fait grâce à l’objet CBLDatabase
. On peut préciser manuellement l’identifiant de document ou décider d’en avoir un auto-généré.
Les données du document sont passées sous la forme d’un dictionnaire. La clef sera obligatoirement une string, la valeur un objet serialisable.
let properties: [String : Any] = [ "name": "julien", "age": 27 ] let document = database.document(withID: "myAwesomeID") try! document?.putProperties(properties)
Pour mettre à jour un document, il faut le rechercher avec l’identifiant souhaité puis appeler la méthode update qui prend en argument une closure:
if let documentToUpdate = database.existingDocument(withID: "myAwesomeID") { try! documentToUpdate.update() { newDoc in newDoc["name"] = "toto" return true } }
Et pour supprimer un document, la manipulation est très similaire:
if let documentToDelete = database.existingDocument(withID: "myAwesomeID") { try! documentToDelete.delete() }
Pour requêter les données, c’est une autre histoire. Il faut passer par une vue : cette dernière permet de définir un block map
et un block reduce
. Le premier permet de choisir les données du document (celles contenues dans le JSON) tandis que le deuxième agrège ces données (pour en retourner le nombre dans cet exemple). Le block reduce
est optionnel.
let view = database.viewNamed("getMyName") view.setMapBlock({ (doc, emit) in if let name = doc["name"] { emit(doc["_id"] ?? "defaultId", name) } }, reduceBlock: { (keys, values, rereduce) -> AnyObject! in return values.count }, version: "1.0")
Il est alors possible de créer une query et d’itérer sur son résultat:
let query = view.createQuery() let result = try! query.run() while let row = result.nextRow() { print("My name is \(row.value)") }
Il est aussi possible de créer une « live query » qui permet d’observer et de réagir directement aux changements en base de données:
let query = view.createQuery().asLiveQuery() query.addObserver(self, forKeyPath: "rows", options: nil, context: nil) ... override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) { // Do whatever you want }
Répliquer les données
Configurer un environnement de dev
Avant de commencer à explorer cette technologie, il est nécessaire de comprendre les composants entrant en jeux côté back-end.
Nous allons donc, comme le montre le schéma ci-dessous mettre en place une Sync Gateway et un serveur Couchbase.
http://www.couchbase.com/nosql-databases/couchbase-mobile
Nous avons ici choisi d’utiliser Docker, ce qui évite toute configuration d’environnement sur ma machine. Parfait pour un cas d’exemple !
Tout d’abord, il faut récupérer les deux conteneurs, la sync gateway et le serveur:
docker pull couchbase/server docker pull couchbase/sync-gateway
Puis, pour que les deux machines puissent communiquer entre elles, créer un réseau:
docker network create --driver bridget couchbase
Lançons maintenant le serveur:
docker run --net=couchbase -d -p 8091-8094:8091-8094 -p 11210:11210 couchbase
Vous pouvez tester ce serveur sur votre navigateur à l’adresse suivante : http://localhost:8091. La configuration est simple, il suffit de se laisser guider par le setup. Pour la suite de cet article, je vous propose d’intégrer le bucket (qui contient vos données) « beer-sample » qui vous sera proposé à la fin de l’installation.
Il faut maintenant relever l’ip du conteneur contenant le serveur, pour cela, lancez la commande
docker network inspect couchbase
et reprenez simplement l’adresse contenue dans la clef IPv4Address
du JSON qui se présente à vous.
Ensuite, il faut créer un fichier de configuration pour la Sync Gateway : celui-ci permet de préciser le nom de la base de données à répliquer, l’adresse du serveur Couchbase (celle que vous venez de récupérer) et le bucket contenant les documents à récupérer (le bucket « beer-sample » ne nécessite pas d’authentification et possède un bon nombre de documents, il est donc intéressant pour tester la réplication rapidement)
Voici l’exemple d’un fichier (my-sg-config.json), sans sécurité, de configuration :
{ "log": ["*"], "databases": { "db": { "server": "http://ip-couchbase-server-conteneur:8091", "bucket": "beer-sample", "users": { "GUEST": { "disabled": false, "admin_channels": ["*"] } } } } }
Il reste maintenant à lancer la Sync Gateway:
docker run --net=couchbase -p 4984:4984 -d -v /tmp:/tmp couchbase-sync-gateway /tmp/my-sg-config.json
Pour tester que tout s’est bien déroulé, lancez http://localhost:4984 sur votre navigateur, un message au format JSON devrait être affiché.
Et côté mobile?
Pour répliquer les données, c’est très simple:
let puller = database.createPullReplication(URL(string: "http://my-sync-gateway-url:4984/db/")!) let pusher = database.createPushReplication(URL(string: "http://my-sync-gateway-url:4984/db/")!) puller.continuous = true pusher.continuous = false pusher.start() puller.start()
Les deux objets, puller et pusher, sont de type CBLReplication
. C’est grâce à eux que l’on va pouvoir configurer la réplication et en contrôler l’avancement. Il est possible de synchroniser les bases de données distante et locale de manière continue (continuous=true/false) et de répercuter directement, via une socket, les changements de chaque côté. Aussi, il est possible d’utiliser les méthodes start et stop pour lancer / arrêter la synchronisation manuellement.
Suivre l’avancement
Il est possible de monitorer l’état de la réplication. A chaque document reçu ou envoyé, une notification est lancée. Il reste donc à créer un observer et de gérer les différents status:
NotificationCenter.default.addObserver(self, selector: #selector(replicationProgress(notification:)), name: Notification.Name.cblReplicationChange, object: puller) NotificationCenter.default.addObserver(self, selector: #selector(replicationProgress(notification:)), name: Notification.Name.cblReplicationChange, object: pusher) ... func replicationProgress(notification: Notification) { let replication = notification.object as! CBLReplication let status = replication.status switch status { case .idle: print("Local database up to date") case .active: print("There are \(replication.changesCount) to process, already processed \(replication.completedChangesCount)") case .offline: print("No network connection") case .stopped: print("Replication stopped") } }
Les attachments
Les attachments sont des données binaires attachés à un document, plus précisément à une révision de document. Dans notre exemple, pour attacher une image à un document, le processus est plutôt simple : il faut en créer une nouvelle révision, lui donner les datas de l’image et sauvegarder cette nouvelle révision.
let document = database.existingDocument(withID: "myAwesomeID") let newRevision = document?.newRevision() let imageData = UIImageJPEGRepresentation(UIImage(named: "cute_cat")!, 0.75) newRevision?.setAttachmentNamed("cute_cat.jpg", withContentType: "image/jpeg", content: imageData) try! newRevision?.save()
Il est aussi possible de répliquer les attachments via la classe CBLReplication
.
Dans nos cas d’usage, cependant, Couchbase Lite s’arrête de façon inattendue lorsque trop d’images sont à gérer (au delà de 2000 plus précisément). Aussi, il est à noter que, dans ce cas, la consommation en RAM est également très élevée. Il est donc nécessaire de passer par une autre solution: télécharger les attachments à la demande ou alors en tâche de fond. Voici comment faire :
let document = database.existingDocument(withID: "myAwesomeID") if let attachment = document?.currentRevision?.attachmentNamed("cute_cat.jpg") { let progress = puller?.downloadAttachment(attachment) // Follow progress progress?.addObserver(self, forKeyPath: "fractionCompleted", options: .new, context: nil) } ... override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if keyPath == "fractionCompleted" { let progress : Progress = object as! Progress if progress.fractionCompleted == 1.0 { print("Attachment download completed") } } }
La méthode downloadAttachment retourne un NSProgress dont on peut observer la variable « fractionCompleted » grâce au KVO.
Sécuriser les données …
… localement
Niveau sécurité, Couchbase ne plaisante pas : il est possible de chiffrer la base de données en AES-256. Pour se faire, rien de plus simple:
var options = CBLDatabaseOptions() options.encryptionKey = "my-awesome-key-that-should-not-be-hardcoded-here" var database: CBLDatabase = manager.openDatabaseNamed("db", withOptions: options, error: nil)
Il faut bien sûr s’interroger sur le stockage de la clef de chiffrement en dehors du code. Il est donc conseillé d’utiliser a minima le keychain ou alors le Touch ID si possible.
… lors de la réplication
Il est aussi possible d’ajouter une authentification sur la sync-gateway. Pour cela, il faut modifier le fichier de configuration, comme dans l’exemple suivant :
{ "log": ["*"], "databases": { "db": { "server": "http://ip-couchbase-server-conteneur:8091", "bucket": "beer-sample", "users": { "GUEST": {"disabled": true}, "julien": {"password": "another-awesome-password"} } } } }
Par ailleurs, pour sécuriser les échanges lors de la réplication, on peut ajouter un certificat SSL :
{ "log": ["*"], "SSLCert": "cert.pem", "SSLKey": "key.pem", "databases": { "db": { "server": "http://ip-couchbase-server-conteneur:8091", "bucket": "beer-sample", "users": { "GUEST": {"disabled": true}, "julien": {"password": "another-awesome-pass"} } } } }
En utilisant App Transport Security, il sera alors possible de n’utiliser que du AES 128 ou 256 pendant la réplication, les données sont protégées.
Les alternatives à Couchbase Mobile
Le domaine de la synchronisation de données entre back-end et mobile offre, à date, un nombre d’autres solutions, la plus connue étant sûrement Realm Mobile Platform.
Realm Mobile Platform a été envisagé mais ne s’est pas révélé mature sur deux points : la sécurité et la migration. Concernant la sécurité, à l’époque du test (la version entreprise de la solution n’existait pas), il était possible de créer des comptes dynamiquement, simplement en connaissant l’URL du serveur distant. La migration n’était pas gérée (les champs dépréciés/changés sont alors vides …), ce qui nécessite que tous les clients mobiles devaient être à jour continuellement. Nous vous invitons tout de même à suivre les mises à jour de Realm, dont a connu une évolution rapide dans les derniers mois.
Pour conclure, Couchbase s’est révélé être très efficace pour la synchronisation: rapide, tolérante à l’erreur (reprise automatique quand le réseau est coupé) et fiable. Une fois la synchronisation terminée, on a alors une application complètement offline et on peut même, si le réseau le permet, mettre à jour les données de l’application au fil de l’eau et à n’importe quel moment.
Cependant, la base de données étant orientée document, elle n’est pas adaptée à tout type de modélisation. Par ailleurs, le SDK proposé par Couchbase est un peu laborieux à utiliser : on ressent, en effet, que celui-ci a été architecturé de la même manière pour tous les langages (Java, Swift, Objective-C et C#). C’est dommage car on l’aurait apprécié plus « swifty » sur iOS. En attendant la maturité de Realm, et mises à part les considérations sur le pricing élevé de la version entreprise, cela reste une bonne solution.