إعداد Room Database لـ KMP

توفّر مكتبة Room persistence طبقة تجريد فوق SQLite للسماح بالوصول إلى قاعدة بيانات أكثر فعالية مع الاستفادة من الإمكانات الكاملة لـ SQLite. تركز هذه الصفحة على استخدام Room في مشاريع Kotlin Multiplatform (KMP). لمزيد من المعلومات حول استخدام Room، يُرجى الاطّلاع على مقالة حفظ البيانات في قاعدة بيانات محلية باستخدام Room أو على نماذجنا الرسمية.

إعداد التبعيات

لإعداد Room في مشروع KMP، أضِف التبعيات الخاصة بالعناصر في ملف build.gradle.kts لوحدة KMP.

حدِّد التبعيات في ملف libs.versions.toml:

[versions]
room = "2.8.4"
sqlite = "2.6.2"
ksp = "<kotlinCompatibleKspVersion>"

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

# Optional SQLite Wrapper available in version 2.8.0 and higher
androidx-room-sqlite-wrapper = { module = "androidx.room:room-sqlite-wrapper", version.ref = "room" }

[plugins]
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
androidx-room = { id = "androidx.room", version.ref = "room" }

أضِف Room Gradle Plugin لضبط مخطّطات Room ومكوّن KSP الإضافي.

plugins {
  alias(libs.plugins.ksp)
  alias(libs.plugins.androidx.room)
}

أضِف تبعية وقت تشغيل Room ومكتبة SQLite المجمّعة:

commonMain.dependencies {
  implementation(libs.androidx.room.runtime)
  implementation(libs.androidx.sqlite.bundled)
}

// Optional when using Room SQLite Wrapper
androidMain.dependencies {
  implementation(libs.androidx.room.sqlite.wrapper)
}

أضِف تبعيات KSP إلى كتلة dependencies الجذرية. يُرجى العِلم أنّه عليك إضافة جميع الأهداف التي يستخدمها تطبيقك. لمزيد من المعلومات، يُرجى الاطّلاع على مقالة KSP مع Kotlin Multiplatform.

dependencies {
    add("kspAndroid", libs.androidx.room.compiler)
    add("kspIosSimulatorArm64", libs.androidx.room.compiler)
    add("kspIosX64", libs.androidx.room.compiler)
    add("kspIosArm64", libs.androidx.room.compiler)
    // Add any other platform target you use in your project, for example kspDesktop
}

حدِّد دليل مخطّط Room. لمزيد من المعلومات، يُرجى الاطّلاع على مقالة ضبط موقع المخطّط باستخدام Room Gradle Plugin.

room {
    schemaDirectory("$projectDir/schemas")
}

تحديد فئات قاعدة البيانات

عليك إنشاء فئة قاعدة بيانات تم وضع علامة @Database عليها بالإضافة إلى كائنات الوصول إلى البيانات (DAO) والكيانات داخل مجموعة رموز المصدر الشائعة لوحدة KMP المشترَكة. سيسمح وضع هذه الفئات في مصادر شائعة بمشاركتها بين جميع المنصات المستهدَفة.

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

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

// The Room compiler generates the `actual` implementations.
@Suppress("KotlinNoActualForExpect")
expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase> {
    override fun initialize(): AppDatabase
}

عند تعريف عنصر expect باستخدام الواجهة RoomDatabaseConstructor، يُنشئ مجمّع Room عمليات التنفيذ actual. قد يعرض "استوديو Android" التحذير التالي، ويمكنك إيقافه باستخدام @Suppress("KotlinNoActualForExpect"):

