نقل بيانات الغرف إلى نموذج Kotlin المتعدد

يوضّح هذا المستند كيفية نقل عملية تنفيذ غرفة حالية إلى تطبيق يستخدم Kotlin Multiplatform (KMP).

قد تتفاوت استخدامات غرفة نقل البيانات في قاعدة رموز Android الحالية إلى وحدة KMP مشتركة بشكل كبير من حيث الصعوبة بناءً على واجهات برمجة تطبيقات الغرف المستخدمة أو إذا كان قاعدة الرموز يستخدم تنسيقات الكوروتينات. يقدم هذا القسم بعض الإرشادات والنصائح عند محاولة نقل استخدامات الغرفة إلى وحدة مشتركة.

من المهم أن تتعرف أولاً على الاختلافات والميزات المفقودة بين إصدار Android من Room وإصدار KMP جنبًا إلى جنب مع الإعداد المعني. في الواقع، تتضمن عملية نقل البيانات الناجحة إعادة تنظيم استخدامات واجهات برمجة التطبيقات SupportSQLite* واستبدالها بواجهات برمجة تطبيقات SQLite Driver (واجهة برمجة تطبيقات SQLite Driver)، إضافةً إلى تعريفات استخدام غرفة نقل البيانات (الفئة @Database ذات التعليقات التوضيحية ودوال DAO والكيانات وما إلى ذلك) إلى رمز مشترك.

يُرجى مراجعة المعلومات التالية قبل المتابعة:

تصف الأقسام التالية الخطوات المختلفة المطلوبة لعملية نقل بيانات ناجحة.

نقل البيانات من Support SQLite إلى برنامج تشغيل SQLite

تعمل واجهات برمجة التطبيقات في androidx.sqlite.db على نظام التشغيل Android فقط، ويجب إعادة بناء أي استخدامات باستخدام واجهات برمجة تطبيقات SQLite Driver. بالنسبة إلى التوافق مع الأنظمة القديمة، وطالما أنّ RoomDatabase قد تم ضبطه باستخدام SupportSQLiteOpenHelper.Factory (أي لم يتم ضبط SQLiteDriver)، تعمل الغرفة في "وضع التوافق" حيث تعمل كل من واجهات برمجة تطبيقات SQLite وSQLite Driver API على النحو المتوقَّع. يتيح ذلك إمكانية إجراء عمليات نقل متزايدة بحيث لا تحتاج إلى تحويل جميع استخدامات Support SQLite إلى برنامج SQLite Driver في تغيير واحد.

الأمثلة التالية هي الاستخدامات الشائعة لتطبيق Support SQLite ونظيره من برامج تشغيل SQLite:

دعم SQLite (من)

تنفيذ طلب بحث بدون نتائج

val database: SupportSQLiteDatabase = ...
database.execSQL("ALTER TABLE ...")

تنفيذ استعلام بنتيجة ولكن بدون وسيطات

val database: SupportSQLiteDatabase = ...
database.query("SELECT * FROM Pet").use { cursor ->
  while (cusor.moveToNext()) {
    // read columns
    cursor.getInt(0)
    cursor.getString(1)
  }
}

تنفيذ استعلام باستخدام نتيجة ووسيطات

database.query("SELECT * FROM Pet WHERE id = ?", id).use { cursor ->
  if (cursor.moveToNext()) {
    // row found, read columns
  } else {
    // row not found
  }
}

برنامج تشغيل SQLite (إلى)

تنفيذ طلب بحث بدون نتائج

val connection: SQLiteConnection = ...
connection.execSQL("ALTER TABLE ...")

تنفيذ استعلام بنتيجة ولكن بدون وسيطات

val connection: SQLiteConnection = ...
connection.prepare("SELECT * FROM Pet").use { statement ->
  while (statement.step()) {
    // read columns
    statement.getInt(0)
    statement.getText(1)
  }
}

تنفيذ استعلام باستخدام نتيجة ووسيطات

connection.prepare("SELECT * FROM Pet WHERE id = ?").use { statement ->
  statement.bindInt(1, id)
  if (statement.step()) {
    // row found, read columns
  } else {
    // row not found
  }
}

