غرفة (Kotlin Multiplatform)

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

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

الإصدار الحالي من Room الذي يتيح استخدام KMP هو 2.7.0-alpha01 أو إصدار أحدث.

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

  • androidx.room:room-gradle-plugin - المكوّن الإضافي Gradle لضبط مخطّطات Room
  • androidx.room:room-compiler: معالج KSP الذي ينشئ الرمز
  • androidx.room:room-runtime: جزء وقت التشغيل من المكتبة
  • androidx.sqlite:sqlite-bundled - (اختياري) مكتبة SQLite المُضمّنة

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

للحصول على معلومات إضافية حول الإعداد، يُرجى الاطّلاع على ما يلي:

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

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

عند تعريف عنصر expect باستخدام الواجهة RoomDatabaseConstructor، ينشئ مُجمِّع Room عمليات التنفيذ actual. قد يُصدر Android Studio تحذيرًا "Expected object 'AppDatabaseConstructor' has no actual declaration in module"، ويمكنك إيقافه باستخدام @Suppress("NO_ACTUAL_FOR_EXPECT").

// 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("NO_ACTUAL_FOR_EXPECT")
expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase> {
    override fun initialize(): AppDatabase
}

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

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

يُرجى العِلم أنّه يمكنك اختياريًا استخدام بيانات actual / expect لإنشاء عمليات تنفيذ Room خاصة بالنظام الأساسي. على سبيل المثال، يمكنك إضافة DAO خاص بالنظام الأساسي تم تحديده في رمز عادي باستخدام expect ثم تحديد تعريفات actual باستخدام طلبات بحث إضافية في مجموعات المصادر الخاصة بالنظام الأساسي.

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

عليك تحديد أداة إنشاء قاعدة بيانات لإنشاء مثيل لـ Room على كل منصة. هذا هو الجزء الوحيد من واجهة برمجة التطبيقات المطلوب أن يكون في مجموعات ملف المصدر الخاصة بالنظام الأساسي بسبب الاختلافات في واجهات برمجة تطبيقات نظام الملفات. على سبيل المثال، في Android، يتم عادةً الحصول على موقع قاعدة البيانات من خلال واجهة برمجة التطبيقات Context.getDatabasePath()، في حين يتم الحصول على موقع قاعدة البيانات في iOS باستخدام NSFileManager.

Android

لإنشاء مثيل قاعدة البيانات، حدِّد سياقًا مع ملف قاعدة بيانات المسار.

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

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

iOS

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

// shared/src/iosMain/kotlin/Database.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 الافتراضية (لأجهزة الكمبيوتر المكتبي)

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

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

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

تصغير الحجم وإخفاء مفاتيح فك التشفير

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

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

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

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

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

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

اختيار SQLiteDriver

تستخدِم مقتطفات الرموز البرمجية السابقة BundledSQLiteDriver. هذا هو البرنامج الموصى به الذي يتضمّن SQLite المجمَّع من المصدر، والذي يوفّر أحدث إصدار من SQLite على جميع المنصات. إذا أردت استخدام SQLite المقدَّمة من نظام التشغيل، استخدِم واجهة برمجة التطبيقات setDriver في مجموعات setDriver الخاصة بالنظام الأساسي والتي تحدِّد برنامج تشغيل خاص بالنظام الأساسي. بالنسبة إلى Android، يمكنك استخدام رمز AndroidSQLiteDriver، بينما يمكنك استخدام رمز NativeSQLiteDriver على iOS. لاستخدامNativeSQLiteDriver، عليك تقديم خيار رابط حتى يتم ربط تطبيق 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")
        }
    }
}

الاختلافات

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

حظر دوال DAO

عند استخدام Room لـ KMP، يجب أن تكون جميع وظائف DAO التي تم تجميعها لمنصّات غير Android دالات suspend باستثناء أنواع الإرجاع التفاعلية، مثل Flow.

// shared/src/commonMain/kotlin/MultiplatformDao.kt

@Dao
interface MultiplatformDao {
  // ERROR: Blocking function not valid for non-Android targets
  @Query("SELECT * FROM Entity")
  fun blockingQuery(): List<Entity>

  // OK
  @Query("SELECT * FROM Entity")
  suspend fun query(): List<Entity>

  // OK
  @Query("SELECT * FROM Entity")
  fun queryFlow(): Flow<List<Entity>>

  // ERROR: Blocking function not valid for non-Android targets
  @Transaction
  fun blockingTransaction() { // … }

  // OK
  @Transaction
  suspend fun transaction() { // … }
}

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

الاختلافات في الميزات مع KMP

يصف هذا القسم الاختلافات بين ميزات إصدارَي Room المتوافقَين مع KMP ونظام التشغيل Android.

دوال DAO في @RawQuery

بالنسبة إلى الدوال التي تمّت إضافة تعليقات توضيحية لها باستخدام @RawQuery والتي تمّ تجميعها لأنظمة التشغيل غير المستندة إلى Android، يجب تحديد مَعلمة من النوع RoomRawQuery بدلاً من SupportSQLiteQuery.

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

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

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

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

لا تتوفّر واجهات برمجة التطبيقات التالية لضبط عمليات استدعاء طلبات البحث بشكل شائع، وبالتالي لا تتوفّر في منصات غير 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، وهي غير متاحة في الأنظمة الأساسية الشائعة أو غيرها.