Daten in einer lokalen Datenbank mit Room 2.x speichern

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.

Abbildung 1. Diagramm der Architektur der Room-Bibliothek.

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 tableName Attribut in @Entity und die @ColumnInfo(name = "...") Annotation.
  • Primärschlüssel: Verwenden Sie @PrimaryKey, um einen Primärschlüssel zu definieren. Verwenden Sie für zusammengesetzte Schlüssel das primaryKeys Attribut 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 Long zurückgeben, das die ID der eingefügten Zeile darstellt, oder eine List<Long>, die die IDs aller eingefügten Zeilen enthält.
  • Aktualisieren oder löschen: Die Methode zum Aktualisieren oder Löschen kann ein Int zurü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 @AutoMigration grundlegende Schemaänderungen automatisch migrieren. Dazu müssen Sie exportSchema in Ihrer Datenbankkonfiguration auf true setzen: @Database(version = 2, autoMigrations = [AutoMigration(from = 1, to = 2)]).
  • Destruktiver Fallback: Wenn Datenverlust akzeptabel ist, wenn Migrationspfade fehlen, rufen Sie beim Erstellen der Datenbank .fallbackToDestructiveMigration auf.

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:

  1. Aktualisieren Sie die Abhängigkeiten , um Room einzuschließen.
  2. Annotieren Sie Modellklassen mit @Entity, @PrimaryKey und @ColumnInfo.
  3. Erstellen Sie DAOs , um Ihre Hilfsmethoden für Abfragen zu ersetzen.
  4. Erstellen Sie eine RoomDatabase-Klasse , die auf Ihre Entitäten und DAOs verweist. Erhöhen Sie die Versionsnummer.
  5. 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) {} }
  6. Aktualisieren Sie die Instanziierung , um Room.databaseBuilder mit dem Migrationspfad zu verwenden.