Jeśli Twoja aplikacja przetwarza duże ilości uporządkowanych danych, możesz korzystać z lokalnego przechowywania tych danych. Najczęstszym zastosowaniem jest przechowywanie w pamięci podręcznej odpowiednich fragmentów danych, aby użytkownik mógł przeglądać treści w trybie offline, gdy urządzenie nie ma dostępu do sieci.
Biblioteka trwałości danych Room zapewnia warstwę abstrakcji nad SQLite, aby umożliwić płynny dostęp do bazy danych przy jednoczesnym wykorzystaniu pełnej mocy SQLite.
Konfigurowanie Room 2.x
Aby używać Room 2.x w aplikacji, dodaj te zależności do pliku build.gradle aplikacji:
dependencies {
val room_version = "2.6.1"
implementation("androidx.room:room-runtime:$room_version")
annotationProcessor("androidx.room:room-compiler:$room_version")
// To use Kotlin Symbol Processing (KSP)
// ksp("androidx.room:room-compiler:$room_version")
// optional - Kotlin Extensions and Coroutines support for Room
implementation("androidx.room:room-ktx:$room_version")
// optional - RxJava2 support for Room
implementation("androidx.room:room-rxjava2:$room_version")
// optional - Guava support for Room, including Optional and ListenableFuture
implementation("androidx.room:room-guava:$room_version")
// optional - Test helpers
testImplementation("androidx.room:room-testing:$room_version")
}
Główne komponenty
Room ma 3 główne komponenty:
- Klasa bazy danych zawiera bazę danych i służy jako główny punkt dostępu do połączenia z utrwalonymi danymi aplikacji.
- Encje danych reprezentują tabele w bazie danych aplikacji.
- Obiekty umożliwiające dostęp do danych (DAO) udostępniają metody umożliwiające aplikacji wykonywanie zapytań, aktualizowanie, wstawianie i usuwanie danych w bazie danych.
Rysunek 1 przedstawia relacje między poszczególnymi komponentami Room.
Przykładowa implementacja
// Entity
@Entity
data class User(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
// DAO
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): List<User>
@Insert
fun insertAll(vararg users: User)
@Delete
fun delete(user: User)
}
// Database
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
// Usage
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "database-name"
).build()
val userDao = db.userDao()
val users: List<User> = userDao.getAll()
Definiowanie danych za pomocą encji
Każda encja Room reprezentuje tabelę w bazie danych. Każdą encję definiujesz jako
klasę oznaczoną adnotacją @Entity.
@Entity(tableName = "users")
data class User (
@PrimaryKey val id: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?,
@Ignore val picture: Bitmap? = null
)
- Niestandardowe nazwy tabel i kolumn: domyślnie Room używa nazwy klasy jako
nazwy tabeli, a nazw właściwości jako nazw kolumn. Aby je dostosować, użyj
właściwości
tableNamew@Entityi adnotacji@ColumnInfo(name = "..."). - Klucz podstawowy: aby zdefiniować klucz podstawowy, użyj
@PrimaryKey. W przypadku kluczy złożonych użyj właściwościprimaryKeysw@Entity:@Entity(primaryKeys = ["firstName", "lastName"]). - Ignorowanie pól: aby zapobiec utrwalaniu pól, użyj
@Ignore.
Konwertery typów
Czasami trzeba przechowywać typy niestandardowe, np. Date, w jednej kolumnie.
Udostępnij metody @TypeConverter, aby konwertować typy niestandardowe na typy
, które Room może utrwalać, i z powrotem.
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }
@TypeConverter
fun dateToTimestamp(date: Date?): Long? = date?.time
}
// Register in your Database class
@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { ... }
Dostęp do danych za pomocą DAO
DAO definiują metody interakcji z bazą danych. Oznacz interfejs lub klasę abstrakcyjną adnotacją @Dao.
Metody pomocnicze
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUsers(vararg users: User)
@Update
fun updateUsers(vararg users: User)
@Delete
fun deleteUsers(vararg users: User)
}
- Wstawianie: metoda wstawiania może zwrócić wartość
Longreprezentującą identyfikator wstawionego wiersza lubList<Long>zawierającą identyfikatory wszystkich wstawionych wierszy. - Aktualizowanie lub usuwanie: metoda aktualizowania lub usuwania może zwrócić wartość
Intreprezentującą liczbę wierszy, których dotyczy zmiana.
Metody zapytań
Oznacz metody adnotacją @Query, aby pisać instrukcje SQL. Room sprawdza poprawność zapytań w czasie kompilacji.
@Dao
interface UserDao {
// Simple query
@Query("SELECT * FROM user")
fun loadAllUsers(): Array<User>
// Return a subset of columns using a POJO or tuple
@Query("SELECT first_name, last_name FROM user")
fun loadFullName(): List<NameTuple>
// Pass parameters
@Query("SELECT * FROM user WHERE age > :minAge")
fun loadAllUsersOlderThan(minAge: Int): Array<User>
// Collection of parameters
@Query("SELECT * FROM user WHERE region IN (:regions)")
fun loadUsersFromRegions(regions: List<String>): List<User>
// Join tables
@Query("SELECT * FROM book INNER JOIN user ON user.id = book.user_id WHERE user.name = :userName")
fun findBooksBorrowedByName(userName: String): List<Book>
}
Typy zwracane multimap
W Room 2.4 i nowszych metodach zapytań można bezpośrednio zwracać multimapę za pomocą typu Map:
@Query("SELECT * FROM user JOIN book ON user.id = book.user_id")
fun loadUserAndBookNames(): Map<User, List<Book>>
Asynchroniczne zapytania DAO
Aby uniknąć zawieszania się interfejsu, zapytania do bazy danych nie mogą być wykonywane w wątku głównym. Użyj jednej z tych integracji, aby zapytania były asynchroniczne:
Współprogramy i Flow w Kotlinie
Wymaga zależności room-ktx.
@Dao
interface UserDao {
// One-shot async query
@Insert
suspend fun insertUsers(vararg users: User)
// Observable query using Flow
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): Flow<User>
}
Java z RxJava
Wymaga room-rxjava2 lub room-rxjava3.
@Dao
interface UserDao {
@Insert
fun insertUsers(users: List<User>): Completable
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): Flowable<User>
}
Java z LiveData i Guava
Wymaga room-guava dla ListenableFuture.
@Dao
interface UserDao {
// LiveData for observable queries
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): LiveData<User>
// Guava ListenableFuture for one-shot queries
@Insert
fun insertUsers(users: List<User>): ListenableFuture<Integer>
}
Definiowanie relacji w Room 2.x
Aby zapobiec leniwemu ładowaniu w wątku UI, nie można używać bezpośrednich odwołań do obiektów między encjami. Zamiast tego zdefiniuj relacje za pomocą pośrednich
klas danych z @Relation.
Jeden do jednego
Każdy użytkownik ma tylko 1 bibliotekę.
@Entity
data class User(@PrimaryKey val userId: Long, val name: String)
@Entity
data class Library(@PrimaryKey val libraryId: Long, val userOwnerId: Long)
// Intermediate class
data class UserAndLibrary(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userOwnerId"
)
val library: Library
)
// DAO Query
@Transaction
@Query("SELECT * FROM User")
fun getUsersAndLibraries(): List<UserAndLibrary>
Jeden do wielu
Każdy użytkownik może mieć wiele playlist.
@Entity
data class Playlist(@PrimaryKey val playlistId: Long, val userCreatorId: Long)
data class UserWithPlaylists(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userCreatorId"
)
val playlists: List<Playlist>
)
Wiele do wielu
Playlisty mogą zawierać wiele utworów, a utwory mogą znajdować się na wielu playlistach. Wymaga tabeli łączącej.
@Entity
data class Song(@PrimaryKey val songId: Long, val songName: String)
@Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(val playlistId: Long, val songId: Long)
data class PlaylistWithSongs(
@Embedded val playlist: Playlist,
@Relation(
parentColumn = "playlistId",
entityColumn = "songId",
associateBy = Junction(PlaylistSongCrossRef::class)
)
val songs: List<Song>
)
Relacje zagnieżdżone
Wyszukaj użytkowników, ich playlisty i wszystkie utwory na tych playlistach.
data class UserWithPlaylistsAndSongs(
@Embedded val user: User,
@Relation(
entity = Playlist::class,
parentColumn = "userId",
entityColumn = "userCreatorId"
)
val playlists: List<PlaylistWithSongs> // Nesting PlaylistWithSongs
)
Zarządzanie bazami danych
W tej sekcji omówimy różne aspekty zarządzania bazą danych Room, w tym widoki bazy danych, wstępne wypełnianie danych i migracje bazy danych.
Widoki bazy danych
Zamknij złożone zapytanie w klasie oznaczonej adnotacją @DatabaseView.
@DatabaseView("SELECT user.id, user.name, department.name AS departmentName FROM user INNER JOIN department ON user.departmentId = department.id")
data class UserDetail(val id: Long, val name: String, val departmentName: String)
// Register in Database class
@Database(entities = [User::class], views = [UserDetail::class], version = 1)
abstract class AppDatabase : RoomDatabase() { ... }
Wstępne wypełnianie bazy danych
Wypełnij bazę danych podczas inicjowania z pliku zasobu lub systemu plików.
Room.databaseBuilder(appContext, AppDatabase::class.java, "Sample.db")
.createFromAsset("database/myapp.db")
.build()
Migracje
Gdy zmienisz schemat, zwiększ wersję bazy danych i zdefiniuj a
Migration obiekt.
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE User ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
}
}
Room.databaseBuilder(applicationContext, AppDatabase::class.java, "database-name")
.addMigrations(MIGRATION_1_2)
.build()
- Automatyczne migracje: jeśli używasz Room 2.4.0 lub nowszej wersji, możesz użyć
@AutoMigration, aby automatycznie migrować podstawowe zmiany schematu. Wymaga to ustawieniaexportSchemanatruew konfiguracji bazy danych:@Database(version = 2, autoMigrations = [AutoMigration(from = 1, to = 2)]). - Destrukcyjne wycofywanie: jeśli utrata danych jest dopuszczalna w przypadku braku ścieżek migracji, podczas tworzenia bazy danych wywołaj
.fallbackToDestructiveMigration.
Testowanie migracji
Aby sprawdzić migracje, użyj MigrationTestHelper z artefaktu room-testing. Aby to zrobić, upewnij się, że eksportujesz schematy w konfiguracji build.gradle.
@RunWith(AndroidJUnit4::class)
class MigrationTest {
@get:Rule
val helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory()
)
@Test
fun migrate1To2() {
var db = helper.createDatabase("test-db", 1).apply {
execSQL("INSERT INTO User VALUES (1, 'John')")
close()
}
db = helper.runMigrationsAndValidate("test-db", 2, true, MIGRATION_1_2)
// Verify data was migrated correctly
}
}
Migracja z SQLite do Room
Aby przeprowadzić migrację aplikacji z SQLite do Room, wykonaj te czynności:
- Zaktualizuj zależności , aby uwzględnić Room.
- Oznacz klasy modeli adnotacjami
@Entity,@PrimaryKeyi@ColumnInfo. - Utwórz DAO , aby zastąpić metody zapytań pomocniczych.
- Utwórz klasę RoomDatabase odwołującą się do encji i DAO. Zwiększ numer wersji.
- Zdefiniuj pustą ścieżkę migracji , ponieważ schemat się nie zmienia, tylko
framework:
kotlin val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) {} } - Zaktualizuj instancję , aby używać
Room.databaseBuilderze ścieżką migracji.