Lorsque vous utilisez la bibliothèque de persistance Room pour stocker les données de votre application, vous interagissez avec les données stockées en définissant des objets d'accès aux données, ou DAO. Chaque DAO inclut des fonctions qui offrent un accès abstrait à la base de données de votre application. Au moment de la compilation, Room génère automatiquement des implémentations des DAO que vous définissez.
En utilisant des DAO pour accéder à la base de données de votre application au lieu de créer des compilateurs ou des requêtes directes, vous préservez la séparation des préoccupations, qui est un principe architectural essentiel. Les DAO vous permettent également de simuler l'accès à la base de données lorsque vous testez votre application.
Anatomie d'un DAO
Vous pouvez définir chaque DAO en tant qu'interface ou classe abstraite. Pour les cas d'utilisation de base, vous devez généralement recourir à une interface. Dans les deux cas, vous devez toujours
annoter vos DAO avec @Dao. Les DAO ne contiennent pas de propriétés, mais ils définissent une ou plusieurs fonctions permettant d'interagir avec les données de la base de données de votre application.
Le code suivant est un exemple de DAO qui définit des fonctions permettant d'insérer, de supprimer et de sélectionner des objets User dans une base de données Room :
@Dao interface UserDao { @Insert suspend fun insertAll(vararg users: User) @Delete suspend fun delete(user: User) @Query("SELECT * FROM user") suspend fun getAll(): List<User> }
Il existe deux types de fonctions DAO qui définissent les interactions avec la base de données :
- Fonctions pratiques qui vous permettent d'insérer, de mettre à jour et de supprimer des lignes dans votre base de données sans écrire de code SQL.
- Fonctions de requête qui vous permettent d'écrire votre propre requête SQL pour interagir avec la base de données.
Les sections suivantes expliquent comment utiliser les deux types de fonctions DAO pour définir les interactions avec la base de données dont votre application a besoin.
Fonctions pratiques
Room fournit des annotations pratiques pour définir des fonctions qui effectuent des insertions, des mises à jour et des suppressions sans que vous ayez à écrire d'instruction SQL.
Si vous devez définir des insertions, des mises à jour ou des suppressions plus complexes, ou si vous devez interroger les données de la base de données, utilisez plutôt une fonction de requête.
Insérer
L'annotation @Insert vous permet de définir des fonctions qui insèrent leurs
paramètres dans la table appropriée de la base de données. Le code suivant montre des exemples de fonctions @Insert valides qui insèrent un ou plusieurs objets User dans la base de données :
@Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertUsers(vararg users: User) @Insert suspend fun insertBothUsers(user1: User, user2: User) @Insert suspend fun insertUsersAndFriends(user: User, friends: List<User>) }
Chaque paramètre d'une fonction @Insert doit être une instance d'une classe d'entités de données Room
annotée avec @Entity ou une collection d'instances de classe d'entité de données
. Lorsqu'une fonction @Insert est appelée, Room insère chaque instance d'entité transmise dans la table de base de données correspondante.
Si la fonction @Insert reçoit un seul paramètre, elle peut renvoyer une valeur Long, qui correspond au nouveau rowId de l'élément inséré. Si le paramètre est un tableau ou une collection, il doit renvoyer un tableau ou une collection de valeurs Long, chaque valeur étant le rowId de l'un des éléments insérés.
Pour en savoir plus sur le renvoi des valeurs rowId, consultez la documentation de référence
sur l'annotation @Insert, ainsi que la documentation SQLite
sur les tables d'ID de ligne.
Mettre à jour
L'annotation @Update vous permet de définir des fonctions qui mettent à jour des lignes spécifiques
dans une table de base de données. Comme les fonctions @Insert, les fonctions @Update acceptent les instances d'entités de données comme paramètres. Le code suivant montre un exemple de fonction @Update qui tente de mettre à jour un ou plusieurs objets User dans la base de données :
@Dao interface UserDao { @Update suspend fun updateUsers(vararg users: User) }
Room utilise la clé primaire pour faire correspondre les instances d'entités dans les arguments aux lignes de la base de données. S'il n'existe aucune ligne avec la même clé primaire, Room n'effectue aucune modification.
Une fonction @Update peut éventuellement renvoyer une valeur Int indiquant le nombre de lignes qui ont été mises à jour.
Supprimer
L'@Delete annotation vous permet de
définir des fonctions qui suppriment des lignes spécifiques d'une table de base de données. Comme les fonctions @Insert, les fonctions @Delete acceptent les instances d'entités de données comme paramètres. Le code suivant montre un exemple de fonction @Delete qui tente de supprimer un ou plusieurs objets User de la base de données :
@Dao interface UserDao { @Delete suspend fun deleteUsers(vararg users: User) }
Room utilise la clé primaire pour faire correspondre les instances d'entités dans les arguments aux lignes de la base de données. S'il n'existe aucune ligne avec la même clé primaire, Room n'effectue aucune modification.
Une fonction @Delete peut éventuellement renvoyer une valeur Int indiquant le nombre de lignes qui ont été supprimées.
Faire un upsert
L'annotation @Upsert vous permet de
définir des fonctions qui insèrent des instances d'entités lorsqu'aucune ligne ne correspond, ou qui les
mettent à jour si une ligne existe déjà avec la même clé primaire.
Comme les fonctions @Insert et @Update, les fonctions @Upsert acceptent les instances d'entités de données comme paramètres. Le code suivant montre un exemple de fonction @Upsert qui tente de faire un upsert d'un ou plusieurs objets User dans la base de données :
@Dao interface UserDao { @Upsert suspend fun upsertUsers(vararg users: User) }
Si la fonction @Upsert reçoit un seul paramètre, elle peut renvoyer une valeur Long. Si elle entraîne l'insertion d'une nouvelle ligne, elle renvoie le rowId de la ligne nouvellement insérée. Si elle entraîne la mise à jour d'une ligne existante, elle renvoie -1. Si le paramètre est un tableau ou une collection, il doit renvoyer un tableau ou une collection de valeurs Long.
Fonctions de requête
L'annotation @Query vous permet d'
écrire des instructions SQL et de les exposer en tant que fonctions DAO. Utilisez ces fonctions de requête pour interroger les données de la base de données de votre application ou lorsque vous devez effectuer des insertions, des mises à jour et des suppressions plus complexes.
Room valide les requêtes SQL au moment de la compilation. Autrement dit, en cas de problème avec votre requête, une erreur de compilation se produit au lieu d'un échec d'exécution.
Requêtes simples
Le code suivant définit une fonction qui utilise une requête SELECT pour renvoyer tous les objets User de la base de données :
@Query("SELECT * FROM user") suspend fun loadAllUsers(): List<User>
Les sections suivantes expliquent comment modifier cet exemple pour des cas d'utilisation types.
Afficher un sous-ensemble de colonnes d'une table
La plupart du temps, vous n'avez besoin que d'afficher un sous-ensemble des colonnes de la table que vous interrogez. Par exemple, votre interface utilisateur peut n'afficher que le prénom et le nom d'un utilisateur au lieu de toutes les informations le concernant. Pour économiser des ressources et simplifier l'exécution de votre requête, n'interrogez que les propriétés dont vous avez besoin.
Room vous permet d'afficher un objet de données à partir de n'importe laquelle de vos requêtes, à condition que vous puissiez mapper l'ensemble des colonnes de résultat sur l'objet renvoyé. Par exemple, vous pouvez définir l'objet suivant pour contenir le prénom et le nom d'un utilisateur :
data class NameTuple( @ColumnInfo(name = "first_name") val firstName: String, @ColumnInfo(name = "last_name") val lastName: String )
Vous pouvez ensuite afficher cet objet de données à partir de votre fonction de requête :
@Query("SELECT first_name, last_name FROM user") suspend fun loadFullName(): List<NameTuple>
Étant donné que la requête renvoie des valeurs pour les colonnes first_name et last_name, Room mappe ces valeurs sur les propriétés de la classe NameTuple. Si la requête renvoie une colonne qui ne correspond pas à une propriété de l'objet renvoyé, Room affiche un avertissement.
Bien que l'exemple précédent utilise une classe de données personnalisée pour récupérer un sous-ensemble de colonnes, Room est également compatible avec le renvoi de kotlin.Pair et kotlin.Triple pour plus de commodité lorsqu'une requête renvoie exactement deux ou trois colonnes. Lorsque vous utilisez ces types, les colonnes sont mappées par ordre de définition dans l'instruction de requête. L'ordre des colonnes dans l'instruction SELECT doit donc correspondre à l'ordre des types dans Pair ou Triple.
Transmettre des paramètres simples à une requête
La plupart du temps, vos fonctions DAO doivent accepter des paramètres afin de pouvoir effectuer des opérations de filtrage. Room est compatible avec l'utilisation de paramètres de fonction en tant que paramètres de liaison dans vos requêtes.
Par exemple, le code suivant définit une fonction qui renvoie tous les utilisateurs ayant plus d'un certain âge :
@Query("SELECT * FROM user WHERE age > :minAge") suspend fun loadAllUsersOlderThan(minAge: Int): Array<User>
Vous pouvez également transmettre plusieurs paramètres ou référencer le même paramètre plusieurs fois dans une requête, comme illustré dans le code suivant :
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge") suspend fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<User> @Query( """ SELECT * FROM user WHERE first_name LIKE :search OR last_name LIKE :search """ ) suspend fun findUserWithName(search: String): List<User>
Transmettre une collection de paramètres à une requête
Certaines de vos fonctions DAO peuvent nécessiter la transmission d'un nombre variable de paramètres qui n'est connu qu'au moment de l'exécution. Si un paramètre représente une collection, il est automatiquement développé au moment de l'exécution en fonction du nombre de valeurs.
Par exemple, le code suivant définit une fonction qui renvoie des informations sur tous les utilisateurs d'un sous-ensemble de régions :
@Query("SELECT * FROM user WHERE region IN (:regions)") suspend fun loadUsersFromRegions(regions: List<String>): List<User>
Interroger plusieurs tables
Certaines de vos requêtes peuvent nécessiter l'accès à plusieurs tables pour calculer le résultat. Vous pouvez utiliser des clauses JOIN dans des requêtes SQL pour référencer plusieurs tables.
Le code suivant définit une fonction qui joint trois tables pour renvoyer les livres actuellement empruntés par un utilisateur spécifique :
@Query( """ SELECT * FROM book INNER JOIN loan ON loan.book_id = book.id INNER JOIN user ON user.id = loan.user_id WHERE user.name LIKE :userName """ ) suspend fun findBooksBorrowedByName(userName: String): List<Book>
Vous pouvez également définir des objets de données pour afficher un sous-ensemble de colonnes à partir de plusieurs tables jointes. Pour en savoir plus, consultez Afficher un sous-ensemble de colonnes d'une table. Le code suivant définit un DAO avec une fonction qui renvoie les noms des utilisateurs et les noms des livres qu'ils ont empruntés :
interface UserBookDao { @Query( """ SELECT user.name AS userName, book.name AS bookName FROM user, book WHERE user.id = book.user_id """ ) fun loadUserAndBookNames(): Flow<List<UserBook>> } data class UserBook(val userName: String, val bookName: String)
Renvoyer un résultat à plusieurs mappages
Pour les opérations de jointure, vous pouvez également interroger des colonnes à partir de plusieurs tables sans définir de classe de données supplémentaire en écrivant des fonctions de requête qui renvoient un résultat à plusieurs mappages.
Prenons l'exemple de la section Interroger plusieurs tables. Au lieu d'afficher une liste d'instances d'une classe de données personnalisée contenant des paires d'instances User et Book, vous pouvez afficher un mappage de User et Book directement à partir de votre fonction de requête :
@Query( """ SELECT * FROM user JOIN book ON user.id = book.user_id """ ) suspend fun loadUserAndBookNames(): Map<User, List<Book>>
Lorsque votre fonction de requête renvoie un résultat à plusieurs mappages, vous pouvez écrire des requêtes qui utilisent des clauses GROUP BY, ce qui vous permet de profiter des fonctionnalités SQL pour les calculs et les filtres avancés. Par exemple, vous pouvez modifier votre fonction loadUserAndBookNames pour n'afficher que les utilisateurs ayant emprunté au moins trois livres :
@Query( """ SELECT * FROM user JOIN book ON user.id = book.user_id GROUP BY user.name HAVING COUNT(book.id) >= 3 """ ) suspend fun loadUserAndBookNamesGrouped(): Map<User, List<Book>>
Si vous n'avez pas besoin de mapper des objets entiers, vous pouvez également afficher des mappages entre
des colonnes spécifiques de votre requête à l'aide de l'annotation @MapColumn sur les
paramètres génériques du type de retour.
@Query( """ SELECT user.name AS username, book.name AS bookname FROM user JOIN book ON user.id = book.user_id """ ) suspend fun loadUserAndBookNamesColumns(): Map< @MapColumn(columnName = "username") String, List<@MapColumn(columnName = "bookname") String> >
Types de retours spéciaux
Room fournit des types de retours spéciaux pour l'intégration avec d'autres bibliothèques d'API.
Requêtes paginées avec la bibliothèque Paging
Room est compatible avec les requêtes paginées grâce à l'intégration dans la bibliothèque Paging. Pour utiliser les types renvoyés Paging 3, vous devez enregistrer les convertisseurs de types renvoyés Paging dans votre base de données ou votre DAO :
- Incluez l'artefact
androidx.room3:room3-pagingdans votre configuration de compilation. - Annotez votre
@Databaseou@Daodéclaration avec@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class).
Une fois enregistrés, vos DAO peuvent renvoyer des objets PagingSource à utiliser avec
Paging 3 :
@Dao @DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class) interface UserDao { @Query("SELECT * FROM users WHERE label LIKE :query") fun pagingSource(query: String): PagingSource<Int, User> }
Pour plus d'informations sur la sélection des paramètres de type pour un élément PagingSource, consultez
Sélectionner des types de clés et de valeurs.
Accès direct à la connexion à la base de données
Si la logique de votre application nécessite un accès direct et de bas niveau à la connexion à la base de données, vous pouvez utiliser les API de connexion de Room. Vous pouvez obtenir une
connexion à l'aide de
useReaderConnection
pour les opérations en lecture seule ou
useWriterConnection
pour les opérations d'écriture sur votre RoomDatabase instance, et utiliser
usePrepared
pour exécuter des instructions :
val result: List<Pair<Long, String>> = roomDatabase.useReaderConnection { connection -> connection.usePrepared( "SELECT * FROM user WHERE age > :minAge LIMIT 5" ) { stmt -> // Bind arguments if needed stmt.bindLong(1, minAge.toLong()) buildList { // Step through the results while (stmt.step()) { add(stmt.getLong(0) to stmt.getText(1)) } } } }
Si vous devez effectuer des transactions de base de données de bas niveau directement sur la
connexion, vous pouvez utiliser les fonctions d'assistance immediateTransaction,
deferredTransaction, ou exclusiveTransaction sur
une instance Transactor dans un bloc useWriterConnection :
roomDatabase.useWriterConnection { transactor -> transactor.immediateTransaction { // Perform transactional database operations using transactor } }
Vous pouvez également utiliser les withReadTransaction ou withWriteTransaction
fonctions d'extension d'assistance sur votre instance RoomDatabase si vous n'avez besoin d'exécuter que des opérations DAO de haut niveau dans une
transaction :
// Perform transactional read operations (DEFERRED transaction) val userCount = roomDatabase.withReadTransaction { userDao.countUsers() } // Perform transactional write operations (IMMEDIATE transaction) roomDatabase.withWriteTransaction { userDao.insert(newUser) userDao.update(existingUser) }