Realm, la solution pour la persistance des données de vos applications mobiles iOS/Android ?

Une base de données destinée aux mobiles, tablettes et wearables

Realm est la première base de données destinée uniquement aux plates-formes mobiles qui ne repose sur aucune autre, contrairement aux nombreuses librairies qui proposent des solutions mobiles basées sur SQLite. On peut citer par exemple le framework mis à disposition par Apple pour les applications iOS : Core Data, ou bien ORMLite qui propose de faire le pont entre des objets Java et une base de données relationnelle.

Realm est un projet jeune, en développement depuis 2011, qui s’est focalisé avant tout vers iOS et qui depuis le 29 septembre 2014 propose ses fonctionnalités au système Android.

Pourquoi Realm ?

timelineCe schéma montre que les innovations apportées aux bases de données mobiles depuis 2000 sont clairement inexistantes. Cependant, depuis 2007, pléthore de bases de données orientées serveurs ont fait leur apparition, notamment grâce au phénomène de Big Data apparu avec la croissance phénoménale des données à stocker, traiter et analyser.

Le problème majeur ici est qu’aucune d’entre elle ne peut être intégrée dans un projet mobile/tablette ou wearable du fait qu’elles n’ont pas été conçues pour ça.

À l’heure actuelle, pour les développeurs mobile, la solution la plus performante pour la persistance des données est d’avoir recours à SQLite. Ainsi l’introduction en 2000 de SQLite a été une étape révolutionnaire dans le domaine du développement mobile, mais 14 années plus tard cette activité n’a plus rien à voir. En effet, il suffit de se représenter l’image d’un téléphone ou même d’une “application” au début des années 2000 pour comprendre que la conception et le développement d’applications mobiles ont bien changé.

C’est à ce moment que Realm intervient en mettant à disposition des développeurs un système de persistance de données multi plates-formes (iOS/Android) performant et simple à mettre en oeuvre.

Performant : comment ?

Comme énoncé précédemment, le coeur de la technologie développée pour Realm ne s’appuie pas sur une base de données existante et n’est pour l’instant pas open source. Ce que l’on sait à propos du coeur de Realm c’est qu’il repose sur un noyau codé en C++ et créé dans le but de s’exécuter sur des plates-formes mobiles. En prenant donc en compte les contraintes imposées par ce type de terminaux, notamment l’espace mémoire et la relative lenteur des processeurs embarqués.

Pour gagner en performance, Realm s’appuie sur les concepts de :

De plus, les fichiers .realm dans lesquels sont stockées les données sont compatibles quels que soient la plate-forme et le langage utilisés (iOS: Objective-C et Swift ou Android: Java).

Pour illustrer ces promesses de performances Realm met à disposition le code utilisé pour effectuer leurs études comparatives ici.

Benchmark

Dans un premier temps intéressons-nous à l’objectif principal d’une base de données, la persistance, et plus particulièrement les insertions.
Pour ce benchmark, la base de données créée contient une table Employee, avec pour attributs un nom, un âge et un booléen hired = true si l’employé est embauché.

La figure ci-dessous provient de l’application standalone Realm browser disponible avec Realm qui permet de consulter et éditer le contenu de la base de données sur laquelle on travaille, un outil très pratique pour le développeur.

Capture d’écran 2014-10-17 à 15.03.47

Capture d’écran 2014-10-16 à 18.12.31

La requête d’insertion a inséré 200 000 fois un employé avec un nom allant de “Foo0” à “Foo999” avec une tranche d’âge de 20 à 69 et une valeur de hired true pour les lignes impaires et false pour les lignes paires.

Ce diagramme indique que Realm, malgré sa jeunesse, promet déjà plus de performance en insertion massive en une seule transaction que les frameworks actuels utilisés par des millions d’applications. On pense notamment à celui mis en avant par Apple: Core Data qui permet d’insérer 5 fois moins d’enregistrements par seconde en une seule transaction.

Cependant il n’est pas plus performant sur ce terrain que la version “compiled statement” de SQLite, qui double le nombre d’insertions effectuées par seconde en une transaction.

Dans un second temps voyons comment ces bases de données réagissent face aux requêtes qu’on leur soumet.

