Migracja z Room 2.x do Room 3.0

Room 3.0 to aktualizacja do wersji głównej, która przenosi bibliotekę do Kotlin-first. Obsługuje Kotlin Multiplatform (KMP), wymaga Kotlin Symbol Processing (KSP) i wymusza używanie współprogramów w przypadku operacji asynchronicznych.

Aby zapobiec problemom ze zgodnością z istniejącymi aplikacjami Room 2.x i zależnościami przechodnimi, Room 3.0 znajduje się w nowym pakiecie: androidx.room3.

Z tego przewodnika dowiesz się, jak przeprowadzić migrację z implementacji Room 2.x do Room 3.0.

Najważniejsze zmiany w Room 3.0

Zanim rozpoczniesz migrację, zapoznaj się z najważniejszymi różnicami:

  • Nowy pakiet i artefakty: wszystkie klasy znajdują się w androidx.room3. Artefakty używają prefiksu room3, np. androidx.room3:room3-runtime.
  • Tylko Kotlin i KSP: Room 3.0 nie obsługuje generowania kodu Java. Zamiast KAPT lub procesorów adnotacji Java używaj KSP. Room 3.0 nadal obsługuje źródła Java jako dane wejściowe.
  • Współprogramy: funkcje DAO muszą być funkcjami suspend, z wyjątkiem typów obserwowalnych. CoroutineContext zastępuje wykonawców.
  • Brak SupportSQLite: SQLiteDriver interfejsy API obsługują Room. Room usuwa SupportSQLiteDatabase z podstawowych interfejsów API.
  • Zmiany w interfejsie API: migracje i wywołania zwrotne bazy danych używają SQLiteConnection zamiast SupportSQLiteDatabase.
  • Konwertery typów reaktywnych: typy zwracane RxJava, LiveData, Guava i Paging wymagają zarejestrowania @DaoReturnTypeConverters.

Zalecamy przeprowadzenie migracji w 2 odrębnych etapach: najpierw przygotuj i zmodernizuj bazę kodu w Room 2.x, a następnie przejdź na Room 3.0.


Przygotowanie i modernizacja w Room 2.x

Przed migracją do Room 3.0 możesz wykonać większość prac związanych z modernizacją, aktualizując do bieżącej wersji Room 2.x, np. Room 2.8. Room 2.8 obsługuje Kotlin Multiplatform (KMP) i zawiera wiele interfejsów API sterowników, których używa Room 3.0.

Aktualizacja do Room 2.8 lub nowszej wersji

Zaktualizuj konfigurację kompilacji, aby używać bieżącej wersji Room 2.x:

[versions]
room2 = "2.8.4" # Use the current Room 2.8 version

[libraries]
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room2" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room2" }

Migracja z KAPT do KSP

Room 3.0 nie obsługuje procesorów adnotacji Java ani KAPT. Musisz używać Kotlin Symbol Processing (KSP). Możesz dokonać tego przejścia, gdy nadal używasz Room 2.x.

  1. W pliku build.gradle.kts modułu zastosuj wtyczkę KSP:

    plugins {
        id("com.google.devtools.ksp") version "<ksp_version>"
    }
    

    Upewnij się, że wersja KSP jest zgodna z wersją Kotlin.

  2. Zastąp kapt lub annotationProcessor przez ksp w zależności od kompilatora Room:

    dependencies {
        implementation(libs.androidx.room.runtime)
        ksp(libs.androidx.room.compiler)
    }
    

Wprowadzenie współprogramów

Room 3.0 wymaga współprogramów do operacji asynchronicznych.

  • Zaktualizuj DAO: wszystkie funkcje DAO muszą być funkcjami suspend, chyba że zwracają obserwowalny typ reaktywny, np. Flow lub typy RxJava.
// Before (Blocking)
@Dao
interface UserDao {
    @Query("SELECT * FROM User")
    fun getAll(): List<User>
}

// After (Suspend)
@Dao
interface UserDao {
    @Query("SELECT * FROM User")
    suspend fun getAll(): List<User>
}
  • Jeśli skonfigurowano RoomDatabase za pomocą niestandardowego Executor do wykonywania operacji na bazie danych, przeprowadź migrację do CoroutineContext za pomocą setQueryCoroutineContext w konstruktorze:
Room.databaseBuilder<AppDatabase>(context, "db")
    .setQueryCoroutineContext(Dispatchers.IO)
    .build()

Wprowadzenie interfejsów API sterowników i unikanie Support SQLite

Room 3.0 jest w pełni obsługiwany przez SQLiteDriver i nie obsługuje już SupportSQLiteDatabase w swoich podstawowych interfejsach API.