تتوفّر واجهات برمجة التطبيقات لمعاملات قاعدة البيانات مباشرةً في SupportSQLiteDatabase مع beginTransaction() وsetTransactionSuccessful() وendTransaction(). وتتوفّر أيضًا من خلال الغرفة باستخدام runInTransaction(). قم بترحيل هذه الاستخدامات إلى واجهات برمجة تطبيقات SQLite Driver.

دعم SQLite (من)

إجراء معاملة (باستخدام "RoomDatabase")

val database: RoomDatabase = ...
database.runInTransaction {
  // perform database operations in transaction
}

إجراء معاملة (باستخدام "SupportSQLiteDatabase")

val database: SupportSQLiteDatabase = ...
database.beginTransaction()
try {
  // perform database operations in transaction
  database.setTransactionSuccessful()
} finally {
  database.endTransaction()
}

برنامج تشغيل SQLite (إلى)

إجراء معاملة (باستخدام "RoomDatabase")

val database: RoomDatabase = ...
database.useWriterConnection { transactor ->
  transactor.immediateTransaction {
    // perform database operations in transaction
  }
}

إجراء معاملة (باستخدام "SQLiteConnection")

val connection: SQLiteConnection = ...
connection.execSQL("BEGIN IMMEDIATE TRANSACTION")
try {
  // perform database operations in transaction
  connection.execSQL("END TRANSACTION")
} catch(t: Throwable) {
  connection.execSQL("ROLLBACK TRANSACTION")
}

يجب أيضًا نقل العديد من عمليات إلغاء معاودة الاتصال إلى نظيراتها في برنامج التشغيل:

دعم SQLite (من)

الفئات الفرعية لنقل البيانات

object Migration_1_2 : Migration(1, 2) {
  override fun migrate(db: SupportSQLiteDatabase) {
    // ...
  }
}

الفئات الفرعية لمواصفات نقل البيانات التلقائي

class AutoMigrationSpec_1_2 : AutoMigrationSpec {
  override fun onPostMigrate(db: SupportSQLiteDatabase) {
    // ...
  }
}

الفئات الفرعية لمعاودة الاتصال بقاعدة البيانات

object MyRoomCallback : RoomDatabase.Callback {
  override fun onCreate(db: SupportSQLiteDatabase) {
    // ...
  }

  override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
    // ...
  }

  override fun onOpen(db: SupportSQLiteDatabase) {
    // ...
  }
}

برنامج تشغيل SQLite (إلى)

الفئات الفرعية لنقل البيانات

object Migration_1_2 : Migration(1, 2) {
  override fun migrate(connection: SQLiteConnection) {
    // ...
  }
}

الفئات الفرعية لمواصفات نقل البيانات التلقائي

class AutoMigrationSpec_1_2 : AutoMigrationSpec {
  override fun onPostMigrate(connection: SQLiteConnection) {
    // ...
  }
}

الفئات الفرعية لمعاودة الاتصال بقاعدة البيانات

object MyRoomCallback : RoomDatabase.Callback {
  override fun onCreate(connection: SQLiteConnection) {
    // ...
  }

  override fun onDestructiveMigration(connection: SQLiteConnection) {
    // ...
  }

  override fun onOpen(connection: SQLiteConnection) {
    // ...
  }
}

باختصار، استبدِل استخدامات SQLiteDatabase بـ SQLiteConnection عند عدم توفّر RoomDatabase، كما هو الحال في عمليات إلغاء معاودة الاتصال (onMigrate وonCreate وما إلى ذلك). إذا كان RoomDatabase متاحًا، يمكنك الوصول إلى اتصال قاعدة البيانات الأساسي باستخدام RoomDatabase.useReaderConnection وRoomDatabase.useWriterConnection بدلاً من RoomDatabase.openHelper.writtableDatabase.

تحويل دوال DAO المحظورة إلى دوال تعليق

يعتمد إصدار KMP من Room على الكوروتينات لإجراء عمليات وحدات الإدخال والإخراج على CoroutineContext الذي تم إعداده. يعني هذا أنك بحاجة إلى ترحيل أي دوال DAO محظورة لتعليق الدوال.

حظر دالة DAO (من)

@Query("SELECT * FROM Todo")
fun getAllTodos(): List<Todo>

تعليق دالة DAO (إلى)