Capture d’écran 2014-10-16 à 18.13.19

La requête sur la table Employee est la suivante :

Sélectionner toutes les entrées de la table Employee dont la valeur de hired est fausse et dont l’âge est compris entre 20 et 50 et dont le nom est “Foo0”

En SQL cela donne :

SELECT * FROM Employee WHERE hired = 0 AND age >= 20 AND age <= 50 AND name = 'Foo0’;

Le résultat est tel que Realm double au minimum le nombre de requêtes effectuées par seconde pour le même jeu de données que les frameworks existants.

Dans la même lignée voici les résultats quasiment identiques de la capacité de ces frameworks à compter le nombre de résultats retournés par la même requête que précédemment.

Capture d’écran 2014-10-16 à 18.12.57

Après ces tests de performance, intéressons-nous au poids de chacune de ces solutions qui sont, rappelons le, intégrées dans nos devices.

Pour cela le même jeu de données a été conservé avec cette fois-ci 1 millions d’entrées dans chacune des bases, les résultats sont en Mo.

Capture d’écran 2014-10-17 à 15.58.42

Globalement on peut dire que ces bases de données sont et respectent leur engagement en tant que base “lite”, excepté pour CouchBaseLite qui affiche pour ce même test une empreinte mémoire de 348,7 Mo. Il a été retiré du diagramme dans un souci de lisibilité.
Realm est le plus léger sur nos devices/wearable avec 12,6Mo contre près de 17Mo pour SQLite et FMDB.

Aux vues de ces résultats, Realm s’en sort plutôt bien côté performance, mais qu’en est-t-il de sa prétendue simplicité d’utilisation ?

Facile à mettre en oeuvre ? comment ?

Realm propose d’exposer vos données en tant qu’objets et d’y accéder directement dans le code. Pour cela il suffit de faire hériter vos POJO (plain old java object) de la classe RealmObject pour définir un modèle de données, à la manière des JavaBeans.

Realm propose en tout et pour tout 3 classes majeures :

  • Realm : Le coeur du framework, permet un accès à la base de données où sont stockés vos objets.
  • RealmObject : Votre modèle Realm, le fait de créer un modèle va définir votre schéma de base de données.
  • RealmList<? extends RealmObject> : Permet de stocker des objets de type RLMObject, de définir des relations au sein de votre modèle et se comporte comme un tableau quelconque quelle que soit la plate-forme sur laquelle vous développez. Le résultat d’une requête sur une base .realm sera de ce type.

et 1 classe utilitaire :

  • RealmMigration: Permet de conserver une base de données cohérente en cas de changement du modèle initial de données par le biais d’une migration.

Ainsi on peut définir le modèle Employee héritant de la classe RealmObject de cette manière :

public class Employee extends RealmObject {
    private String          name;
    private int             age;
    private boolean         hired;
    // + Standard setters and getters here

}

Ajouter une entrée de cette manière :

Realm realm = Realm.getInstance(this.getContext());
// Transactions give you easy thread-safety
realm.beginTransaction();
Employee john = realm.createObject(Employee.class);
john.setName("John");
john.setAge(35);
john.setHired(true);
realm.commitTransaction();

On récupère une instance de la classe singleton Realm.
On créer un bloc realm.beginTransaction(); et realm.commitTransaction();
Puis entre ces deux instructions on va créer notre objet Employee et on lui affecte des valeurs.

A l’exécution de realm.beginTransaction(); notre nouvel employé John sera enregistré en base et accessible à tout moment par la suite.
Les transactions Realm sont thread-safe et conservent les propriétés ACID.

Pour accéder aux données en base, rien de plus simple :

RealmResults<Employee> query = realm.where(Employee.class)
                               .greaterThan("age", 20)
                               .findAll();

Les requêtes sont chaînables pour par exemple affiner le résultat de la requête ci-dessus:

RealmResults<Employee> allJohn = query.where()
                                .contains("name", "john")
                                .findAll();

Les conditions supportées par Realm sont :
greaterThan(), lessThan(), greaterThanOrEqualTo(), lessThanOrEqualTo()
equalTo(), notEqualTo()
contains(), beginsWith(), endsWith()