Jeśli nie wywołasz setDriver, aby ustawić SQLiteDriver w konstruktorze bazy danych, Room 2.8 będzie działać w trybie zgodności, w którym działają zarówno interfejsy API Support SQLite, jak i sterowników. Ten tryb zgodności umożliwia stopniowe przekształcanie bazy kodu przed włączeniem sterownika.

  • Konwertowanie migracji: przeprowadź migrację podklas Migration i AutoMigrationSpec , aby używać SQLiteConnection zamiast SupportSQLiteDatabase.
// Before (SupportSQLiteDatabase)
val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("ALTER TABLE User ADD COLUMN age INTEGER DEFAULT 0 NOT NULL")
    }
}

// After (SQLiteConnection - Room 2.8)
import androidx.sqlite.SQLiteConnection
import androidx.sqlite.execSQL

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(connection: SQLiteConnection) {
        connection.execSQL(
            "ALTER TABLE User ADD COLUMN age INTEGER DEFAULT 0 NOT NULL"
        )
    }
}
  • Konwertowanie wywołań zwrotnych bazy danych: zaktualizuj RoomDatabase.Callback implementacje, aby używać SQLiteConnection:
// Before (SupportSQLiteDatabase)
val callback = object : RoomDatabase.Callback() {
    override fun onCreate(db: SupportSQLiteDatabase) {
        // ...
    }
}

// After (SQLiteConnection - Room 2.8)
val callback = object : RoomDatabase.Callback() {
    override fun onCreate(connection: SQLiteConnection) {
        // ...
    }
}
  • Konwertowanie funkcji DAO @RawQuery: w przypadku funkcji oznaczonych adnotacją @RawQuery używaj RoomRawQuery zamiast SupportSQLiteQuery:
// Before (SupportSQLiteQuery)
@Dao
interface UserDao {
    @RawQuery
    fun getUser(query: SupportSQLiteQuery): User
}

// After (RoomRawQuery)
@Dao
interface UserDao {
    @RawQuery
    suspend fun getUser(query: RoomRawQuery): User
}

Możesz utworzyć RoomRawQuery w czasie działania:

val query = RoomRawQuery(
    sql = "SELECT * FROM User WHERE id = ?",
    onBindStatement = { statement ->
        statement.bindInt(1, userId)
    }
)
  • Konwertowanie interfejsów API transakcji: zastąp bloki tylko na Androidzie withTransaction i runInTransaction przez withWriteTransaction lub withReadTransaction:
// Before (withTransaction)
db.withTransaction {
    // perform database operations
}

// After (withWriteTransaction - Room 2.8)
import androidx.room.withWriteTransaction

db.withWriteTransaction {
    // perform database operations
}

Jeśli potrzebujesz bezpośredniego dostępu do połączenia transakcji na niskim poziomie, możesz też użyć useWriterConnection z immediateTransaction.

  • Unikanie bezpośredniego użycia SupportSQLiteDatabase: jeśli masz obszerny starszy kod, który nadal wymaga SupportSQLiteDatabase i nie możesz go jeszcze przenieć , użyj artefaktu zgodności androidx.room:room-sqlite-wrapper:
dependencies {
    implementation("androidx.room:room-sqlite-wrapper:$roomVersion")
}

Następnie użyj getSupportWrapper, aby uzyskać SupportSQLiteDatabase z instancji bazy danych Room:

import androidx.room.support.getSupportWrapper

val legacyDb: SupportSQLiteDatabase = roomDatabase.getSupportWrapper()
  • Ustawianie sterownika SQLite: po przeniesieniu wszystkich zastosowań interfejsu API Room do interfejsów API sterowników skonfiguruj sterownik, np. BundledSQLiteDriver lub AndroidSQLiteDriver, wywołując setDriver w konstruktorze RoomDatabase:
import androidx.sqlite.driver.bundled.BundledSQLiteDriver

val db = Room.databaseBuilder<AppDatabase>(context, "db")
    .setDriver(BundledSQLiteDriver())
    .build()

Wprowadzenie śledzenia unieważnień opartego na przepływie

Room 2.8 wprowadza interfejs API InvalidationTracker.createFlow. Użyj tego interfejsu API, aby przeprowadzić migrację ze starszych implementacji InvalidationTracker.Observer, gdy nadal używasz Room 2.x. Przygotowuje to bazę kodu do Room 3.0, która całkowicie usuwa Observer.

// Before (InvalidationTracker.Observer)
val observer = object : InvalidationTracker.Observer("User") {
    override fun onInvalidated(tables: Set<String>) {
        // reload user data
    }
}
db.invalidationTracker.addObserver(observer)

// After (createFlow - Room 2.8)
val userFlow = db.invalidationTracker.createFlow("User").map { _ ->
    userDao.getAllUsers()
}

Migracja do Room 3.0

