Complex data queries in Android databases

Le développement d’une application complexe est impossible sans l’utilisation de bases de données qui fournissent des fonctionnalités puissantes pour le stockage, le tri et la récupération d’informations. Leur application dans le développement Android présente ses propres spécificités dues aux caractéristiques des appareils mobiles : moins de ressources matérielles, économie de batterie, architecture des applications mobiles. Par conséquent, l’utilisation de bases de données dans le développement Android est un sujet distinct qui nécessite une étude approfondie.

En 2020, SQLite, Realm et ObjectBox restent les systèmes de gestion de bases de données (SGBD) les plus populaires pour les applications Android. Chacun d’eux occupe sa propre niche sur le marché des applications mobiles, offrant aux développeurs diverses approches pour le stockage et l’utilisation de données structurées.

SQLite a été introduit pour la première fois en 2000 et est depuis devenu l’un des SGBD les plus populaires. Il est basé sur une approche relationnelle du stockage des données et utilise le langage de requête SQL pour gérer les informations. Contrairement à d’autres SGBD bien connus, tels que MySQL et PostgreSQL, SQLite ne nécessite pas de serveur de base de données séparé, stocke toutes les informations dans un seul fichier et occupe peu d’espace disque. Ces caractéristiques en font la solution la plus optimale parmi les SGBD relationnels pour une utilisation sur appareils mobiles. Actuellement, SQLite est le SGBD le plus fréquemment utilisé dans le développement d’applications Android. SQLite pur est rarement utilisé de nos jours dans le développement d’applications Android. Pour simplifier le travail, l’ORM Room de Google est largement utilisé, ce qui évite d’écrire du code répétitif. Les tables de données sont créées à l’aide d’annotations dans la classe modèle, qui déterminent la transformation des attributs de classe en noms et propriétés des colonnes de table.

Une alternative à SQLite est Realm. Sa première version stable pour Android est apparue en 2014. Les créateurs ont conçu Realm comme une base de données qui permettrait d’éviter d’écrire du code SQL répétitif, de travailler avec les données comme des objets et d’augmenter la vitesse des opérations CRUD. En 2019, Realm a été acquis par MongoDB Inc. et la plateforme serverless a été ajoutée aux fonctionnalités d’origine. Realm appartient au groupe des SGBD NoSQL, utilisant des classes de modèles pour décrire la structure des données, plutôt que des tables et les relations entre elles. Cela élimine le problème de la cartographie objet-relationnel, pertinent pour les bases de données relationnelles, et réduit les coûts de ressources pour la conversion des données.

ObjectBox est le plus récent des SGBD considérés. Il a été créé par Green Robot, un développeur Android bien connu pour ses produits GreenDao, EventBus et Essentials. ObjectBox a été initialement développé comme un SGBD pour les appareils mobiles et IoT ; par conséquent, il se compare favorablement à ses concurrents en termes de vitesse de fonctionnement et de facilité d’intégration dans les applications mobiles. Comme Realm, il implémente une approche NoSQL dans laquelle les attributs des classes de modèles et les relations entre eux sont directement écrits dans la base de données.

Utiliser une base de données dans votre application Android implique dans la plupart des cas de travailler avec des informations structurées et interconnectées. Le simple stockage de données primitives est simple. Cependant, des manipulations plus complexes avec les informations peuvent représenter un véritable défi pour un développeur, nécessitant une bonne connaissance des outils de son SGBD choisi. Essayons de considérer comment les bases de données mobiles les plus populaires – SQLite, Realm et ObjectBox – résolvent les problèmes complexes :

  1. stockage d’objets complexes
  2. récupération de données avec de nombreuses conditions
  3. obtention d’entités liées (un-à-un, un-à-plusieurs, plusieurs-à-plusieurs).

À titre d’exemple, prenons une application pour une bibliothèque qui travaillera avec des données sur les livres. Chaque livre a un ou plusieurs auteurs, un titre, un lieu et une année de publication, une maison d’édition, un statut actuel (émis, disponible, en restauration), etc. Examinons les options d’organisation des données qui peuvent être utilisées dans le cas de l’utilisation de chacun des SGBD ci-dessus.

Stockage d’objets complexes

SQLite et Room

