Composants d'architecture Android la bibliothèque de persistance de la pièce

Dans ce dernier article de la série Composants d'architecture Android, nous allons explorer la bibliothèque de persistance de la salle, une excellente nouvelle ressource qui facilite beaucoup le travail avec les bases de données dans Android. Il fournit une couche d'abstraction sur SQLite, des requêtes SQL vérifiées au moment de la compilation, ainsi que des requêtes asynchrones et observables. Room prend les opérations de base de données sur Android à un autre niveau.

Comme il s’agit de la quatrième partie de la série, je suppose que vous connaissez les concepts et les composants du package Architecture, tels que LiveData et LiveModel. Cependant, si vous n'avez lu aucun des trois derniers articles, vous pourrez toujours suivre. Néanmoins, si vous ne connaissez pas grand chose à propos de ces composants, prenez le temps de lire la série - vous pourrez l’apprécier.

1. Le composant de la pièce

Comme mentionné, Room n'est pas un nouveau système de base de données. C'est une couche abstraite qui enveloppe la base de données SQLite standard adoptée par Android. Cependant, Room ajoute tellement de fonctionnalités à SQLite qu'il est presque impossible de les reconnaître. Room simplifie toutes les opérations liées à la base de données et les rend beaucoup plus puissantes, car elles permettent de renvoyer des requêtes SQL vérifiées et vérifiées au moment de la compilation..

La pièce est composée de trois composants principaux: Base de données, la DAO (Objets d'accès aux données), et le Entité. Chaque composant a sa responsabilité et tous doivent être implémentés pour que le système fonctionne. Heureusement, une telle implémentation est assez simple. Grâce aux annotations et aux classes abstraites fournies, le passe-partout à implémenter Room est réduit au minimum..

  • Entité est la classe en cours d'enregistrement dans la base de données. Une table de base de données exclusive est créée pour chaque classe annotée avec @Entité.
  • le DAO est l'interface annotée avec @Dao qui médie l'accès aux objets de la base de données et de ses tables. Il existe quatre annotations spécifiques pour les opérations DAO de base: @Insérer, @Mettre à jour, @Effacer, et @Question.
  • Le composant Database est une classe abstraite annotée avec @Base de données, qui s'étend RoomDatabase. La classe définit la liste des entités et ses DAO.

2. Mise en place de l'environnement

Pour utiliser Room, ajoutez les dépendances suivantes au module d'application dans Gradle:

compiler "android.arch.persistence.room:runtime:1.0.0" annotationProcessor "android.arch.persistence.room:compiler:1.0.0"

Si vous utilisez Kotlin, vous devez appliquer le kapt plugin et ajouter une autre dépendance.

apply plugin: 'kotlin-kapt' //… dépendances //… kapt "android.arch.persistence.room:compiler:1.0.0"

3. Entité, la table de base de données

Un Entité représente l'objet en cours d'enregistrement dans la base de données. Chaque Entité class crée une nouvelle table de base de données, chaque champ représentant une colonne. Les annotations sont utilisées pour configurer des entités et leur processus de création est très simple. Remarquez comme il est simple de créer un Entité en utilisant les classes de données Kotlin.

Classe de données @Entity Note (@PrimaryKey (autoGenerate = true) var id: Long ?, var texte: String ?, var date: Long?)

Une fois qu'une classe est annotée avec @Entité, la bibliothèque de pièces créera automatiquement une table en utilisant les champs de classe comme des colonnes. Si vous devez ignorer un champ, annotez-le simplement avec @Ignorer. Chaque Entité doit également définir un @Clé primaire.

Table et Colonnes

Room utilisera la classe et ses noms de champs pour créer automatiquement une table. Cependant, vous pouvez personnaliser la table générée. Pour définir un nom pour la table, utilisez le nom de la table option sur le @Entité annotation, et pour éditer le nom des colonnes, ajoutez un @ColumnInfo annotation avec l'option de nom sur le champ. Il est important de se rappeler que les noms de table et de colonne sont sensibles à la casse..

@Entity (tableName = “tb_notes”) classe de données Note (@PrimaryKey (autoGenerate = true) @ColumnInfo (name = “_id”) var id: Long ?, //…) 

Indices et contraintes d'unicité

Room nous permet d’implémenter facilement des contraintes sur SQLite sur nos entités. Pour accélérer les requêtes de recherche, vous pouvez créer SQLite des indices aux champs qui sont plus pertinents pour de telles requêtes. Les index vont accélérer les requêtes de recherche; Cependant, ils ralentiront également l'insertion, la suppression et la mise à jour des requêtes. Vous devez donc les utiliser avec précaution. Jetez un coup d'oeil à la documentation de SQLite pour mieux la comprendre.

Il existe deux manières différentes de créer des index dans Room. Vous pouvez simplement régler le ColumnInfo propriété, indice, à vrai, laisser Room définir les indices pour vous.

@ColumnInfo (name = "date", index = true) var date: Long

Ou, si vous avez besoin de plus de contrôle, utilisez le des indices propriété du @Entité annotation, listant les noms des champs devant composer l’index dans le champ valeur propriété. Notez que l'ordre des éléments dans valeur est important car il définit le tri de la table d'index.

@Entity (tableName = "tb notes", indices = arrayOf (index (valeur = * arrayOf ("date", "titre"), nom)), nom = "idx_date_title"))) 

