
Room est une couche d'abstraction au-dessus de SQLite qui simplifie le stockage. Si vous ĂȘtes nouveau dans Room, consultez cet article d'introduction:
7 étapes pour utiliser Room. Procédure pas à pas pour la migration de votre application vers Room
Et dans cet article, je voudrais partager quelques conseils sur la façon d'utiliser Room aussi efficacement que possible.
1. Préremplir la base de données
Devez-vous ajouter des données par défaut à votre base de données immédiatement aprÚs sa création ou au moment du premier accÚs? Utilisez RoomDatabase # Callback . Appelez la méthode addCallback lors de la création de votre base de données et remplacez onCreate ou onOpen .
onCreate
sera appelé lors de la premiÚre création de la base de données, immédiatement aprÚs la création des tables. onOpen
est appelé lorsque la base de données est ouverte. Puisque l'accÚs au DAO n'est possible qu'aprÚs l'achÚvement de ces méthodes, nous créons un nouveau flux dans lequel nous obtenons un lien vers la base de données, puis nous obtenons le DAO et insérons les données nécessaires.
Room.databaseBuilder(context.applicationContext, DataDatabase::class.java, "Sample.db") // prepopulate the database after onCreate was called .addCallback(object : Callback() { override fun onCreate(db: SupportSQLiteDatabase) { super.onCreate(db)
Voir l'exemple complet ici .
Remarque: si vous utilisez l'approche ioThread
et que l'application se bloque au premier démarrage, entre la création de la base de données et l'insertion, les données ne seront jamais insérées.
2. Utilisation des capacités d'héritage DAO
Avez-vous plusieurs tables dans votre base de donnĂ©es et copiez-vous les mĂȘmes mĂ©thodes d' insertion , de mise Ă jour et de suppression ? Les DAO prennent en charge l'hĂ©ritage, alors crĂ©ez une BaseDao<T>
et définissez-y vos méthodes courantes @Insert
, @Update
et @Delete
. Laissez chaque DAO étendre BaseDao
et ajoutez des méthodes spécifiques à chacun d'eux.
interface BaseDao<T> { @Insert fun insert(vararg obj: T) } @Dao abstract class DataDao : BaseDao<Data>() { @Query("SELECT * FROM Data") abstract fun getData(): List<Data> }
Voir les détails ici .
Les DAO doivent ĂȘtre des interfaces ou des classes abstraites car Room gĂ©nĂšre leur implĂ©mentation au moment de la compilation, y compris les mĂ©thodes de BaseDao
.
3. ExĂ©cution de requĂȘtes dans les transactions sans code passe-partout
L'annotation d'une mĂ©thode Ă l'aide de @Transaction garantit que toutes les opĂ©rations de base de donnĂ©es que vous effectuez dans cette mĂ©thode sont exĂ©cutĂ©es dans la mĂȘme transaction. Une transaction ne sera pas exĂ©cutĂ©e si une exception se produit dans le corps de la mĂ©thode.
@Dao abstract class UserDao { @Transaction open fun updateData(users: List<User>) { deleteAllUsers() insertAll(users) } @Insert abstract fun insertAll(users: List<User>) @Query("DELETE FROM Users") abstract fun deleteAllUsers() }
Vous souhaiterez peut-ĂȘtre utiliser l'annotation @Transaction
pour les méthodes @Transaction
qui utilisent l'instruction select
dans les cas:
- Lorsque le rĂ©sultat de la requĂȘte est assez volumineux. En effectuant une demande en une seule transaction, vous garantissez que si le rĂ©sultat de la demande ne tient pas dans une "partie" du curseur, il ne sera pas endommagĂ© en raison des changements dans la base de donnĂ©es entre les permutations du curseur .
- Lorsque le rĂ©sultat de la requĂȘte est un POJO avec des champs
@Relation
. Chaque champ est une demande en soi, donc les exécuter en une seule transaction garantit des résultats cohérents entre les demandes.
Les @Delete
, @Update
et @Insert
, qui ont plusieurs paramÚtres, sont automatiquement lancées dans la transaction.
4. Lire uniquement ce dont vous avez besoin
Lorsque vous faites une demande à la base de données, utilisez-vous tous les champs que vous obtenez dans la réponse? Prenez soin de la quantité de mémoire utilisée par votre application et téléchargez uniquement les champs que vous utiliserez finalement. Cela augmentera également la vitesse de vos demandes en réduisant le coût des E / S. Room fera le mapping entre les colonnes et l'objet pour vous.
Considérez cet objet User
complexe:
@Entity(tableName = "users") data class User(@PrimaryKey val id: String, val userName: String, val firstName: String, val lastName: String, val email: String, val dateOfBirth: Date, val registrationDate: Date)
Sur certains écrans, nous n'avons pas besoin d'afficher toutes ces informations. Ainsi, à la place, nous pouvons créer un objet UserMinimal
qui contient uniquement les données nécessaires.
data class UserMinimal(val userId: String, val firstName: String, val lastName: String)
Dans la classe DAO, nous dĂ©finissons la requĂȘte et sĂ©lectionnons les colonnes correctes dans la table des users
.
@Dao interface UserDao { @Query(âSELECT userId, firstName, lastName FROM Users) fun getUsersMinimal(): List<UserMinimal> }
5. ContrÎle des dépendances entre les entités avec des clés étrangÚres
MĂȘme si Room ne prend pas directement en charge les relations entre les entitĂ©s, il vous permet de dĂ©finir des dĂ©pendances entre des objets Ă l'aide de clĂ©s Ă©trangĂšres.
Room a l'annotation @ForeignKey , qui fait partie de l'annotation @Entity . En termes de fonctionnalité, il est similaire aux clés étrangÚres dans SQLite . Il garantit la préservation des relations entre les entités lorsque des modifications sont apportées à la base de données. Pour l'ajouter, définissez l'objet à référencer, ainsi que les colonnes de l'objet courant et le volume auquel vous faites référence.
Considérez la classe User
et Pet
. Pet
a un propriétaire - l'ID utilisateur référencé par la clé étrangÚre.
@Entity(tableName = "pets", foreignKeys = arrayOf( ForeignKey(entity = User::class, parentColumns = arrayOf("userId"), childColumns = arrayOf("owner")))) data class Pet(@PrimaryKey val petId: String, val name: String, val owner: String)
Si vous le souhaitez, vous pouvez déterminer l'action à entreprendre lorsque le parent est supprimé ou mis à jour dans la base de données. Vous pouvez choisir l'une des options suivantes: NO_ACTION
, RESTRICT
, SET_NULL
, SET_DEFAULT
ou CASCADE
, qui se comportent de la mĂȘme maniĂšre que dans SQLite .
Remarque: dans Room, SET_DEFAULT
fonctionne comme SET_NULL
, car Room ne vous permet pas encore de définir des valeurs par défaut pour les colonnes.
6. Simplifiez les requĂȘtes un-Ă -plusieurs avec @Relation
Dans l'exemple précédent User
- Pet
, nous pouvons dire qu'il existe une relation un-Ă -plusieurs : l'utilisateur peut avoir plusieurs animaux. Supposons que nous voulons obtenir une liste d'utilisateurs avec leurs animaux de compagnie: List<UserAndAllPets>
.
data class UserAndAllPets (val user: User, val pets: List<Pet> = ArrayList())
Pour ce faire manuellement, nous avons besoin de 2 requĂȘtes: l'une pour obtenir une liste de tous les utilisateurs et l'autre pour obtenir une liste d'animaux de compagnie en fonction de l'ID utilisateur.
@Query(âSELECT * FROM Usersâ) public List<User> getUsers(); @Query(âSELECT * FROM Pets where owner = :userIdâ) public List<Pet> getPetsForUser(String userId);
Ensuite, nous allons parcourir la liste des utilisateurs et accéder à la table Pets
chaque fois.
L' annotation @Relation nous facilitera la vie: elle demandera automatiquement les objets associés. @Relation
ne peut ĂȘtre appliquĂ© qu'Ă List
ou Set
. Mettez Ă jour la classe UserAndAllPets:
class UserAndAllPets { @Embedded var user: User? = null @Relation(parentColumn = âuserIdâ, entityColumn = âownerâ) var pets: List<Pet> = ArrayList() }
Dans le DAO, nous dĂ©finissons une requĂȘte, et Room interrogera les tables Users
and Pets
et mappera les objets de maniĂšre autonome.
@Transaction @Query(âSELECT * FROM Usersâ) List<UserAndAllPets> getUsers();
7. Ăviter les fausses notifications pour les demandes observables
Supposons que vous souhaitiez obtenir un utilisateur par son identifiant Ă l'aide d'une requĂȘte observable:
@Query(âSELECT * FROM Users WHERE userId = :id) fun getUserById(id: String): LiveData<User>
Vous recevrez un nouvel objet User
chaque mise à jour. Mais vous recevrez également cet objet lorsque d'autres modifications se produisent dans la table Users
(suppressions, mises Ă jour ou insertions) qui n'ont rien Ă voir avec l'utilisateur qui vous intĂ©resse, ce qui entraĂźnera de fausses notifications. De plus, si votre requĂȘte comprend plusieurs tableaux, vous recevrez de nouveaux messages chaque fois que quelque chose change dans l'un d'eux.
Voici ce qui se passe dans les coulisses:
- SQLite a des déclencheurs qui se déclenchent chaque fois que
DELETE
, UPDATE
ou INSERT
se produit dans une table. - Room crée un InvalidationTracker qui utilise des
Observers
qui suivent toutes les modifications dans les tables observées. - Les demandes
LiveData
et LiveData
reposent toutes deux sur la notification InvalidationTracker.Observer # onInvalidated . Lorsqu'il est reçu, une demande répétée se produit.
La chambre sait seulement que la table a été changée, mais ne sait pas pourquoi ni ce qui a changé. Par conséquent, aprÚs une deuxiÚme demande, le résultat de la demande est transmis à l'aide de LiveData
ou LiveData
. Parce que La piĂšce ne stocke aucune donnĂ©e en mĂ©moire; elle ne peut pas dĂ©terminer s'il s'agit des mĂȘmes donnĂ©es ou non.
Vous devez vous assurer que votre DAO filtre les demandes et ne répond qu'aux objets nécessaires.
Si la requĂȘte observable est implĂ©mentĂ©e Ă l'aide de Flowables
, utilisez Flowable # diverUntilChanged .
@Dao abstract class UserDao : BaseDao<User>() { @Query(âSELECT * FROM Users WHERE userid = :idâ) protected abstract fun getUserById(id: String): Flowable<User> fun getDistinctUserById(id: String): Flowable<User> = getUserById(id) .distinctUntilChanged() }
Si votre requĂȘte renvoie LiveData
, vous pouvez utiliser MediatorLiveData
, qui ne recevra que les objets souhaités de la source.
fun <T> LiveData<T>.getDistinct(): LiveData<T> { val distinctLiveData = MediatorLiveData<T>() distinctLiveData.addSource(this, object : Observer<T> { private var initialized = false private var lastObj: T? = null override fun onChanged(obj: T?) { if (!initialized) { initialized = true lastObj = obj distinctLiveData.postValue(lastObj) } else if ((obj == null && lastObj != null) || obj != lastObj) { lastObj = obj distinctLiveData.postValue(lastObj) } } }) return distinctLiveData }
Dans vos DAO, la méthode qui retourne LiveData
est public
et la méthode qui interroge la base de données est protected
.
@Dao abstract class UserDao : BaseDao<User>() { @Query(âSELECT * FROM Users WHERE userid = :idâ) protected abstract fun getUserById(id: String): LiveData<User> fun getDistinctUserById(id: String): LiveData<User> = getUserById(id).getDistinct() }
Voir l'exemple de code complet ici .
Remarque: si vous demandez une liste à afficher, faites attention à la bibliothÚque de pagination , qui renverra un LivePagedListBuilder . La bibliothÚque vous aidera à calculer automatiquement la différence entre les éléments de la liste et à mettre à jour votre interface utilisateur.
Voir aussi: 7 étapes pour utiliser Room. Procédure pas à pas pour la migration de votre application vers Room