Gdy używasz biblioteki trwałości danych Room do przechowywania danych aplikacji, wchodzisz w interakcję z zapisanymi danymi, definiując obiekty dostępu do danych, czyli DAO. Każdy DAO zawiera funkcje, które zapewniają abstrakcyjny dostęp do bazy danych aplikacji. W czasie kompilacji Room automatycznie generuje implementacje zdefiniowanych przez Ciebie DAO.
Używanie DAO do uzyskiwania dostępu do bazy danych aplikacji zamiast konstruktorów zapytań lub zapytań bezpośrednich pozwala zachować rozdzielenie odpowiedzialności, co jest kluczową zasadą architektury. DAO umożliwiają też symulowanie dostępu do bazy danych podczas testowania aplikacji .
Anatomia DAO
Każdy DAO możesz zdefiniować jako interfejs lub klasę abstrakcyjną. W podstawowych przypadkach użycia zwykle stosuje się interfejs. W obu przypadkach musisz zawsze
oznaczyć DAO adnotacją @Dao. DAO nie mają właściwości, ale definiują co najmniej jedną funkcję do interakcji z danymi w bazie danych aplikacji.
Poniższy kod to przykład DAO, który definiuje funkcje wstawiania, usuwania i wybierania obiektów User w bazie danych 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> }
Istnieją 2 typy funkcji DAO, które definiują interakcje z bazą danych:
- Funkcje pomocnicze, które umożliwiają wstawianie, aktualizowanie i usuwanie wierszy w bazie danych bez pisania kodu SQL.
- Funkcje zapytań, które umożliwiają pisanie własnych zapytań SQL do interakcji z bazą danych.
W kolejnych sekcjach pokazujemy, jak używać obu typów funkcji DAO do definiowania interakcji z bazą danych, których potrzebuje Twoja aplikacja.
Funkcje pomocnicze
Room udostępnia adnotacje pomocnicze do definiowania funkcji, które wykonują wstawienia, aktualizacje i usunięcia bez konieczności pisania instrukcji SQL.
Jeśli musisz zdefiniować bardziej złożone wstawienia, aktualizacje lub usunięcia albo jeśli mu sisz wysyłać zapytania o dane w bazie danych, użyj funkcji zapytania.
Wstaw
Adnotacja @Insert umożliwia definiowanie funkcji, które wstawiają swoje
parametry do odpowiedniej tabeli w bazie danych. Poniższy kod zawiera przykłady prawidłowych funkcji @Insert, które wstawiają do bazy danych co najmniej 1 obiekt User:
@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>) }
Każdy parametr funkcji @Insert musi być instancją klasy encji danych Room oznaczonej adnotacją @Entity lub kolekcją instancji klasy encji danych. Gdy wywoływana jest funkcja @Insert, Room wstawia każdą przekazaną instancję encji do odpowiedniej tabeli bazy danych.
Jeśli funkcja @Insert otrzyma pojedynczy parametr, może zwrócić wartość Long, która jest nowym rowId wstawionego elementu. Jeśli parametr jest tablicą lub kolekcją, funkcja powinna zwrócić tablicę lub kolekcję wartości Long, z których każda jest rowId jednego z wstawionych elementów.
Więcej informacji o zwracaniu rowId wartości znajdziesz w dokumentacji referencyjnej
adnotacji @Insert oraz w dokumentacji SQLite
dotyczącej tabel rowid.
Aktualizuj
Adnotacja @Update umożliwia definiowanie funkcji, które aktualizują określone
wiersze w tabeli bazy danych. Podobnie jak funkcje @Insert, funkcje @Update przyjmują jako parametry instancje encji danych. Poniższy kod zawiera przykład funkcji @Update, która próbuje zaktualizować co najmniej 1 obiekt User w bazie danych:
@Dao interface UserDao { @Update suspend fun updateUsers(vararg users: User) }
Room używa klucza podstawowego do dopasowywania instancji encji w argumentach do wierszy w bazie danych. Jeśli nie ma wiersza z tym samym kluczem podstawowym, Room nie wprowadza żadnych zmian.
Funkcja @Update może opcjonalnie zwrócić wartość Int wskazującą liczbę wierszy, które zostały pomyślnie zaktualizowane.
Usuń
Adnotacja @Delete umożliwia
definiowanie funkcji, które usuwają określone wiersze z tabeli bazy danych. Podobnie jak funkcje @Insert, funkcje @Delete przyjmują jako parametry instancje encji danych. Poniższy kod zawiera przykład funkcji @Delete, która próbuje usunąć co najmniej 1 obiekt User z bazy danych:
@Dao interface UserDao { @Delete suspend fun deleteUsers(vararg users: User) }
Room używa klucza podstawowego do dopasowywania instancji encji w argumentach do wierszy w bazie danych. Jeśli nie ma wiersza z tym samym kluczem podstawowym, Room nie wprowadza żadnych zmian.
Funkcja @Delete może opcjonalnie zwrócić wartość Int wskazującą liczbę wierszy, które zostały pomyślnie usunięte.
Wstaw przez upsert
Adnotacja @Upsert umożliwia
definiowanie funkcji, które wstawiają instancje encji, gdy nie ma pasującego wiersza, lub
aktualizują je, jeśli istnieje już wiersz z tym samym kluczem podstawowym.
Podobnie jak funkcje @Insert i @Update, funkcje @Upsert przyjmują jako parametry instancje encji danych. Poniższy kod zawiera przykład funkcji @Upsert, która próbuje wstawić przez upsert co najmniej 1 obiekt User w bazie danych:
@Dao interface UserDao { @Upsert suspend fun upsertUsers(vararg users: User) }
Jeśli funkcja @Upsert otrzyma pojedynczy parametr, może zwrócić wartość Long. Jeśli spowoduje to wstawienie nowego wiersza, funkcja zwróci rowId nowo wstawionego wiersza. Jeśli spowoduje to zaktualizowanie istniejącego wiersza, funkcja zwróci wartość -1. Jeśli parametr jest tablicą lub kolekcją, funkcja powinna zwrócić tablicę lub kolekcję wartości Long.
Funkcje zapytań
Adnotacja @Query umożliwia
pisanie instrukcji SQL i udostępnianie ich jako funkcji DAO. Używaj tych funkcji zapytań, aby wysyłać zapytania o dane z bazy danych aplikacji lub gdy musisz wykonać bardziej złożone wstawienia, aktualizacje i usunięcia.
Room weryfikuje zapytania SQL w czasie kompilacji. Oznacza to, że jeśli wystąpi problem z zapytaniem, zamiast błędu w czasie działania pojawi się błąd kompilacji.
Proste zapytania
Poniższy kod definiuje funkcję, która używa zapytania SELECT do zwracania wszystkich obiektów User w bazie danych:
@Query("SELECT * FROM user") suspend fun loadAllUsers(): List<User>
W kolejnych sekcjach pokazujemy, jak zmodyfikować ten przykład w typowych przypadkach użycia.
Zwracanie podzbioru kolumn tabeli
W większości przypadków musisz zwracać tylko podzbiór kolumn z tabeli, o którą wysyłasz zapytanie. Na przykład interfejs może wyświetlać tylko imię i nazwisko użytkownika zamiast wszystkich jego danych. Aby oszczędzać zasoby i usprawnić wykonywanie zapytań, wysyłaj zapytania tylko o te właściwości, których potrzebujesz.
Room umożliwia zwracanie obiektu danych z dowolnego zapytania, o ile możesz zmapować zestaw kolumn wyników na zwracany obiekt. Możesz na przykład zdefiniować ten obiekt, aby przechowywać imię i nazwisko użytkownika:
data class NameTuple( @ColumnInfo(name = "first_name") val firstName: String, @ColumnInfo(name = "last_name") val lastName: String )
Następnie możesz zwrócić ten obiekt danych z funkcji zapytania:
@Query("SELECT first_name, last_name FROM user") suspend fun loadFullName(): List<NameTuple>
Ponieważ zapytanie zwraca wartości kolumn first_name i last_name, Room mapuje te wartości na właściwości w klasie NameTuple. Jeśli zapytanie zwraca kolumnę, która nie jest mapowana na właściwość w zwracanym obiekcie, Room wyświetla ostrzeżenie.
Chociaż w poprzednim przykładzie do pobierania podzbioru kolumn użyto niestandardowej klasy danych, Room obsługuje też zwracanie kotlin.Pair i kotlin.Triple, co jest wygodne, gdy zapytanie zwraca dokładnie 2 lub 3 kolumny. W przypadku tych typów kolumny są mapowane według kolejności, w jakiej są zdefiniowane w instrukcji zapytania, więc kolejność kolumn w instrukcji SELECT musi odpowiadać kolejności typów w Pair lub Triple.
Przekazywanie prostych parametrów do zapytania
W większości przypadków funkcje DAO muszą akceptować parametry, aby mogły wykonywać operacje filtrowania. Room obsługuje używanie parametrów funkcji jako parametrów powiązania w zapytaniach.
Na przykład poniższy kod definiuje funkcję, która zwraca wszystkich użytkowników powyżej określonego wieku:
@Query("SELECT * FROM user WHERE age > :minAge") suspend fun loadAllUsersOlderThan(minAge: Int): Array<User>
Możesz też przekazywać wiele parametrów lub odwoływać się do tego samego parametru wiele razy w zapytaniu, jak pokazano w tym kodzie:
@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>
Przekazywanie kolekcji parametrów do zapytania
Niektóre funkcje DAO mogą wymagać przekazania zmiennej liczby parametrów, która nie jest znana do czasu działania. Jeśli parametr reprezentuje kolekcję, jest ona automatycznie rozwijana w czasie działania na podstawie liczby wartości.
Na przykład poniższy kod definiuje funkcję, która zwraca informacje o wszystkich użytkownikach z podzbioru regionów:
@Query("SELECT * FROM user WHERE region IN (:regions)") suspend fun loadUsersFromRegions(regions: List<String>): List<User>
Wysyłanie zapytań do wielu tabel
Niektóre zapytania mogą wymagać dostępu do wielu tabel, aby obliczyć wynik. Aby odwoływać się do więcej niż 1 tabeli, możesz użyć klauzul JOIN w zapytaniach SQL.
Poniższy kod definiuje funkcję, która łączy 3 tabele, aby zwrócić książki, które są obecnie wypożyczone przez konkretnego użytkownika:
@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>
Możesz też zdefiniować obiekty danych, aby zwracać podzbiór kolumn z wielu połączonych tabel. Więcej informacji znajdziesz w artykule Zwracanie podzbioru kolumn tabeli. Poniższy kod definiuje DAO z funkcją, która zwraca imiona i nazwiska użytkowników oraz nazwy wypożyczonych przez nich książek:
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)
Zwracanie multimapy
W przypadku operacji łączenia możesz też wysyłać zapytania o kolumny z wielu tabel bez definiowania dodatkowej klasy danych, pisząc funkcje zapytań, które zwracają multimapę.
Rozważ przykład z sek0}Wysyłanie zapytań do wielu tabel. Zamiast zwracać listę instancji niestandardowej klasy danych, która zawiera pary instancji User i Book, możesz zwrócić mapowanie User i Book bezpośrednio z funkcji zapytania:
@Query( """ SELECT * FROM user JOIN book ON user.id = book.user_id """ ) suspend fun loadUserAndBookNames(): Map<User, List<Book>>
Gdy funkcja zapytania zwraca multimapę, możesz pisać zapytania, które używają klauzul GROUP BY, co pozwala korzystać z możliwości SQL w zakresie zaawansowanych obliczeń i filtrowania. Możesz na przykład zmodyfikować funkcję loadUserAndBookNames, aby zwracała tylko użytkowników, którzy mają wypożyczone co najmniej 3 książki:
@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>>
Jeśli nie musisz mapować całych obiektów, możesz też zwracać mapowania między
określonymi kolumnami w zapytaniu, używając adnotacji @MapColumn w
parametrach ogólnych typu zwracanego.
@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> >
Specjalne typy zwracane
Room udostępnia kilka specjalnych typów zwracanych do integracji z innymi bibliotekami interfejsów API.
Zapytania z podziałem na strony za pomocą biblioteki Paging
Room obsługuje zapytania z podziałem na strony dzięki integracji z biblioteką Paging. Aby używać zwracanych typów Paging 3, musisz zarejestrować konwertery zwracanych typów Paging w bazie danych lub DAO:
- Dodaj artefakt
androidx.room3:room3-pagingdo konfiguracji kompilacji. - Oznacz deklarację
@Databaselub@Daoadnotacją@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class).
Po zarejestrowaniu DAO mogą zwracać PagingSource obiekty do użycia z
Paging 3:
@Dao @DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class) interface UserDao { @Query("SELECT * FROM users WHERE label LIKE :query") fun pagingSource(query: String): PagingSource<Int, User> }
Więcej informacji o wyborze parametrów typu dla PagingSource znajdziesz w
artykule Wybieranie typów kluczy i wartości.
Bezpośredni dostęp do połączenia z bazą danych
Jeśli logika aplikacji wymaga bezpośredniego dostępu do połączenia z bazą danych na niskim poziomie, możesz użyć interfejsów API połączeń Room. Połączenie możesz uzyskać za pomocą
useReaderConnection
w przypadku operacji tylko do odczytu lub
useWriterConnection
w przypadku operacji zapisu w instancji RoomDatabase. Do wykonywania instrukcji użyj
usePrepared:
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)) } } } }
Jeśli musisz wykonywać transakcje bazy danych na niskim poziomie bezpośrednio w
połączeniu, możesz użyć funkcji pomocniczych immediateTransaction,
deferredTransaction lub exclusiveTransaction w
instancji Transactor w bloku useWriterConnection:
roomDatabase.useWriterConnection { transactor -> transactor.immediateTransaction { // Perform transactional database operations using transactor } }
Jeśli musisz wykonywać tylko operacje DAO na wysokim poziomie w
transakcji, użyj funkcji pomocniczych withReadTransaction lub withWriteTransaction
w instancji RoomDatabase:
// 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) }