Expected object 'AppDatabaseConstructor' has no actual declaration in module`

بعد ذلك، حدِّد واجهة DAO جديدة أو انقل واجهة حالية إلى commonMain

// shared/src/commonMain/kotlin/TodoDao.kt

@Dao
interface TodoDao {
  @Insert
  suspend fun insert(item: TodoEntity)

  @Query("SELECT count(*) FROM TodoEntity")
  suspend fun count(): Int

  @Query("SELECT * FROM TodoEntity")
  fun getAllAsFlow(): Flow<List<TodoEntity>>
}

حدِّد كياناتك أو انقلها إلى commonMain:

// shared/src/commonMain/kotlin/TodoEntity.kt

@Entity
data class TodoEntity(
  @PrimaryKey(autoGenerate = true) val id: Long = 0,
  val title: String,
  val content: String
)

إنشاء أداة إنشاء قاعدة البيانات الخاصة بالمنصّة

عليك تحديد أداة إنشاء قاعدة بيانات لإنشاء مثيل من Room على كل منصّة. هذا هو الجزء الوحيد من واجهة برمجة التطبيقات الذي يجب أن يكون في مجموعات المصادر الخاصة بالمنصّة بسبب الاختلافات في واجهات برمجة تطبيقات نظام الملفات.

Android

على Android، يتم عادةً الحصول على موقع قاعدة البيانات من خلال Context.getDatabasePath() واجهة برمجة التطبيقات. لإنشاء مثيل قاعدة البيانات، حدِّد Context مع مسار قاعدة البيانات.

// shared/src/androidMain/kotlin/Database.android.kt

fun getDatabaseBuilder(context: Context): RoomDatabase.Builder<AppDatabase> {
  val appContext = context.applicationContext
  val dbFile = appContext.getDatabasePath("my_room.db")
  return Room.databaseBuilder<AppDatabase>(
    context = appContext,
    name = dbFile.absolutePath
  )
}

iOS

لإنشاء مثيل قاعدة البيانات على iOS، قدِّم مسار قاعدة بيانات باستخدام الـ NSFileManager، الذي يكون عادةً في الـNSDocumentDirectory.

// shared/src/iosMain/kotlin/Database.ios.kt

fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
    val dbFilePath = documentDirectory() + "/my_room.db"
    return Room.databaseBuilder<AppDatabase>(
        name = dbFilePath,
    )
}

private fun documentDirectory(): String {
  val documentDirectory = NSFileManager.defaultManager.URLForDirectory(
    directory = NSDocumentDirectory,
    inDomain = NSUserDomainMask,
    appropriateForURL = null,
    create = false,
    error = null,
  )
  return requireNotNull(documentDirectory?.path)
}

آلة جافا الافتراضية (JVM) (أجهزة الكمبيوتر)

لإنشاء مثيل قاعدة البيانات، قدِّم مسار قاعدة بيانات باستخدام واجهات برمجة تطبيقات Java أو Kotlin.

// shared/src/jvmMain/kotlin/Database.desktop.kt

fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
    val dbFile = File(System.getProperty("java.io.tmpdir"), "my_room.db")
    return Room.databaseBuilder<AppDatabase>(
        name = dbFile.absolutePath,
    )
}

إنشاء مثيل من قاعدة البيانات

بعد الحصول على RoomDatabase.Builder من أحد أدوات الإنشاء الخاصة بالمنصّة، يمكنك ضبط بقية قاعدة بيانات Room في رمز برمجي شائع مع إنشاء مثيل قاعدة البيانات الفعلي.

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

fun getRoomDatabase(
    builder: RoomDatabase.Builder<AppDatabase>
): AppDatabase {
  return builder
      .setDriver(BundledSQLiteDriver())
      .setQueryCoroutineContext(Dispatchers.IO)
      .build()
}

اختيار برنامج تشغيل SQLite

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

إذا كنت تريد استخدام SQLite الذي يوفّره نظام التشغيل، استخدِم واجهة برمجة التطبيقات setDriver في مجموعات المصادر الخاصة بالمنصّة التي تحدِّد برنامج تشغيل خاصًا بالمنصّة. يُرجى الاطّلاع على عمليات تنفيذ برنامج التشغيل للحصول على أوصاف لعمليات تنفيذ برنامج التشغيل المتاحة. يمكنك استخدام أيٍّ مما يلي:

لاستخدام NativeSQLiteDriver، عليك تقديم خيار الرابط -lsqlite3 لكي يرتبط تطبيق iOS ديناميكيًا بـ SQLite الخاص بالنظام.

// shared/build.gradle.kts

kotlin {
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach { iosTarget ->
        iosTarget.binaries.framework {
            baseName = "TodoApp"
            isStatic = true
            // Required when using NativeSQLiteDriver
            linkerOpts.add("-lsqlite3")
        }
    }
}

ضبط سياق Coroutine (اختياري)

يمكن اختياريًا ضبط عنصر RoomDatabase على Android باستخدام منفّذات التطبيق المشترَكة باستخدام RoomDatabase.Builder.setQueryExecutor() لتنفيذ عمليات قاعدة البيانات.

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

التصغير والتشويش

إذا تم تصغير المشروع أو تشويشه، عليك تضمين قاعدة ProGuard التالية لكي يتمكّن Room من العثور على عملية التنفيذ التي تم إنشاؤها لتعريف قاعدة البيانات:

-keep class * extends androidx.room.RoomDatabase { <init>(); }

نقل البيانات إلى Kotlin Multiplatform

تم تطوير Room في الأصل كمكتبة Android، وتم نقلها لاحقًا إلى KMP مع التركيز على توافق واجهة برمجة التطبيقات. يختلف إصدار KMP من Room إلى حد ما بين المنصات وعن الإصدار الخاص بنظام Android. تم إدراج هذه الاختلافات ووصفها على النحو التالي.

نقل البيانات من Support SQLite إلى SQLite Driver

يجب إعادة تصميم أي استخدامات لـ SupportSQLiteDatabase وواجهات برمجة التطبيقات الأخرى في androidx.sqlite.db باستخدام واجهات برمجة تطبيقات SQLite Driver، لأنّ واجهات برمجة التطبيقات في androidx.sqlite.db خاصة بنظام Android فقط (يُرجى العِلم بالحزمة المختلفة عن حزمة KMP).

لضمان التوافق مع الإصدارات السابقة، وطالما تم ضبط RoomDatabase باستخدام SupportSQLiteOpenHelper.Factory (على سبيل المثال، لم يتم ضبط SQLiteDriver)، فإنّ Room يتصرف في "وضع التوافق" حيث تعمل كل من واجهات برمجة تطبيقات Support SQLite وSQLite Driver على النحو المتوقّع. يتيح ذلك عمليات نقل تدريجية، لذا لن تحتاج إلى تحويل جميع استخدامات Support SQLite إلى SQLite Driver في تغيير واحد.

استخدام Room SQLite Wrapper (اختياري)

يوفّر العنصر androidx.room:room-sqlite-wrapper واجهات برمجة تطبيقات للربط بين SQLiteDriver وSupportSQLiteDatabase أثناء عملية النقل.

للحصول على SupportSQLiteDatabase من RoomDatabase تم ضبطه باستخدام SQLiteDriver، استخدِم دالة الإضافة الجديدة RoomDatabase.getSupportWrapper(). يساعد هذا الغلاف المتوافق في الحفاظ على الاستخدامات الحالية لـ SupportSQLiteDatabase (التي يتم الحصول عليها غالبًا من RoomDatabase.openHelper.writableDatabase) أثناء استخدام SQLiteDriver، خاصةً لقواعد الرموز البرمجية التي تتضمّن استخدامات واسعة النطاق لواجهة برمجة التطبيقات SupportSQLite التي تريد استخدام BundledSQLiteDriver.

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

يجب نقل الفئات الفرعية لعمليات النقل إلى نظيراتها في برنامج تشغيل SQLite:

Kotlin Multiplatform

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

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

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

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

Android فقط

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

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

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

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

تحويل معاودة الاتصال بقاعدة البيانات

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

Kotlin Multiplatform

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

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

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

Android فقط

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

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

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

تحويل دوال DAO التي تستخدم @RawQuery

ستحتاج الدوال التي تم وضع العلامة @RawQuery عليها والتي تم تجميعها للمنصات غير Android إلى الإعلان عن مَعلمة من النوع RoomRawQuery بدلاً من SupportSQLiteQuery.

Kotlin Multiplatform

تحديد طلب البحث الأولي

@Dao
interface TodoDao {
  @RawQuery
  suspend fun getTodos(query: RoomRawQuery): List<TodoEntity>
}

يمكن بعد ذلك استخدام RoomRawQuery لإنشاء طلب بحث في وقت التشغيل:

suspend fun AppDatabase.getTodosWithLowercaseTitle(title: String): List<TodoEntity> {
    val query = RoomRawQuery(
        sql = "SELECT * FROM TodoEntity WHERE title = ?",
        onBindStatement = {
            it.bindText(1, title.lowercase())
        }
    )

    return todoDao().getTodos(query)
}

Android فقط

تحديد طلب البحث الأولي

@Dao
interface TodoDao {
  @RawQuery
  suspend fun getTodos(query: SupportSQLiteQuery): List<TodoEntity>
}

يمكن بعد ذلك استخدام SimpleSQLiteQuery لإنشاء طلب بحث في وقت التشغيل:

suspend fun AndroidOnlyDao.getTodosWithLowercaseTitle(title: String): List<TodoEntity> {
  val query = SimpleSQLiteQuery(
      query = "SELECT * FROM TodoEntity WHERE title = ?",
      bindArgs = arrayOf(title.lowercase())
  )
  return getTodos(query)
}

تحويل دوال DAO التي تحظر التنفيذ

تستفيد Room من مكتبة kotlinx.coroutines غير المتزامنة الغنية بالميزات التي توفّرها Kotlin لمنصات متعددة. للحصول على أفضل وظائف، يتم فرض دوال suspend لكائنات الوصول إلى البيانات (DAO) التي تم تجميعها في مشروع KMP، باستثناء كائنات الوصول إلى البيانات (DAO) التي تم تنفيذها في androidMain للحفاظ على التوافق مع الإصدارات السابقة لقاعدة الرموز البرمجية الحالية. عند استخدام Room لـ KMP، يجب أن تكون جميع دوال DAO التي تم تجميعها للمنصات غير Android دوال suspend.

Kotlin Multiplatform

طلبات البحث التي تعلّق التنفيذ

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

المعاملات التي تعلّق التنفيذ

@Transaction
suspend fun transaction() {  }

Android فقط

طلبات البحث التي تحظر التنفيذ

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

المعاملات التي تحظر التنفيذ

@Transaction
fun blockingTransaction() {  }

تحويل الأنواع التفاعلية إلى Flow

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

Kotlin Multiplatform

الأنواع التفاعلية Flows

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

Android فقط

الأنواع التفاعلية، مثل LiveData أو Flowable من RxJava

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

تحويل واجهات برمجة تطبيقات المعاملات

يمكن أن تفرّق واجهات برمجة تطبيقات معاملات قاعدة البيانات لـ Room KMP بين معاملات الكتابة (useWriterConnection) والقراءة (useReaderConnection).

Kotlin Multiplatform

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

Android فقط

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

معاملات الكتابة

استخدِم معاملات الكتابة للتأكّد من أنّ طلبات البحث المتعددة تكتب البيانات بشكلٍ ذري، لكي يتمكّن القرّاء من الوصول إلى البيانات باستمرار. يمكنك إجراء ذلك باستخدام useWriterConnection مع أي من أنواع المعاملات الثلاثة:

  • immediateTransaction: في وضع Write-Ahead Logging (WAL) (تلقائيًا)، يحصل هذا النوع من المعاملات على قفل عند بدئه، ولكن يمكن للقرّاء مواصلة القراءة. هذا هو الخيار المفضّل في معظم الحالات.

  • deferredTransaction: لن تحصل المعاملة على قفل حتى أول عبارة كتابة. استخدِم هذا النوع من المعاملات كتحسين عندما لا تكون متأكدًا مما إذا كانت عملية كتابة ستكون مطلوبة داخل المعاملة. على سبيل المثال، إذا بدأت معاملة لحذف الأغاني من قائمة تشغيل باستخدام اسم قائمة التشغيل فقط ولم تكن قائمة التشغيل موجودة، فلن تكون هناك حاجة إلى أي عملية كتابة (حذف).

  • exclusiveTransaction: يتصرف هذا الوضع بشكلٍ مطابق لـ immediateTransaction في وضع WAL. في أوضاع التسجيل الأخرى، يمنع هذا الوضع اتصالات قاعدة البيانات الأخرى من قراءة قاعدة البيانات أثناء إجراء المعاملة.

معاملات القراءة

استخدِم معاملات القراءة لقراءة قاعدة البيانات بشكلٍ متّسق عدة مرات. على سبيل المثال، عندما يكون لديك طلبَا بحث منفصلَين أو أكثر ولا تستخدم عبارة JOIN. يُسمح فقط بالمعاملات المؤجّلة في اتصالات القارئ. ستؤدي محاولة بدء معاملة فورية أو حصرية في اتصال قارئ إلى طرح استثناء، لأنّ هذه المعاملات تُعتبر عمليات "كتابة".

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

غير متاح في Kotlin Multiplatform

بعض واجهات برمجة التطبيقات التي كانت متاحة لنظام Android غير متاحة في Kotlin Multiplatform.

معاودة الاتصال بطلب البحث

لا تتوفّر واجهات برمجة التطبيقات التالية لضبط معاودات الاتصال بطلبات البحث في `common`، وبالتالي لا تتوفّر في المنصات الأخرى غير Android.

  • RoomDatabase.Builder.setQueryCallback
  • RoomDatabase.QueryCallback

ننوي إضافة دعم لمعاودة الاتصال بطلب البحث في إصدار مستقبلي من Room.

إنّ واجهة برمجة التطبيقات لضبط RoomDatabase باستخدام معاودة الاتصال بطلب البحث RoomDatabase.Builder.setQueryCallback بالإضافة إلى واجهة رد الاتصال RoomDatabase.QueryCallback غير متاحة في `common`، وبالتالي لا تتوفّر في المنصات الأخرى غير Android.

الإغلاق التلقائي لقاعدة البيانات

لا تتوفّر واجهة برمجة التطبيقات RoomDatabase.Builder.setAutoCloseTimeout لتفعيل الإغلاق التلقائي بعد انتهاء المهلة إلا على Android، ولا تتوفّر في المنصات الأخرى.

قاعدة البيانات المجمّعة مسبقًا

لا تتوفّر واجهات برمجة التطبيقات التالية لإنشاء RoomDatabase باستخدام قاعدة بيانات حالية (أي قاعدة بيانات مجمّعة مسبقًا) في `common`، وبالتالي لا تتوفّر في المنصات الأخرى غير Android. في ما يلي واجهات برمجة التطبيقات هذه:

  • RoomDatabase.Builder.createFromAsset
  • RoomDatabase.Builder.createFromFile
  • RoomDatabase.Builder.createFromInputStream
  • RoomDatabase.PrepackagedDatabaseCallback

ننوي إضافة دعم لقواعد البيانات المجمّعة مسبقًا في إصدار مستقبلي من Room.

إبطال صلاحية مثيلات متعددة

لا تتوفّر واجهة برمجة التطبيقات RoomDatabase.Builder.enableMultiInstanceInvalidation لتفعيل إبطال صلاحية فتح نسختين من التطبيق إلا على Android، ولا تتوفّر في `common` أو المنصات الأخرى.