نقل البيانات من Room 2.x إلى Room 3.0

‫Room 3.0 هو تحديث رئيسي للإصدار ينقل المكتبة لتكون متوافقة مع Kotlin أولاً. وهي تتوافق مع Kotlin Multiplatform (KMP)، وتتطلّب Kotlin Symbol Processing (KSP)، وتفرض استخدام الكوروتين للعمليات غير المتزامنة.

لمنع حدوث مشاكل في التوافق مع تطبيقات Room 2.x الحالية والتبعيات المتعدية، يتوفّر Room 3.0 في حزمة جديدة: androidx.room3.

يحدّد هذا الدليل الخطوات المطلوبة لنقل عملية تنفيذ Room 2.x الحالية إلى Room 3.0.

التغييرات الرئيسية في Room 3.0

قبل بدء عملية النقل، تعرَّف على الاختلافات الرئيسية:

  • حزمة وعناصر جديدة: تتوفّر جميع الفئات في androidx.room3. تستخدم العناصر البادئة room3، مثل androidx.room3:room3-runtime.
  • Kotlin وKSP فقط: لا يتوافق Room 3.0 مع إنشاء رموز Java البرمجية. استخدِم KSP بدلاً من KAPT أو معالِجات تعليقات Java التوضيحية. لا يزال Room 3.0 يتوافق مع مصادر Java كمدخلات.
  • الكوروتين أولاً: يجب أن تكون دوالّ DAO دوالّ suspend، باستثناء الأنواع القابلة للمراقبة. يحلّ CoroutineContext محلّ المنفّذين.
  • لا تتوفّر SupportSQLite: تستند واجهات برمجة التطبيقات SQLiteDriver إلى Room. يزيل Room السمة SupportSQLiteDatabase من واجهات برمجة التطبيقات الأساسية.
  • تغييرات واجهة برمجة التطبيقات: تستخدِم عمليات النقل وعمليات معاودة الاتصال بقاعدة البيانات SQLiteConnection بدلاً من SupportSQLiteDatabase.
  • المحوّلات للأنواع التفاعلية: تتطلّب أنواع الإرجاع RxJava وLiveData وGuava وPaging تسجيل @DaoReturnTypeConverters.

ننصحك بإجراء عملية النقل على مرحلتَين مختلفتَين: أولاً، إعداد قاعدة الرموز البرمجية وتحديثها في Room 2.x، ثم التبديل إلى Room 3.0.


الإعداد والتحديث في Room 2.x

قبل النقل إلى Room 3.0، يمكنك إجراء معظم أعمال التحديث من خلال الترقية إلى إصدار Room 2.x الحالي، مثل Room 2.8. يتوافق Room 2.8 مع Kotlin Multiplatform أو KMP، ويتضمّن العديد من واجهات برمجة التطبيقات الخاصة ببرامج التشغيل التي يستخدمها Room 3.0.

الترقية إلى Room 2.8 والإصدارات الأحدث

عدِّل إعدادات التصميم لاستخدام إصدار 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" }

النقل من KAPT إلى KSP

لا يتوافق Room 3.0 مع معالِجات تعليقات Java التوضيحية أو KAPT. عليك استخدام Kotlin Symbol Processing (KSP). يمكنك إجراء عملية النقل هذه أثناء استخدام Room 2.x.

  1. في ملف build.gradle.kts الخاص بالوحدة، طبِّق المكوّن الإضافي KSP:

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

    تأكَّد من توافق إصدار KSP مع إصدار Kotlin.

  2. استبدِل kapt أو annotationProcessor بـ ksp لتبعّية برنامج تجميع Room:

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

استخدِم الكوروتين

يتطلّب Room 3.0 استخدام الكوروتين للعمليات غير المتزامنة.

  • عدِّل دوالّ DAO: يجب أن تكون جميع دوالّ DAO دوالّ suspend، ما لم تعرض نوعًا تفاعليًا قابلاً للمراقبة، مثل Flow أو أنواع 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>
}
  • إذا سبق لك ضبط RoomDatabase باستخدام Executor مخصّص لإجراء عمليات قاعدة البيانات، يمكنك النقل إلى CoroutineContext باستخدام setQueryCoroutineContext في أداة الإنشاء:
Room.databaseBuilder<AppDatabase>(context, "db")
    .setQueryCoroutineContext(Dispatchers.IO)
    .build()

استخدِم واجهات برمجة التطبيقات الخاصة ببرامج التشغيل وتجنَّب Support SQLite

يستند Room 3.0 بالكامل إلى SQLiteDriver ولم يعُد يتوافق مع SupportSQLiteDatabase في واجهات برمجة التطبيقات الأساسية.

إذا لم تستدعِ setDriver لضبط SQLiteDriver في أداة إنشاء قاعدة البيانات، سيعمل Room 2.8 في وضع التوافق الذي تعمل فيه كل من واجهات برمجة التطبيقات Support SQLite وDriver. يتيح لك وضع التوافق هذا تحويل قاعدة الرموز البرمجية تدريجيًا قبل تفعيل برنامج التشغيل.

  • تحويل عمليات النقل: يمكنك نقل الفئتَين الفرعيتَين Migration وAutoMigrationSpec لاستخدام SQLiteConnection بدلاً من 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"
        )
    }
}
  • تحويل عمليات معاودة الاتصال بقاعدة البيانات: يمكنك تعديل عمليات تنفيذ RoomDatabase.Callback لاستخدام 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) {
        // ...
    }
}
  • تحويل دوالّ DAO التي تحمل التعليق التوضيحي @RawQuery: بالنسبة إلى الدوالّ التي تحمل التعليق التوضيحي @RawQuery، استخدِم RoomRawQuery بدلاً من 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 في وقت التشغيل:

