نقل البيانات من 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.

التغييرات الرئيسية في الإصدار 3.0 من Room

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

  • الحزمة الجديدة والعناصر: تتوفّر جميع الفئات في androidx.room3. تستخدم التجهيزات البادئة room3، مثل androidx.room3:room3-runtime.
  • لغة Kotlin وKSP فقط: لا يتيح الإصدار 3.0 من Room إنشاء رموز Java البرمجية. استخدِم KSP بدلاً من KAPT أو أدوات معالجة التعليقات التوضيحية في Java. لا يزال Room 3.0 يتيح استخدام مصادر Java كمدخلات.
  • أولاً، الروتينات المشتركة: يجب أن تكون دوال DAO من النوع suspend، باستثناء الأنواع القابلة للمراقبة. يحلّ CoroutineContext محلّ المنفّذين.
  • No 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. يتوافق الإصدار 2.8 من Room مع Kotlin Multiplatform أو KMP، ويتضمّن العديد من واجهات برمجة التطبيقات الخاصة ببرامج التشغيل التي يستخدمها الإصدار 3.0 من Room.

تثبيت الإصدار 2.8 من Room والإصدارات الأحدث

عدِّل إعدادات التصميم لاستخدام الإصدار الحالي 2.x من Room:

[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). يمكنك إجراء هذا الانتقال أثناء استخدام الإصدار 2.x من Room.

  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)
    }
    

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

يتطلّب الإصدار 3.0 من Room استخدام إجراءات فرعية للعمليات غير المتزامنة.

  • تعديل عناصر الوصول إلى البيانات (DAO): ما لم تعرض هذه العناصر نوعًا تفاعليًا قابلاً للمراقبة، مثل Flow أو أنواع RxJava، يجب أن تكون جميع دوال عناصر الوصول إلى البيانات دوال suspend.
// 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) {
        // ...
    }
}
  • تحويل دوال @RawQuery DAO: بالنسبة إلى الدوال التي تمّت إضافة تعليقات توضيحية إليها باستخدام @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)
    }
)
  • تحويل واجهات برمجة التطبيقات الخاصة بالمعاملات: استبدِل الرمزين withTransaction وrunInTransaction المخصّصَين لنظام التشغيل Android فقط بالرمزين withWriteTransaction أو withReadTransaction:
// Before (withTransaction)
db.withTransaction {
    // perform database operations
}

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

db.withWriteTransaction {
    // perform database operations
}

إذا كنت بحاجة إلى الوصول المباشر إلى مستوى منخفض من اتصال المعاملة، يمكنك أيضًا استخدام useWriterConnection مع immediateTransaction.

  • تجنُّب الاستخدام المباشر لـ 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 إلى واجهات برمجة التطبيقات الخاصة ببرنامج التشغيل، اضبط برنامج تشغيل، مثل BundledSQLiteDriver أو AndroidSQLiteDriver، من خلال استدعاء setDriver في أداة إنشاء RoomDatabase:
import androidx.sqlite.driver.bundled.BundledSQLiteDriver

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

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

يقدّم الإصدار 2.8 من Room واجهة برمجة التطبيقات 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()

تعديل عمليات معاودة الاتصال لتعليق الوظائف

في 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>
}
  • تقسيم المحتوى إلى صفحات (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

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

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

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