Une autre contrainte utile de SQLite est unique, ce qui interdit au champ marqué d'avoir des valeurs en double. Malheureusement, dans la version 1.0.0, Room ne fournit pas cette propriété comme il se doit, directement dans le champ entité. Mais vous pouvez créer un index et le rendre unique, en obtenant un résultat similaire.

@Entity (tableName = "tb_users", indices = arrayOf (index (valeur = "nomutilisateur", nom = "idx_username", unique = true))) 

D'autres contraintes comme PAS NULL, DÉFAUT, et VÉRIFIER ne sont pas présents dans Room (du moins jusqu'à maintenant, dans la version 1.0.0), mais vous pouvez créer votre propre logique sur l'entité pour obtenir des résultats similaires. Pour éviter les valeurs NULL sur les entités Kotlin, il suffit de supprimer le ? à la fin du type de variable ou, en Java, ajoutez le @NonNull annotation.

Relation entre objets

Contrairement à la plupart des bibliothèques de mappage relationnel-objet, Room ne permet pas à une entité de référencer directement une autre. Cela signifie que si vous avez une entité appelée Bloc-notes et un appelé Remarque, vous ne pouvez pas créer un Collection de Remarques à l'intérieur du Bloc-notes comme vous le feriez avec de nombreuses bibliothèques similaires. Au début, cette limitation peut sembler agaçante, mais la décision d'adapter la bibliothèque Room aux limitations de l'architecture d'Android a été prise. Pour mieux comprendre cette décision, jetez un coup d'œil à l'explication fournie par Android pour son approche..

Bien que la relation d'objet de Room soit limitée, elle existe toujours. À l'aide de clés étrangères, il est possible de référencer des objets parent et enfant et de transférer leurs modifications en cascade. Notez qu'il est également recommandé de créer un index sur l'objet enfant pour éviter les analyses de table complètes lorsque le parent est modifié..

