Migracja z Room 2.x do Room 3.0

Room 3.0 to aktualizacja do wersji głównej, która sprawia, że biblioteka jest w pierwszej kolejności przeznaczona do używania z Kotlinem. 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 jako priorytet: 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 bieżącą wersję 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 korzystasz z 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 przypadku zależności 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
}

RoomRawQuery możesz utworzyć 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 dostępne tylko na Androidzie withTransaction i runInTransaction przez useWriterConnection i immediateTransaction:
// Before (withTransaction)
db.withTransaction {
    // perform database operations
}

// After (useWriterConnection - Room 2.8)
import androidx.room.useWriterConnection
import androidx.room.immediateTransaction

db.useWriterConnection { connection ->
    connection.immediateTransaction {
        // perform database operations
    }
}
  • 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ę z implementacji starszych wersji InvalidationTracker.Observer, gdy nadal korzystasz z 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 konwerterów typów

Room 3.0 zmienia nazwy interfejsów API konwerterów 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 zawieszających

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 w kroku 1 nie przeprowadzono jeszcze migracji do przepływów współprogramów, musisz przenieść wszystkie Observer zastosowania do createFlow:

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