Wenn Ihre App nicht unerhebliche Mengen strukturierter Daten verarbeitet, kann es sehr nützlich sein, diese Daten lokal zu speichern. Der häufigste Anwendungsfall ist das Speichern relevanter Daten im Cache, damit Sie diese Inhalte auch offline aufrufen können, wenn das Gerät keinen Zugriff auf das Netzwerk hat.
Die Room-Persistenzbibliothek bietet eine Abstraktionsebene über SQLite, mit der Sie flüssig auf die Datenbank zugreifen und gleichzeitig die volle Leistung von SQLite nutzen können.
Room 2.x einrichten
Wenn Sie Room 2.x in Ihrer App verwenden möchten, fügen Sie der Datei build.gradle Ihrer App die folgenden Abhängigkeiten hinzu:
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")
}
Hauptkomponenten
Room hat drei Hauptkomponenten:
- Datenbankklasse , die die Datenbank enthält und als Hauptzugriffspunkt für die zugrunde liegende Verbindung zu den persistenten Daten Ihrer App dient.
- Datenentitäten , die Tabellen in der Datenbank Ihrer App darstellen.
- Datenzugriffsobjekte (Data Access Objects, DAOs) , die Methoden bereitstellen, mit denen Ihre App Daten in der Datenbank abfragen, aktualisieren, einfügen und löschen kann.
Abbildung 1 veranschaulicht die Beziehung zwischen den verschiedenen Komponenten von Room.
Implementierungsbeispiel
// 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()
Daten mithilfe von Entitäten definieren
Jede Room-Entität stellt eine Tabelle in der Datenbank dar. Sie definieren jede Entität als eine
Klasse, die mit @Entity annotiert ist.
@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
)
- Benutzerdefinierte Tabellen- und Spaltennamen: Standardmäßig verwendet Room den Klassennamen als
Tabellennamen und die Attributnamen als Spaltennamen. Wenn Sie sie anpassen möchten, verwenden Sie
das
tableNameAttribut in@Entityund die@ColumnInfo(name = "...")Annotation. - Primärschlüssel: Verwenden Sie
@PrimaryKey, um einen Primärschlüssel zu definieren. Verwenden Sie für zusammengesetzte Schlüssel dasprimaryKeysAttribut von@Entity:@Entity(primaryKeys = ["firstName", "lastName"]). - Felder ignorieren: Wenn Sie verhindern möchten, dass Felder persistent gespeichert werden, verwenden Sie
@Ignore.
Typkonverter
Manchmal müssen Sie benutzerdefinierte Typen wie Date in einer einzelnen Spalte speichern.
Geben Sie @TypeConverter-Methoden an, um benutzerdefinierte Typen in Typen zu konvertieren, die von Room persistent gespeichert werden können, und umgekehrt.
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() { ... }
Mit DAOs auf Daten zugreifen
DAOs definieren Methoden für die Datenbankinteraktion. Annotieren Sie die Schnittstelle oder abstrakte
Klasse mit @Dao.
Hilfsmethoden
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUsers(vararg users: User)
@Update
fun updateUsers(vararg users: User)
@Delete
fun deleteUsers(vararg users: User)
}
- Einfügen: Die Methode zum Einfügen kann ein
Longzurückgeben, das die ID der eingefügten Zeile darstellt, oder eineList<Long>, die die IDs aller eingefügten Zeilen enthält. - Aktualisieren oder löschen: Die Methode zum Aktualisieren oder Löschen kann ein
Intzurückgeben, das die Anzahl der betroffenen Zeilen darstellt.
Abfragemethoden
Annotieren Sie Methoden mit @Query, um SQL-Anweisungen zu schreiben. Room validiert Abfragen zur Kompilierzeit.
@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>
}
Multimap-Rückgabetypen
In Room 2.4 und höher können Abfragemethoden mit dem Typ Map direkt eine Multimap zurückgeben:
@Query("SELECT * FROM user JOIN book ON user.id = book.user_id")
fun loadUserAndBookNames(): Map<User, List<Book>>
Asynchrone DAO-Abfragen
Um UI-Einfrierungen zu vermeiden, können Datenbankabfragen nicht im Hauptthread ausgeführt werden. Machen Sie Ihre Abfragen mit einer der folgenden Integrationen asynchron:
Kotlin-Koroutinen und -Datenfluss
Erfordert die Abhängigkeit 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 mit RxJava
Erfordert room-rxjava2 oder 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 mit LiveData und Guava
Erfordert room-guava für 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>
}
Beziehungen in Room 2.x definieren
Um Lazy Loading im UI-Thread zu verhindern, können Sie keine direkten Objektverweise zwischen Entitäten verwenden. Definieren Sie Beziehungen stattdessen mit Zwischendatenklassen mit
@Relation.
1:1
Jeder Nutzer hat nur eine Bibliothek.
@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>
1:n
Jeder Nutzer kann viele Playlists haben.
@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>
)
n:m
Playlists können viele Songs enthalten und Songs können in vielen Playlists enthalten sein. Erfordert eine Verbindungstabelle.
@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>
)
Verschachtelte Beziehungen
Fragen Sie Nutzer, ihre Playlists und alle Songs in diesen Playlists ab.
data class UserWithPlaylistsAndSongs(
@Embedded val user: User,
@Relation(
entity = Playlist::class,
parentColumn = "userId",
entityColumn = "userCreatorId"
)
val playlists: List<PlaylistWithSongs> // Nesting PlaylistWithSongs
)
Datenbankverwaltung
In diesem Abschnitt werden verschiedene Aspekte der Verwaltung Ihrer Room-Datenbank behandelt, darunter Datenbankansichten, das Vorabfüllen von Daten und Datenbankmigrationen.
Datenbankansichten
Kapseln Sie eine komplexe Abfrage in einer Klasse, die mit @DatabaseView annotiert ist.
@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() { ... }
Datenbank vorabfüllen
Füllen Sie die Datenbank bei der Initialisierung aus einer Asset-Datei oder dem Dateisystem.
Room.databaseBuilder(appContext, AppDatabase::class.java, "Sample.db")
.createFromAsset("database/myapp.db")
.build()
Migrationen
Wenn Sie das Schema ändern, erhöhen Sie die Datenbankversion und definieren Sie ein
Migration-Objekt.
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()
- Automatisierte Migrationen: Wenn Sie Room 2.4.0 oder höher verwenden, können Sie mit
@AutoMigrationgrundlegende Schemaänderungen automatisch migrieren. Dazu müssen SieexportSchemain Ihrer Datenbankkonfiguration auftruesetzen:@Database(version = 2, autoMigrations = [AutoMigration(from = 1, to = 2)]). - Destruktiver Fallback: Wenn Datenverlust akzeptabel ist, wenn Migrationspfade
fehlen, rufen Sie beim Erstellen der
Datenbank
.fallbackToDestructiveMigrationauf.
Migrationen testen
Verwenden Sie MigrationTestHelper aus dem Artefakt room-testing, um Migrationen zu überprüfen. Dazu müssen Sie Schemas in Ihrer build.gradle-Konfiguration exportieren.
@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
}
}
Von SQLite zu Room migrieren
So migrieren Sie Ihre App von SQLite zu Room:
- Aktualisieren Sie die Abhängigkeiten , um Room einzuschließen.
- Annotieren Sie Modellklassen mit
@Entity,@PrimaryKeyund@ColumnInfo. - Erstellen Sie DAOs , um Ihre Hilfsmethoden für Abfragen zu ersetzen.
- Erstellen Sie eine RoomDatabase-Klasse , die auf Ihre Entitäten und DAOs verweist. Erhöhen Sie die Versionsnummer.
- Definieren Sie einen leeren Migrationspfad , da sich das Schema nicht ändert, sondern nur
das Framework:
kotlin val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) {} } - Aktualisieren Sie die Instanziierung , um
Room.databaseBuildermit dem Migrationspfad zu verwenden.