L’une des caractéristiques des bases de données relationnelles est la prise en charge de types de données strictement définis qui peuvent être attribués aux colonnes de table. Dans le cas de SQLite, il s’agit de INTEGER, REAL, TEXT, BLOB et NULL. Pour enregistrer des objets, vous devez les convertir en un ensemble d’attributs des types correspondants. Room permet de le faire de plusieurs manières :

  1. Ajout de l’annotation @Entity à une classe qui décrit un modèle de données. Dans ce cas, Room créera une table séparée dans la base de données SQLite et enregistrera vos objets sous forme de lignes de cette table. À l’aide d’annotations, vous pouvez spécifier les noms de colonnes, les champs nécessaires à enregistrer, les propriétés des données, etc.
    import androidx.room.ColumnInfo
    import androidx.room.Entity
    import androidx.room.PrimaryKey
    
    @Entity(tableName = "books")
    data class Book(
       @PrimaryKey val id: Int,
       val title: String?,
       val city: String?,
       val year: Int?,
       @ColumnInfo(name = "publishing_house") val publishingHouse: String?
    )
    
  2. Utilisation de TypeConverters, qui décrivent la logique de conversion d’un objet en un type de données simple, adapté au stockage dans une seule cellule. Un exemple classique est que le type Date peut être enregistré dans la base de données sous forme de timestamp.
    class Converters {
       @TypeConverter
       fun fromUnix(unixTime: Long?): Date? {
           return if (unixTime == null) null else Date(unixTime)
       }
    
       @TypeConverter
       fun toUnix(date: Date?): Long? {
           return date?.time
       }
    }
    

    La conversion en valeurs primitives peut concerner des objets plus complexes, tels que des tableaux et des listes.

    @TypeConverter
    public static ArrayList fromString(String value) {
        Type listType = new TypeToken>() {}.getType();
        return new Gson().fromJson(value, listType);
    }
    
    @TypeConverter
    public static String fromArrayList(ArrayList list) {
        Gson gson = new Gson();
        String json = gson.toJson(list);
        return json;
    }
    
  3. Utilisation de l’annotation @Embedded. Lors de son utilisation, Room créera automatiquement des champs supplémentaires pour les attributs de l’objet imbriqué dans la table parente. Avec une simplicité évidente, la principale limitation de cette approche est évidente : elle ne convient que pour enregistrer des objets imbriqués.

Realm

Comme Realm a été initialement développé comme une base de données orientée objet, la conversion des modèles d’application pour les enregistrer dans la base de données nécessite un minimum d’effort de la part du développeur. Tout d’abord, la classe modèle doit être accessible pour l’héritage (en Java, rien n’a besoin d’être modifié, en Kotlin – ajoutez le modificateur open). De plus, cette classe doit descendre de la classe abstraite RealmObject, qui encapsule la logique d’interaction avec la base de données.

import io.realm.RealmObject
import io.realm.annotations.PrimaryKey

open class Book(
   @PrimaryKey var id: Int,
   var title: String?,
   var city: String?,
   var year: Int?,
   var publishingHouse: String?
) : RealmObject()

L’enregistrement d’objets imbriqués n’est pas non plus un problème. Les exigences pour les objets imbriqués sont les mêmes que pour la classe modèle.

open class Author(
   @PrimaryKey var id: Int,
   var firstName: String?,
   var lastName: String?,
   var dateOfBirth: Date?,
   var dateOfDeath: Date?,
   var books: RealmList
) : RealmObject()

ObjectBox

Le principe de fonctionnement d’ObjectBox est similaire à celui de Realm, il nécessite donc également une manipulation minimale des classes de modèles pour les enregistrer dans la base de données : ajout des annotations @Entity et @Id, attributs publics ou leurs getters et setters.

@Entity
data class Book(
   @Id var id: Long = 0,
   var title: String?,
   var city: String?,
   var year: Int?,
   var publishingHouse: String?
)

L’enregistrement de liens vers d’autres objets nécessite de spécifier le type de connexion – ToOne, s’il n’y a qu’un seul objet associé, ou ToMany s’il y en a plusieurs. Dans notre cas, nous pouvons compléter notre classe Book avec un lien vers les auteurs.

lateinit var authors: ToMany

Récupération de données avec de nombreuses conditions

SQLite et Room

Ils offrent de puissantes fonctionnalités pour générer des requêtes complexes basées sur des expressions SQL. Pour les utiliser, vous devez avoir une bonne connaissance de SQL, ainsi que des spécificités de son implémentation dans SQLite.

Par exemple, une requête ressemble à ceci, retournant tous les livres après une année donnée, triés par ordre alphabétique.

@Query("SELECT * FROM books WHERE year > :minYear ORDER BY title")
fun loadAllBooksNewerThan(minYear: Int): List

Une requête retournant tous les livres avec un titre spécifique publiés entre des années spécifiées.

@Query("SELECT * FROM books WHERE title LIKE :search " +
       "AND year BETWEEN :dateFrom AND :dateTo")
fun findBooksByTitleForPeriod(search: String, dateFrom: Int, dateTo: Int): List