@Query("SELECT * FROM Todo")
suspend fun getAllTodos(): List<Todo>

قد يكون نقل دوال حظر DAO الحالية لتعليق الدوال معقدًا إذا كانت قاعدة التعليمات البرمجية الحالية لا تتضمن الكوروتينات. راجِع الكوروتينات في Android للبدء في استخدام الكوروتينات في قاعدة الرموز.

تحويل أنواع النتائج التفاعلية إلى التدفق

لا يلزم أن تكون جميع دوال DAO عبارة عن تعليق دوال. يجب عدم تحويل دوال DAO التي تعرض أنواعًا تفاعلية مثل LiveData أو Flowable في RxJava إلى تعليق الدوال. ومع ذلك، لا تكون بعض الأنواع، مثل LiveData، متوافقة مع برنامج KMP. يجب نقل دوال DAO ذات أنواع الإرجاع التفاعلية إلى تدفقات الكوروتين.

نوع KMP غير متوافق (من)

@Query("SELECT * FROM Todo")
fun getTodosLiveData(): LiveData<List<Todo>>

نوع KMP المتوافق (إلى)

@Query("SELECT * FROM Todo")
fun getTodosFlow(): Flow<List<Todo>>

يُرجى الرجوع إلى Flows in Android لبدء استخدام التدفقات في قاعدة الرموز.

ضبط سياق الكوروتين (اختياري)

يمكن ضبط RoomDatabase اختياريًا مع منفّذي التطبيقات المشتركة باستخدام RoomDatabase.Builder.setQueryExecutor() لتنفيذ عمليات قاعدة البيانات. نظرًا لأن أدوات التنفيذ غير متوافقة مع KMP، فإن واجهة برمجة التطبيقات setQueryExecutor() في الغرفة غير متاحة للمصادر الشائعة. بدلاً من ذلك، يجب ضبط RoomDatabase باستخدام CoroutineContext. يمكن ضبط سياق باستخدام السمة RoomDatabase.Builder.setCoroutineContext()، وفي حال عدم ضبط أي سياق، سيتم ضبط السمة RoomDatabase تلقائيًا على استخدام Dispatchers.IO.

إعداد برنامج تشغيل SQLite

بعد نقل استخدامات Support SQLite إلى واجهات برمجة تطبيقات SQLite Driver، يجب ضبط برنامج التشغيل باستخدام RoomDatabase.Builder.setDriver. وبرنامج التشغيل المقترَح هو BundledSQLiteDriver. يمكنك الاطّلاع على عمليات تنفيذ برنامج التشغيل للحصول على أوصاف لعمليات التنفيذ المتاحة لبرنامج التشغيل.

لا تتوافق SupportSQLiteOpenHelper.Factory المخصّصة التي تم ضبطها باستخدام RoomDatabase.Builder.openHelperFactory() في KMP، ويجب إعادة تنفيذ الميزات التي يقدمها المساعد المفتوح المخصص إلى واجهات SQLite Driver.

نقل نماذج بيانات الغرفة

وبعد اكتمال معظم خطوات عمليات نقل البيانات، يمكن نقل تعريفات الغرفة إلى مجموعة مصادر مشتركة. يُرجى العلم أنّه يمكن استخدام استراتيجيات expect / actual لنقل التعريفات ذات الصلة بالغرفة بشكل تدريجي. على سبيل المثال، إذا لم يكن من الممكن نقل جميع دوال DAO المحظورة لتعليق الدوال، من الممكن الإعلان عن واجهة تتضمّن تعليقات توضيحية expect @Dao فارغة في الرمز الشائع، ولكنها تحتوي على وظائف حظر في Android.

// shared/src/commonMain/kotlin/Database.kt

@Database(entities = [TodoEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
  abstract fun getDao(): TodoDao
  abstract fun getBlockingDao(): BlockingTodoDao
}

@Dao
interface TodoDao {
    @Query("SELECT count(*) FROM TodoEntity")
    suspend fun count(): Int
}

@Dao
expect interface BlockingTodoDao
// shared/src/androidMain/kotlin/BlockingTodoDao.kt

@Dao
actual interface BlockingTodoDao {
    @Query("SELECT count(*) FROM TodoEntity")
    fun count(): Int
}