@Entity (tableName = "tb_notes", indices = arrayOf (Index (value = * arrayOf ("note_date", "note_title"), name = "idx_date_title"), index (valeur = * arrayOf ("note_pad_id"), nom = "idx_pad_note")), foreignKeys = arrayOf (ForeignKey (entity = NotePad :: class, parentColumns = arrayOf ("pad_id"), childColumns = arrayOf ("note_pad_id"), onDelete = ForeignKey.CASCADE, onUpelate = ForeignKey.CASCADE, onUpdate = ForeignKey. ) Classe de données Note (@PrimaryKey (autoGenerate = true) @ColumnInfo (name = "id_de_la" Note) var id: Long, @ColumnInfo (name = "titre_title") var title: String ?, @ColumnInfo (name = "note_text") var text: String, @ColumnInfo (name = "note_date") var date: Long, @ColumnInfo (name = "note_pad_id") var padId: Long)

Enrobage d'objets

Il est possible d’incorporer des objets dans des entités à l’aide de la @ Embarqué annotation. Une fois qu'un objet est incorporé, tous ses champs sont ajoutés en tant que colonnes dans la table de l'entité, en utilisant les noms de champs de l'objet incorporé comme noms de colonnes. Considérons le code suivant.

classe de données Emplacement (var lat: Float, var lon: Float) @Entity (tableName = "tb_notes") classe de données Note (@PrimaryKey (autoGenerate = true) @ColumnInfo (name = "note_id") var id: Long, @Embedded (prefix = "note_location_") var location: Location?)

Dans le code ci-dessus, le Emplacement la classe est intégrée dans le Remarque entité. La table de l'entité aura deux colonnes supplémentaires, correspondant aux champs de l'objet incorporé. Puisque nous utilisons la propriété prefix sur le @ Embarqué annotation, les noms des colonnes seront 'note_location_lat' et 'note_location_lon', et il sera possible de référencer ces colonnes dans les requêtes.

4. Objet d'accès aux données

Pour accéder aux bases de données de la salle, un objet DAO est nécessaire. Le DAO peut être défini comme une interface ou une classe abstraite. Pour l'implémenter, annotez la classe ou l'interface avec @Dao et vous êtes bon pour accéder aux données. Même s'il est possible d'accéder à plus d'une table à partir d'un DAO, il est recommandé, au nom d'une bonne architecture, de maintenir le principe de séparation des problèmes et de créer un DAO responsable de l'accès à chaque entité..

@Dao interface NoteDAO 

Insérer, mettre à jour et supprimer

Room fournit une série d’annotations pratiques pour les opérations CRUD dans le DAO: @Insérer, @Mettre à jour, @Effacer, et @Question. le @Insérer l'opération peut recevoir une seule entité, une tableau, ou un liste des entités en tant que paramètres. Pour des entités uniques, il peut retourner un longue, représentant la ligne de l'insertion. Pour plusieurs entités en tant que paramètres, il peut retourner un longue[] ou un liste au lieu.

@Insert (onConflict = OnConflictStrategy.REPLACE) fun insertNote (note: Remarque): Longue @Insert (onConflict = OnConflictStrategy.ABORT) fun insertNotes (notes: List): Liste

Comme vous pouvez le constater, il y a une autre propriété dont il faut parler: sur le conflit. Ceci définit la stratégie à suivre en cas de conflits en utilisant OnConflictStrategy des constantes. Les options sont assez explicites, avec AVORTER, ÉCHOUER, et REMPLACER étant les possibilités les plus importantes.

Pour mettre à jour des entités, utilisez le @Mettre à jour annotation. Il suit le même principe que @Insérer, recevoir des entités uniques ou des entités multiples comme arguments. Room utilisera l'entité réceptrice pour mettre à jour ses valeurs, en utilisant l'entité Clé primaire comme référence. Cependant, le @Mettre à jour peut seulement retourner un int représentant le total des lignes de la table mises à jour.

@Update () fun updateNote (note: note): Int

Encore une fois, suivant le même principe, le @Effacer annotation peut recevoir une ou plusieurs entités et renvoyer une int avec le total des lignes de la table mis à jour. Il utilise également le Clé primaire trouver et supprimer le registre dans la table de la base de données.

@Delete fun deleteNote (note: note): Int

Faire des requêtes

Finalement, le @Question L'annotation fait des consultations dans la base de données. Les requêtes sont construites de manière similaire aux requêtes SQLite, la plus grande différence étant la possibilité de recevoir des arguments directement des méthodes. Mais la caractéristique la plus importante est que les requêtes sont vérifiées au moment de la compilation, ce qui signifie que le compilateur trouvera une erreur dès que vous construisez le projet..

Pour créer une requête, annotez une méthode avec @Question et écrivez une requête SQLite comme valeur. Nous ne ferons pas trop attention à la manière d'écrire des requêtes car ils utilisent le standard SQLite. Mais généralement, vous utiliserez des requêtes pour récupérer des données de la base de données à l'aide du SÉLECTIONNER commander. Les sélections peuvent renvoyer des valeurs uniques ou de collection.

@Query ("SELECT * FROM tb_notes") fun findAllNotes (): List

Il est très simple de passer des paramètres aux requêtes. Room déduira le nom du paramètre en utilisant le nom de l'argument de la méthode. Pour y accéder, utilisez :, suivi du nom.

@Query ("SELECT * FROM tb_notes WHERE note_id =: id") fun findNoteById (id: Long): Remarque @Query ("SELECT * FROM tb_noted WHERE note_date BETWEEN: early AND: late") fun findNoteByDate (early: date, en retard : Date): Liste

Requêtes LiveData

La chambre a été conçue pour fonctionner avec élégance Données en direct. Pour un @Question retourner un Données en direct, il suffit de terminer le retour standard avec Données en direct et vous êtes prêt à partir.

@Query ("SELECT * FROM tb_notes WHERE note_id =: id") fun findNoteById (id: Long): LiveData

Après cela, il sera possible d'observer le résultat de la requête et d'obtenir des résultats asynchrones assez facilement. Si vous ne connaissez pas la puissance de LiveData, prenez le temps de lire notre tutoriel sur le composant..

5. Création de la base de données

La base de données est créée par une classe abstraite, annotée avec @Base de données et étendre la RoomDatabase classe. De plus, les entités qui seront gérées par la base de données doivent être passées dans un tableau du entités propriété dans le @Base de données annotation.

@Database (entités = arrayOf (NotePad :: class, Note :: class)) classe abstraite Base de données: RoomDatabase () abstraite fun padDAO (): PadDAO abstraite fun noteDAO (): NoteDAO

Une fois que la classe de base de données est implémentée, il est temps de construire. Il est important de souligner que l'instance de base de données ne devrait idéalement être construite qu'une fois par session. Le meilleur moyen d'y parvenir serait d'utiliser un système d'injection de dépendance, comme Dagger. Cependant, nous n'allons pas plonger dans DI maintenant, car cela sort du cadre de ce tutoriel..

fun suppliesAppDatabase (): Database return Room.databaseBuilder (context, Database :: class.java, "base de données") .build ()

Normalement, les opérations sur une base de données Room ne peuvent pas être effectuées à partir du thread d'interface utilisateur, car elles bloquent et vont probablement créer des problèmes pour le système. Toutefois, si vous souhaitez forcer l'exécution sur le thread d'interface utilisateur, ajoutez allowMainThreadQueries aux options de construction. En fait, il existe de nombreuses options intéressantes pour la construction de la base de données, et je vous conseille de lire la RoomDatabase.Builder documentation pour comprendre les possibilités.

6. Conversion de types de données et de données

Une colonne de type de données est automatiquement définie par Pièce. Le système déduira du type de champ quel type de type de données SQLite est le plus approprié. Gardez à l'esprit que la plupart des POJO de Java seront convertis immédiatement; Cependant, il est nécessaire de créer des convertisseurs de données pour gérer automatiquement des objets plus complexes non reconnus par Room, tels que Rendez-vous amoureux et Enum.

Pour que Room comprenne les conversions de données, il est nécessaire de fournir TypeConvertisseurs et enregistrez ces convertisseurs dans Room. Il est possible d’effectuer cette inscription en tenant compte d’un contexte particulier. Par exemple, si vous enregistrez la TypeConverter dans le Base de données, toutes les entités de la base de données utiliseront le convertisseur. Si vous vous inscrivez sur une entité, seules les propriétés de cette entité peuvent l'utiliser, etc..

Pour convertir un Rendez-vous amoureux objet directement à un Longue pendant les opérations de sauvegarde de la salle, puis convertir un Longue à un Rendez-vous amoureux lors de la consultation de la base de données, commencez par déclarer un TypeConverter.

class DataConverters @TypeConverter fun fromTimestamp (mills: Long?): Date? return if (mills == null) null other Date (mills) @TypeConverter fun fromDate (date: date?): Longue? = date? heure

Ensuite, enregistrez le TypeConverter dans le Base de données, ou dans un contexte plus spécifique si vous voulez.

@Database (entités = arrayOf (NotePad :: class, Note :: class), version = 1) @TypeConverters (DataConverters :: class) classe abstraite Database: RoomDatabase ()

7. Utilisation de Room dans une application

L'application que nous avons développée au cours de cette série a utilisé Préférences partagées mettre en cache les données météorologiques. Maintenant que nous savons utiliser Room, nous allons l'utiliser pour créer un cache plus sophistiqué nous permettant d'obtenir des données en cache par ville et en tenant compte de la date météo lors de l'extraction des données..

Tout d'abord, créons notre entité. Nous enregistrerons toutes nos données en utilisant uniquement le MétéoMain classe. Il suffit d’ajouter des annotations à la classe et c’est fini.

Classe de données @Entity (tableName = "weather") WeatherMain (@ColumnInfo (name = "date")) var dt: Long ?, @ColumnInfo (name = "ville") var name: String ?, @ColumnInfo (name = "temp_min ") var tempMin: Double ?, @ColumnInfo (name =" temp_max ") var tempMax: Double ?, @ColumnInfo (name =" main ") var main: Chaîne ?, @ColumnInfo (nom =" description ") description de la variable: String ?, @ColumnInfo (name = "icon") icône var: String?) @ColumnInfo (name = "id") @PrimaryKey (autoGenerate = true) var id: Long = 0 //… 

Nous avons également besoin d'un DAO. le WeatherDAO gérera les opérations CRUD dans notre entité. Notez que toutes les requêtes retournent Données en direct.

@Dao interface WeatherDAO @Insert (onConflict = OnConflictStrategy.REPLACE) Fun insert (w: WeatherMain) @Delete fun remove (w: WeatherMain) @Query ("SELECT * FROM weather" + "COMMANDER PAR id ID DESC LIMIT 1") fun findLast (): LiveData @Query ("SELECT * FROM météo" + "WHERE ville comme: ville" + "ORDER BY date DESC LIMIT 1") fun findByCity (ville: String): LiveData @Query ("SELECT * FROM weather" + "WHERE date < :date " + "ORDER BY date ASC LIMIT 1" ) fun findByDate( date: Long ): List 

Enfin, il est temps de créer le Base de données.

@Database (entités = arrayOf (WeatherMain :: class), version = 2) classe abstraite Base de données: RoomDatabase () résumé amusant weatherDAO (): WeatherDAO

Ok, notre base de données Room est maintenant configurée. Tout ce qui reste à faire est de le câbler avec Poignard et commencez à l'utiliser. dans le Module de données, fournissons le Base de données et le WeatherDAO.

@Module class DataModule (val contexte: Context) //… @Provides @Singleton fun fournitAppDatabase (): Base de données return Room.databaseBuilder (contexte, Database :: class.java, "database") .allowMainThreadQueries (). ) .build () @Provides @Singleton fun fournitWeatherDAO (base de données: Base de données): WeatherDAO return database.weatherDAO ()

Comme vous devez vous en souvenir, nous avons un référentiel responsable de la gestion de toutes les opérations de données. Continuons à utiliser cette classe pour la demande de données de salle de l'application. Mais d’abord, nous devons éditer le fournitMainRépôt méthode du Module de données, inclure le WeatherDAO pendant la construction de la classe.

@Module class DataModule (val contexte: Context) //… @Provides @Singleton fun fournitMainRepository (openWeatherService: OpenWeatherService, prefsDAO: PrefsDAO, weatherDAO, locationLiveData: LocationLiveData): MainRepository (retourPermodisteSite) ) /…

La plupart des méthodes que nous ajouterons à la MainRepository sont assez simples. Cela vaut la peine de regarder de plus près clearOldData (), bien que. Cela efface toutes les données antérieures à un jour, en ne conservant que les données météorologiques pertinentes enregistrées dans la base de données..

classe MainRepository @Inject constructeur (val privé openWeatherService: OpenWeatherService, val privé valsDAO: PrefsDAO, privé val weatherDAO: WeatherDAO, privé val emplacement: LocationLiveData): AnkoLogger fun getWeatherByCity (ville: Chaîne): LiveData> info ("getWeatherByCity: $ city") return openWeatherService.getWeatherByCity (city) fun saveOnDb (weatherMain: WeatherMain) info ("saveOnDb: \ n $ weatherMain") weatherDAO.insert (weatherMain) fun getRecentWeather (: Données en direct info ("getRecentWeather") return weatherDAO.findLast () fun getRecentWeatherForLocation (emplacement: String): LiveData info ("getWeatherByDateAndLocation") return weatherDAO.findByCity (location) fun clearOldData () info ("clearOldData") val c = Calendar.getInstance () c.add (Calendar.DATE, -1) // obtenir les données météo depuis 2 jours val oldData = weatherDAO.findByDate (c.timeInMillis) oldData.forEach w -> info ("Suppression des données pour '$ w.name': $ w.dt") weatherDAO.remove (w ) //…

le MainViewModel est responsable de faire des consultations à notre référentiel. Ajoutons un peu de logique pour adresser nos opérations à la base de données Room. Tout d'abord, nous ajoutons un MutableLiveData, la weatherDB, qui est chargé de consulter le MainRepository. Ensuite, nous supprimons les références à Préférences partagées, faire en sorte que notre cache repose uniquement sur la base de données Room.

Classe constructeur MainViewModel @Inject (référentiel de valeurs privé: MainRepository): ViewModel (), AnkoLogger //… // Météo enregistrée dans la base de données private var weatherDB: LiveData = MutableLiveData () //… // nous supprimons la consultation dans SharedPreferences // rendant le cache exclusif à Fun, divertissement privé getWeatherCached () info ("getWeatherCached") weatherDB = repository.getRecentWeather () weather.addSource (weatherDB, w -> info ("weatherDB: DB: \ n $ w") weather.postValue (ApiResponse (data = w)) weather.removeSource (weatherDBSaved))

Pour rendre notre cache pertinent, nous effacerons les anciennes données à chaque nouvelle consultation météo..

 var privé weatherByLocationResponse: LiveData> = Transformations.switchMap (emplacement, l -> info ("weatherByLocation: \ nlocation: $ l") doAsync repository.clearOldData () return @ switchMap repository.getWeatherByLocation (l)) privé var weatherByCityResponse: LiveData> = Transformations.switchMap (cityName, city -> info ("weatherByCityResponse: city: $ city") doAsync repository.clearOldData () return @ switchMap repository.getWeatherByCity (city))

Enfin, nous enregistrerons les données dans la base de données de la salle chaque fois qu'une nouvelle météo est reçue..

// Reçoit la réponse météorologique mise à jour, // l'envoie à l'interface utilisateur et l'enregistre également. Fun fun updateWeather (w: WeatherResponse) info ("updateWeather") // obtenir la météo à partir d'aujourd'hui val weatherMain = WeatherMain.factory (w) // save sur les préférences partagées repository.saveWeatherMainOnPrefs (weatherMain) // enregistrer sur la base de données repository.saveOnDb (weatherMain) // mettre à jour la valeur météo weather.postValue (ApiResponse (data = weatherMain))

Vous pouvez voir le code complet dans le dépôt GitHub pour ce post.

Conclusion

Enfin, nous en sommes à la conclusion de la série Android Architecture Components. Ces outils seront d’excellents compagnons dans votre parcours de développement Android. Je vous conseille de continuer à explorer les composants. Essayez de prendre un peu de temps pour lire la documentation. 

Et consultez certains de nos autres articles sur le développement d'applications Android ici sur Envato Tuts+!