Le chaînage des requêtes est par défaut configuré sur l’opérateur AND, mais Realm supporte aussi l’opérateur OR, pour cela il faut explicitement faire figurer .or().

En plus de la classe RealmObject, on dispose de la classe RealmList<? extends RealmObject> permettant par exemple de définir une relation many-to-many ou many-to-one:

public class Company extends RealmObject {
    private RealmList<Employee> employees;
    // Other fields…

}

Ceci étant une présentation rapide de la mise en oeuvre d’un modèle simple, toutes les fonctionnalités sont accessibles ici.

Realm propose une mise en place simple pour les développeurs en exposant les données persistantes en tant qu’objets et propose une API relativement complète et peu verbeuse pour manipuler les données sauvegardées.

Pour illustrer la simplicité de l’API de manipulation des données, on peut par exemple montrer le contenu des deux méthodes d’insertions de données du Benchmark, en Objective-C.
Rappelons quand même que SQLite propose d’insérer deux fois plus de données par seconde que Realm, mais à quel prix pour le développeur ?
Voici donc la méthode permettant de faire une insertion en base SQLite compiled statement:

- (void)insertObject:(NSUInteger)index {

    sqlite3_stmt *ppStmt = NULL;
    int rc = sqlite3_prepare(self.db,
                         "INSERT INTO t1 VALUES(?1, ?2, ?3);",
                         -1,
                         &ppStmt,
                         NULL);
    if (rc != SQLITE_OK) {
        @throw [NSException exceptionWithName:@"SQL failure"
                                       reason:@"Prepare of insert statement failed"
                                     userInfo:nil];
    }

    const char *nameValue = [self nameValue:index].UTF8String;
    const int hiredValue = [self hiredValue:index] ? 1 : 0;
    const int ageValue = [self ageValue:index];

    sqlite3_reset(ppStmt);
    sqlite3_bind_text(ppStmt, 1, nameValue, -1, NULL); // name

    sqlite3_bind_int(ppStmt, 2, ageValue);  // age

    sqlite3_bind_int(ppStmt, 3, hiredValue); // hired

    rc = sqlite3_step(ppStmt);
    if (rc != SQLITE_DONE) {
        @throw [NSException exceptionWithName:@"SQL failure"
                                       reason:@"sqlite3_step on insert failed"
                                     userInfo:nil];
    }
    sqlite3_finalize(ppStmt); // Cleanup

}

Puis la méthode avec Realm :

- (void)insertObject:(NSUInteger)index {
    Employee *employee = [[Employee alloc] init];
    employee.name = [self nameValue:index];
    employee.age = [self ageValue:index];
    employee.hired = [self hiredValue:index];
    [self.realm addObject:employee];
}

La même action avec un code beaucoup moins verbeux, Realm est certes moins performant que SQLite quand il s’agit d’insérer des données massivement, mais il propose aux développeurs une interface relativement simple d’utilisation associée à des performances très correctes en comparaison des frameworks existants.

Sécurité des données, qu’en est-il ?

Faisons un tour d’horizon des solutions proposées par Realm pour sécuriser vos données. Pour l’instant en version 0.86.3 pour iOS et 0.71.0 pour Android, Realm est encore en bêta. Il ne dispose pas de solution cross plate-forme ou officielle pour la sécurisation des données sensibles.

iOS

Le chiffrement des données sous iOS peut se faire de différentes manières

  • NSFileProtectionComplete :

L’idée est d’enregistrer le fichier .realm à sa création avec pour attribut NSFileProtectionComplete lors de l’appel à NSFileManager. Ce qui à pour but d’enregistrer le fichier .realm dans un format crypté sur le disque.
Cette solution est facile à mettre en oeuvre mais ne couvre pas tous les cas d’utilisation. En effet, on ne peut ni lire ni écrire sur le fichier tant que l’appareil sur lequel il se trouve est verrouillé ou tant que l’appareil démarre. Ce qui laisse tous les autres cas d’utilisation non couvert par cette protection.
+ : Facile à mettre en place, accès rapide aux données, toutes les fonctionnalités de Realm sont conservées, la base entière est cryptée, la base Realm contient l’intégralité des données.
: iOS uniquement, effectif uniquement sur les appareils protégés par mot de passe.

  • Stocker les données sensibles dans le KeyChain et stocker les clef d’accès au KeyChain dans Realm :