Une requête retournant une liste d’éditeurs enregistrés dans une base de données pour une ville spécifique.

@Query("SELECT DISTINCT publishing_house from books WHERE city = :city")
fun getPublishingHousesForCity(city: String): List

Realm

Ce SGBD fournit un riche ensemble d’opérateurs pour récupérer et filtrer des données. En utilisant leurs combinaisons, vous pouvez créer des requêtes de toute complexité. Réécrivons les exemples SQLite pour Realm.

fun loadAllBooksNewerThan(minYear: Int): List {
   val books = realm
       .where(Book::class.java)
       .greaterThan("year", minYear)
       .sort("title")
       .findAll()
   return realm.copyFromRealm(books)
}


fun findBooksByTitleForPeriod(search: String, dateFrom: Int, dateTo: Int): List? {
   val books = realm
       .where(Book::class.java)
           .like("title", search)
           .and()
           .between("year", dateFrom, dateTo)
           .findAll()
   return realm.copyFromRealm(books)
}

fun getPublishingHousesForCity(city: String): List {
   val books = realm
       .where(Book::class.java)
       .equalTo("city", city)
       .distinct("publishingHouse")
       .findAll()
   return realm.copyFromRealm(books).map { it.publishingHouse }
}

ObjectBox

Dans ce SGBD, QueryBuilder est utilisé pour créer des requêtes, qui dispose d’un vaste arsenal de fonctions pour créer la sélection de données souhaitée. Le principe de création des requêtes est très similaire à celui de Realm. La principale différence est que dans Realm, la requête renvoie des données encapsulées dans la classe RealmResults. Sa tâche principale est de maintenir les liens vers les données à jour et de fournir des fonctionnalités pour y accéder. Pour obtenir les données brutes, nous devons les copier de la base de données à l’aide de la méthode copyFromRealm(). ObjectBox renvoie immédiatement les données lors de la demande. Sa deuxième caractéristique est la capacité de mettre en cache et de réutiliser les requêtes. Les exemples qui nous sont déjà familiers lors de l’utilisation d’ObjectBox ressembleront à ceci.

fun loadAllBooksNewerThan(minYear: Long): List {
   return bookBox.query()
       .greater(Book_.year, minYear)
       .order(Book_.title)
       .build()
       .find()
}

fun findBooksByTitleForPeriod(search: String, dateFrom: Long, dateTo: Long): List {
   return bookBox.query()
       .contains(Book_.title, search)
       .and()
       .between(Book_.year, dateFrom, dateTo)
       .build()
       .find()
}

fun getPublishingHousesForCity(city: String): List {
   return bookBox.query()
       .equal(Book_.city, city)
       .build()
       .property(Book_.publishingHouse)
       .distinct()
       .findStrings()
     .toList()
}

Obtention d’entités liées (un-à-un, un-à-plusieurs, plusieurs-à-plusieurs)

SQLite et Room

Bien que l’implémentation des relations entre entités soit un point fort des bases de données relationnelles, leur mise en œuvre dans SQLite et Room nécessite une quantité importante de code et d’efforts de développement. La raison en est l’interdiction d’utiliser des références d’objets. Comme l’expliquent les créateurs de Room, dans une application Android, l’accès aux objets imbriqués entraîne un chargement différé depuis le thread principal avec toutes les conséquences qui en découlent : retard dans le rendu de l’interface utilisateur ou consommation excessive de ressources. Par conséquent, la manière la plus évidente de mettre en œuvre des relations entre entités n’est pas prise en charge.

Pour mettre en œuvre des relations dans Room, vous devez créer une classe supplémentaire qui sera retournée lors de la demande des données correspondantes.

Exemple d’une relation un-à-un et un-à-plusieurs. Créons une classe Meta qui contient des informations de service sur chaque livre : code et statut. Chaque livre ne peut avoir qu’une seule métadonnée et chaque métadonnée se réfère à un seul livre.

@Entity(tableName = "meta")
data class Meta(
   @PrimaryKey val code: Long,
   val status: Int
   )

data class BookWithMeta(
   @Embedded val book: Book,
   @Relation(
       parentColumn = "id",
       entityColumn = "bookId"
   )
   val meta: Meta
)

Les livres avec métadonnées sont reçus sous forme de plusieurs requêtes, ils nécessitent donc l’annotation @Transaction.

@Transaction
@Query("SELECT * FROM books")
fun getBooksWithMeta(): List

La relation un-à-plusieurs dans Room est implémentée de manière identique, mais au lieu d’une référence à un seul objet, un lien de liste est indiqué.

Exemple d’une relation plusieurs-à-plusieurs. Créons une version de la classe Author, qui nous est déjà familière de l’exemple de Realm.

