غرفة (Kotlin Multiplatform)

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

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

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

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

[versions]
room = "2.7.2"
sqlite = "2.5.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 plugin.

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. قد يعرض &quot;استوديو Android&quot; التحذير التالي، ويمكنك إيقافه باستخدام @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)
}

آلة جافا الافتراضية (على الكمبيوتر)

لإنشاء مثيل قاعدة البيانات، قدِّم مسار قاعدة بيانات باستخدام واجهات برمجة تطبيقات 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(). في حال عدم ضبط أي سياق، سيتم تلقائيًا استخدام Dispatchers.IO مع العنصر RoomDatabase.

إزالة البيانات غير الضرورية والتشويش

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

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

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

تم تطوير Room في الأصل كمكتبة Android، وتم نقلها لاحقًا إلى KMP مع التركيز على توافق واجهة برمجة التطبيقات. يختلف إصدار Room المتوافق مع KMP قليلاً بين المنصات وعن الإصدار المتوافق مع 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 في تغيير واحد.

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

يجب نقل الفئات الفرعية لعمليات نقل البيانات إلى نظيراتها في برنامج تشغيل 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) {
    // …
  }
}

تحويل دوال @RawQuery DAO

يجب أن تحدّد الدوال التي تمّت إضافة التعليق التوضيحي @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 التي تحظر

تستفيد الغرفة من مكتبة 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: في وضع تسجيل العمليات قبل تنفيذها (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 في الأنظمة الأساسية الشائعة، وبالتالي لا تتوفّر في الأنظمة الأساسية الأخرى غير Android.

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

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

قاعدة بيانات الحِزم المُعدّة مسبقًا

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

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

نحن بصدد إتاحة إمكانية استخدام قواعد البيانات المُعدّة مسبقًا في إصدار مستقبلي من Room.

إلغاء صلاحية مثيلات متعددة

لا تتوفّر واجهة برمجة التطبيقات لتفعيل إبطال صحة النُسخ المتعدّدة، RoomDatabase.Builder.enableMultiInstanceInvalidation إلا على أجهزة Android، ولا تتوفّر على الأنظمة الأساسية الشائعة أو غيرها.