Apple met à disposition un KeyChain, qui est un espace de stockage sécurisé au sein des appareils. L’accès aux items du KeyChain est réservé aux applications, et chaque application ne peut accéder qu’à ses propres items en disposant de la clef définie lors de la création de celui-ci.

+ : Très sécurisé, compatible iOS/OS X, accès aux données sensibles rapide, fonctionne sur les appareils non sécurisés par mot de passe.
: L’accès aux données sensibles n’est pas direct via le système de requête Realm (on récupère la clef liée aux données puis on y accède), la base Realm ne contient plus l’intégralité des données, nécessite de choisir au cas par cas les données sécurisées, non portable sur d’autre plates-formes que celles d’Apple.

  • Utiliser des Librairies de chiffrement de données et sauvegarder le contenu dans Realm :

Dans ce cas, le développeur a pour responsabilité de mettre en place le système de chiffrement de données en utilisant par exemple RNCryptor ou CommonCrypto proposé par Apple.
+ : Très sécurisé, toutes les données sont contenues dans la base Realm, fonctionne sur les appareils non protégés par mot de passe, accès aux données sensibles relativement rapide.
: Difficile à mettre en oeuvre, définir au cas par cas quelles données crypter ou non, les données cryptés ne peuvent être retournées directement par Realm, augmente les chances d’erreurs lors de l’implémentation.

Android

Du côté d’Android, pour utiliser le chiffrement de données il est nécessaire d’ajouter la ligne encryption=true dans le fichier local.properties avant d’utiliser la commande ./gradlew assemble.
Puis de remplacer le fichier realm-.aar de votre projet par celui situé ici :
realm/build/outputs/aar/realm-.aar

  • Sauvegarder le fichier .realm crypté lors de sa création :

On va générer une clef de chiffrement 256-bit lors de la création de la base Realm afin de crypter et décrypter les données persistantes de manière transparente selon le standard AES-256.

byte[] key = new byte[32];
new SecureRandom().nextBytes(key);
Realm realm = Realm.create(this, key);

// ... use the Realm as normal …

La difficulté de ce procédé va être de sauvegarder la clef dans le KeyStore d’Android de manière sécurisée pour qu’aucune autre application n’accède à cette clef.
Un exemple de mise en place de ce procédé est disponible ici.
+ : Très sécurisé, toute les données sont contenues dans la base Realm, accès aux données rapide.
: Difficile à mettre en oeuvre, usage réservé à Android

En termes de sécurité, il reste du travail aux équipes responsables du projet pour intégrer une solution cross plate-forme de chiffrement des données propre à Realm. À la manière de la solution proposée pour Android, bien qu’expérimentale, qui permettrait une compatibilité entre les deux OS cibles.

Conclusion

Realm est une base de données créée dans le but de s’exécuter sur des plates-formes mobiles. Ses performances combinées avec sa facilité d’utilisation en font un outil très fonctionnel pour les développeurs mobiles que ce soit sur un projet Android ou iOS.
On peut se demander s’il est judicieux d’utiliser Realm pour une application en production connaissant la jeunesse du Framework ?
Tout dépend de la sensibilité des données du projet à stocker. On privilégiera une technologie qui a fait ses preuves pour réaliser une application dont les données sont extrêmement sensibles tant qu’une release du projet ne sera pas disponible.

De plus, Realm n’est disponible au public que depuis juillet 2014 seulement, en développement depuis 2011 et en production depuis 2012. Notamment chez Zinga pour une application qui atteint le seuil du million d’utilisateurs quotidien.

Cependant, Realm reste un projet jeune avec une petite communauté qui tend à grandir, donc en cas de blocage, la bonne pratique est d’aller voir les issues github :

Realm n’est pas une alternative absolue à SQLite et l’ensemble des frameworks dont il est à l’origine, mais une nouvelle solution qui a légitimement sa place dans l’écosystème des bases de données mobiles.