class Author(
   @PrimaryKey var authorId: Int,
   var firstName: String?,
   var lastName: String?,
   var dateOfBirth: Date?,
   var dateOfDeath: Date?,
   var books: List
)

Pour mettre en œuvre la relation, vous devez créer trois classes supplémentaires : un lien et deux résultats de requête.

@Entity(primaryKeys = ["bookId", "authorId"])
data class AuthorBookCrossRef(
   val bookId: Int,
   val authorId: Int
)

data class AuthorWithBooks(
   @Embedded val author: Author,
   @Relation(
       parentColumn = "authorId",
       entityColumn = "bookId",
       associateBy = @Junction(AuthorBookCrossRef::class)
   )
   val books: List
)

data class BookWithAuthors(
   @Embedded val book: Book,
   @Relation(
       parentColumn = "bookId",
       entityColumn = "authorId",
       associateBy = @Junction(AuthorBookCrossRef::class)
   )
   val authors: List
)

Les requêtes de données seront les suivantes.

@Transaction
@Query("SELECT * FROM books")
fun getBooksWithAuthors(): List

@Transaction
@Query("SELECT * FROM authors")
fun getAuthorsWithBooks(): List

Realm

La différence dans l’obtention de données liées entre Room et Realm est énorme. L’obtention se fait de la même manière qu’avec un simple accès à la propriété de l’objet. Point important : lors de la mise en œuvre d’une communication plusieurs-à-plusieurs, la connexion entre les objets est unidirectionnelle par défaut. Pour une dépendance bidirectionnelle entre les objets, vous devez utiliser l’annotation @LinkingObjects.

val book = realm
   .where(Book::class.java)
   .equalTo("id", bookId)
   .findFirst()
val authors = book?.authors

ObjectBox

L’obtention d’entités liées est très similaire à Realm et se fait en une seule ligne.

Exemple un-à-un

val meta = bookBox[bookId].meta.target

Exemple plusieurs-à-plusieurs

val authors = bookBox[bookId].authors

Conclusion

Critère
Type Relationnel avec ORM Orienté objet Orienté objet
Seuil d’entrée Bas Moyen Bas
Stockage d’objets complexes Pratique avec les types simples, nécessite du temps supplémentaire pour implémenter les relations entre objets. Le stockage d’objets complexes demande un minimum d’effort. Le stockage d’objets complexes demande un minimum d’effort.
Récupération de données avec de nombreuses conditions Excellent outil pour la formation de requêtes complexes à plusieurs niveaux. Nécessite de connaître SQL. Grand ensemble de fonctions intégrées pour la sélection et le tri des données. Grand ensemble de fonctions intégrées pour la sélection et le tri des données.
Récupération d’entités liées Nécessite un investissement important en temps d’implémentation. Se fait en quelques lignes. Se fait en une ligne.
Type Relationnel avec ORM
Seuil d’entrée Bas
Stockage d’objets complexes Pratique avec les types simples, nécessite du temps supplémentaire pour implémenter les relations entre objets.
Récupération de données avec de nombreuses conditions Excellent outil pour la formation de requêtes complexes à plusieurs niveaux. Nécessite de connaître SQL.
Récupération d’entités liées Nécessite un investissement important en temps d’implémentation.
Type Orienté objet
Seuil d’entrée Moyen
Stockage d’objets complexes Le stockage d’objets complexes demande un minimum d’effort.
Récupération de données avec de nombreuses conditions Grand ensemble de fonctions intégrées pour la sélection et le tri des données.
Récupération d’entités liées Se fait en quelques lignes.
Type Orienté objet
Seuil d’entrée Bas
Stockage d’objets complexes Le stockage d’objets complexes demande un minimum d’effort.
Récupération de données avec de nombreuses conditions Grand ensemble de fonctions intégrées pour la sélection et le tri des données.
Récupération d’entités liées Se fait en une ligne.

Ainsi, les opérations complexes de stockage et de récupération de données dans les bases de données les plus populaires pour les applications Android ont leurs propres spécificités. La combinaison de SQLite et Room, malgré son statut de solution la plus populaire pour le stockage de données structurées, nécessite des manipulations importantes pour stocker et récupérer des objets liés, ainsi qu’une bonne connaissance des bases de SQL pour un travail efficace. Realm et ObjectBox, basés sur une approche orientée objet de l’organisation des données, surpassent significativement leurs concurrents en termes de vitesse et de convivialité.

Découvrez comment nous avons conçu une solution de base de données multiplateforme à l’aide de MS SQL

Veuillez saisir votre adresse courriel professionnelle n'est pas un courriel professionnel

Image de section