Po zmodernizowaniu aplikacji w Room 2.x przejście na Room 3.0 wymaga zaktualizowania zależności, importów pakietów i wywołań zwrotnych bazy danych.

Aktualizowanie zależności i importów pakietów

  • W konfiguracji kompilacji zastąp androidx.room zależności przez androidx.room3:
[versions]
room3 = "3.0.0" # Use the current Room 3.0 version

[libraries]
androidx-room3-runtime = { module = "androidx.room3:room3-runtime", version.ref = "room3" }
androidx-room3-compiler = { module = "androidx.room3:room3-compiler", version.ref = "room3" }
  • Zaktualizuj blok zależności:
dependencies {
    implementation(libs.androidx.room3.runtime)
    ksp(libs.androidx.room3.compiler)
}
  • Zaktualizuj importy pakietów. Zastąp import androidx.room.* przez import androidx.room3.*.

Aktualizowanie interfejsów API konwertera typów

Room 3.0 zmienia nazwy interfejsów API konwertera typów, aby wyjaśnić ich użycie do konwertowania wartości kolumn i uniknąć pomyłek z konwerterami typów zwracanych przez DAO.

Zaktualizuj te adnotacje i funkcje w bazie kodu:

  • Zmień nazwę @TypeConverter na @ColumnTypeConverter.
  • Zmień nazwę @TypeConverters na @ColumnTypeConverters.
  • Zmień nazwę @ProvidedTypeConverter na @ProvidedColumnTypeConverter.
  • Zmień nazwę RoomDatabase.Builder.addTypeConverter na addColumnTypeConverter.

Przykład:

// Before
@ProvidedTypeConverter
class Converters {
  @TypeConverter
  fun fromTimestamp(value: Long?): Date? = ...
}

@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase()

val db = Room.databaseBuilder<AppDatabase>(...)
  .addTypeConverter(convertersInstance)
  .build()
// After
import androidx.room3.ColumnTypeConverter
import androidx.room3.ColumnTypeConverters
import androidx.room3.ProvidedColumnTypeConverter

@ProvidedColumnTypeConverter
class Converters {
  @ColumnTypeConverter
  fun fromTimestamp(value: Long?): Date? = ...
}

@Database(entities = [User::class], version = 1)
@ColumnTypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase()

val db = Room.databaseBuilder<AppDatabase>(...)
  .addColumnTypeConverter(convertersInstance)
  .build()

Aktualizowanie wywołań zwrotnych do funkcji zawieszania

W Room 3.0 wywołania zwrotne bazy danych i migracje używają SQLiteConnection i są funkcjami suspend.

  • Zaktualizuj ręczne klasy Migration:
import androidx.sqlite.SQLiteConnection
import androidx.sqlite.async.executeSQL

val MIGRATION_1_2 = object : Migration(1, 2) {
    override suspend fun migrate(connection: SQLiteConnection) {
        connection.executeSQL(
            "ALTER TABLE User ADD COLUMN age INTEGER DEFAULT 0 NOT NULL"
        )
    }
}
  • Zaktualizuj implementacje RoomDatabase.Callback:
val callback = object : RoomDatabase.Callback() {
    override suspend fun onCreate(connection: SQLiteConnection) {
        // ...
    }
}

Rejestrowanie konwerterów typów zwracanych przez DAO

W Room 3.0 reaktywne zwracane typy, takie jak RxJava, LiveData, Guava i Paging, wymagają zarejestrowania konwerterów zwracanych typów przez DAO za pomocą @DaoReturnTypeConverters.

import androidx.room3.paging.PagingSourceDaoReturnTypeConverter

@Dao
@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)
interface UserDao {
    @Query("SELECT * FROM User")
    fun getAllPaginated(): PagingSource<Int, User>
}
  • Paging (PagingSource): zarejestruj PagingSourceDaoReturnTypeConverter z artefaktu androidx.room3:room3-paging.
  • RxJava (Observable, Flowable, Single, Maybe, Completable): zarejestruj RxDaoReturnTypeConverters z artefaktu androidx.room3:room3-rxjava3.
  • Guava (ListenableFuture): zarejestruj GuavaDaoReturnTypeConverter z artefaktu androidx.room3:room3-guava.
  • LiveData (LiveData): zarejestruj LiveDataDaoReturnTypeConverter z artefaktu androidx.room3:room3-livedata.

Sprawdzanie usunięcia obserwatora InvalidationTracker

Room 3.0 całkowicie usuwa InvalidationTracker.Observer i powiązane metody rejestracji, takie jak addObserver i removeObserver.

Jeśli nie przeprowadzono jeszcze przejścia na przepływy współprogramów w etapie 1, musisz przenieść wszystkie Observer zastosowania do createFlow:

val userFlow = db.invalidationTracker.createFlow("User").map { _ ->
    userDao.getAllUsers()
}