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.
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é
.@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
.@Base de données
, qui s'étend RoomDatabase
. La classe définit la liste des entités et ses DAO.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"
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
.
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 ?, //…)
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.
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 Remarque
s à 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)
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.
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
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
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
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..
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.
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 ()
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.
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+!