val query = RoomRawQuery(
    sql = "SELECT * FROM User WHERE id = ?",
    onBindStatement = { statement ->
        statement.bindInt(1, userId)
    }
)
  • تحويل واجهات برمجة التطبيقات الخاصة بالمعاملات: استبدِل الحزمتَين المخصّصتَين لنظام Android withTransaction و runInTransaction بـ useWriterConnection و 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
    }
}
  • تجنَّب الاستخدام المباشر لـ SupportSQLiteDatabase: إذا كان لديك رمز قديم واسع النطاق لا يزال يتطلّب SupportSQLiteDatabase ولا يمكنك نقله بعد، استخدِم عنصر التوافق androidx.room:room-sqlite-wrapper
dependencies {
    implementation("androidx.room:room-sqlite-wrapper:$roomVersion")
}

بعد ذلك، استخدِم getSupportWrapper للحصول على SupportSQLiteDatabase من مثيل قاعدة بيانات Room:

import androidx.room.support.getSupportWrapper

val legacyDb: SupportSQLiteDatabase = roomDatabase.getSupportWrapper()
  • ضبط برنامج تشغيل SQLite: بعد نقل جميع استخدامات Room API إلى واجهات برمجة التطبيقات الخاصة ببرامج التشغيل ، يمكنك ضبط برنامج تشغيل، مثل BundledSQLiteDriver أو AndroidSQLiteDriver، من خلال استدعاء setDriver في أداة إنشاء RoomDatabase:
import androidx.sqlite.driver.bundled.BundledSQLiteDriver

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

استخدِم ميزة تتبُّع الإبطال المستندة إلى التدفق

يقدّم Room 2.8 واجهة برمجة التطبيقات InvalidationTracker.createFlow. استخدِم واجهة برمجة التطبيقات هذه لنقل عمليات تنفيذ InvalidationTracker.Observer القديمة أثناء استخدام Room 2.x. يؤدي ذلك إلى إعداد قاعدة الرموز البرمجية لـ Room 3.0 الذي يزيل 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()
}

النقل إلى Room 3.0

بعد تحديث تطبيقك على Room 2.x، يتضمّن الانتقال إلى Room 3.0 تعديل التبعيات وعمليات استيراد الحزم وعمليات معاودة الاتصال بقاعدة البيانات.

تعديل التبعيات وعمليات استيراد الحزم

  • في إعدادات التصميم، استبدِل تبعيات androidx.room بـ 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" }
  • عدِّل حزمة التبعيات:
dependencies {
    implementation(libs.androidx.room3.runtime)
    ksp(libs.androidx.room3.compiler)
}
  • عدِّل عمليات استيراد الحزم. استبدِل import androidx.room.* بـ import androidx.room3.*.

تعديل واجهات برمجة التطبيقات الخاصة بمحوّلات الأنواع

تعيد Room 3.0 تسمية واجهات برمجة التطبيقات الخاصة بمحوّلات الأنواع لتوضيح استخدامها لتحويل قيم الأعمدة وتجنُّب الخلط بينها وبين محوّلات أنواع الإرجاع في DAO.

عدِّل التعليقات التوضيحية والدوالّ التالية في قاعدة الرموز البرمجية:

  • أعِد تسمية @TypeConverter إلى @ColumnTypeConverter.
  • أعِد تسمية @TypeConverters إلى @ColumnTypeConverters.
  • أعِد تسمية @ProvidedTypeConverter إلى @ProvidedColumnTypeConverter.
  • أعِد تسمية RoomDatabase.Builder.addTypeConverter إلى addColumnTypeConverter.

مثال:

// 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()

تعديل عمليات معاودة الاتصال لاستخدام دوالّ `suspend`

في Room 3.0، تستخدِم عمليات معاودة الاتصال بقاعدة البيانات وعمليات النقل SQLiteConnection وتكون دوالّ suspend.

  • عدِّل فئات 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"
        )
    }
}
  • عدِّل عمليات تنفيذ RoomDatabase.Callback:
val callback = object : RoomDatabase.Callback() {
    override suspend fun onCreate(connection: SQLiteConnection) {
        // ...
    }
}

تسجيل محوّلات أنواع القيمة التي تم إرجاعها في DAO

في Room 3.0، تتطلّب أنواع القيمة التي تم إرجاعها التفاعلية، مثل RxJava وLiveData وGuava وPaging، تسجيل محوّلات أنواع القيمة التي تم إرجاعها في DAO باستخدام @DaoReturnTypeConverters.

import androidx.room3.paging.PagingSourceDaoReturnTypeConverter

@Dao
@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)
interface UserDao {
    @Query("SELECT * FROM User")
    fun getAllPaginated(): PagingSource<Int, User>
}
  • Paging (PagingSource): سجِّل PagingSourceDaoReturnTypeConverter من العنصر androidx.room3:room3-paging.
  • RxJava (Observable وFlowable وSingle وMaybe وCompletable): سجِّل RxDaoReturnTypeConverters من العنصر androidx.room3:room3-rxjava3.
  • Guava (ListenableFuture): سجِّل GuavaDaoReturnTypeConverter من العنصر androidx.room3:room3-guava.
  • LiveData (LiveData): سجِّل LiveDataDaoReturnTypeConverter من العنصر androidx.room3:room3-livedata.

التحقّق من إزالة `InvalidationTracker.Observer`

يزيل Room 3.0 بالكامل InvalidationTracker.Observer وطُرق التسجيل ذات الصلة، مثل addObserver وremoveObserver.

إذا لم تنتقل بعد إلى تدفقات الكوروتين في المرحلة 1، عليك نقل جميع استخدامات Observer إلى createFlow:

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