توفّر مكتبة Room لاستدامة البيانات طبقة تجريد فوق SQLite للسماح بالوصول إلى قاعدة بيانات أكثر فعالية مع الاستفادة من الإمكانات الكاملة لـ SQLite. تركز هذه الصفحة على استخدام Room في مشاريع Kotlin Multiplatform (KMP). لمزيد من المعلومات عن استخدام Room، يُرجى الاطّلاع على مقالة حفظ البيانات في قاعدة بيانات محلية باستخدام Room أو على نماذجنا الرسمية.
إعداد التبعيات
لإعداد Room في مشروع KMP، أضِف التبعيات الخاصة بالعناصر في ملف build.gradle.kts لوحدة KMP.
حدِّد التبعيات في ملف libs.versions.toml:
[versions]
room3 = "3.0.1"
sqlite = "2.7.0"
ksp = "<kotlinCompatibleKspVersion>"
[libraries]
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" }
androidx-room3-runtime = { module = "androidx.room3:room3-runtime", version.ref = "room3" }
androidx-room3-compiler = { module = "androidx.room3:room3-compiler", version.ref = "room3" }
# Optional SQLite Wrapper
androidx-room3-sqlite-wrapper = { module = "androidx.room3:room3-sqlite-wrapper", version.ref = "room3" }
[plugins]
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
androidx-room3 = { id = "androidx.room3", version.ref = "room3" }
أضِف Room Gradle Plugin لضبط مخطّطات Room ومكوّن KSP الإضافي.
plugins {
alias(libs.plugins.ksp)
alias(libs.plugins.androidx.room3)
}
أضِف تبعية وقت تشغيل Room ومكتبة SQLite المجمّعة:
commonMain.dependencies {
implementation(libs.androidx.room3.runtime)
implementation(libs.androidx.sqlite.bundled)
}
// Optional when using Room SQLite Wrapper
androidMain.dependencies {
implementation(libs.androidx.room3.sqlite.wrapper)
}
أضِف تبعيات KSP إلى كتلة dependencies الجذرية. عليك إضافة جميع الأهداف التي يستخدمها تطبيقك. لمزيد من المعلومات، يُرجى الاطّلاع على مقالة KSP with
Kotlin Multiplatform.
dependencies {
add("kspAndroid", libs.androidx.room3.compiler)
add("kspIosSimulatorArm64", libs.androidx.room3.compiler)
add("kspIosX64", libs.androidx.room3.compiler)
add("kspIosArm64", libs.androidx.room3.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 API. لإنشاء مثيل قاعدة البيانات، حدِّد
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 في مجموعات المصادر الخاصة بالمنصّة التي تحدِّد برنامج تشغيل خاصًا بالمنصّة. راجِع
عمليات تنفيذ برنامج التشغيل للاطّلاع على أوصاف عمليات تنفيذ برنامج التشغيل
المتاحة. يمكنك استخدام أيّ من الخيارَين التاليَين:
AndroidSQLiteDriverفيandroidMainNativeSQLiteDriverفيiosMain
لاستخدام 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")
}
}
}
ضبط سياق نمط "كوروتين"
يمكن اختياريًا ضبط عنصر RoomDatabase على Android باستخدام منفّذات التطبيق المشترَكة باستخدام RoomDatabase.Builder.setQueryExecutor لتنفيذ عمليات قاعدة البيانات.
بما أنّ المنفّذات غير متوافقة مع KMP، لا تتوفّر واجهة برمجة التطبيقات setQueryExecutor في Room في commonMain. بدلاً من ذلك، يجب ضبط عنصر RoomDatabase باستخدام
CoroutineContext، ويمكنك ضبطه باستخدام
RoomDatabase.Builder.setCoroutineContext. إذا لم تضبط سياقًا، يتم ضبط عنصر RoomDatabase تلقائيًا على Dispatchers.IO.
التصغير والتشويش
إذا تم تصغير المشروع أو تشويشه، عليك تضمين قاعدة ProGuard التالية لكي يتمكّن Room من العثور على عملية التنفيذ التي تم إنشاؤها لتعريف قاعدة البيانات:
-keep class * extends androidx.room3.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.room3:room3-sqlite-wrapper واجهات برمجة تطبيقات للربط بين SQLiteDriver وSupportSQLiteDatabase أثناء عملية النقل.
للحصول على SupportSQLiteDatabase من RoomDatabase تم ضبطه باستخدام
SQLiteDriver، استخدِم دالة الإضافة الجديدة
RoomDatabase.getSupportWrapper. يساعد هذا الغلاف المتوافق في الحفاظ على
الاستخدامات الحالية لـ SupportSQLiteDatabase، التي تحصل عليها غالبًا من
RoomDatabase.openHelper.writableDatabase، مع استخدام SQLiteDriver،
خاصةً لقواعد التعليمات البرمجية التي تتضمّن استخدامات مكثّفة لواجهة برمجة التطبيقات SupportSQLite وتريد
استخدام BundledSQLiteDriver.
تحويل الفئات الفرعية لعمليات النقل
يجب نقل الفئات الفرعية لعمليات النقل إلى نظيراتها في SQLite Driver:
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 Driver:
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.setQueryCallbackRoomDatabase.QueryCallback
ننوي إضافة دعم لمعاودة الاتصال بطلب البحث في إصدار مستقبلي من Room.
لا تتوفّر واجهة برمجة التطبيقات لضبط RoomDatabase باستخدام معاودة الاتصال بطلب البحث
RoomDatabase.Builder.setQueryCallback، بالإضافة إلى واجهة رد الاتصال
RoomDatabase.QueryCallback، في common، وبالتالي لا تتوفّر
في المنصات الأخرى غير Android.
الإغلاق التلقائي لقاعدة البيانات
لا تتوفّر واجهة برمجة التطبيقات لتفعيل الإغلاق التلقائي بعد انتهاء المهلة، RoomDatabase.Builder.setAutoCloseTimeout، إلا على Android ولا تتوفّر في المنصات الأخرى.
قاعدة البيانات المجمّعة مسبقًا
لا تتوفّر واجهات برمجة التطبيقات التالية لإنشاء RoomDatabase باستخدام قاعدة بيانات حالية (أي قاعدة بيانات مجمّعة مسبقًا) في `common`، وبالتالي لا تتوفّر في المنصات الأخرى غير Android. في ما يلي واجهات برمجة التطبيقات هذه:
RoomDatabase.Builder.createFromAssetRoomDatabase.Builder.createFromFileRoomDatabase.Builder.createFromInputStreamRoomDatabase.PrepackagedDatabaseCallback
ننوي إضافة دعم لقواعد البيانات المجمّعة مسبقًا في إصدار مستقبلي من Room.
إبطال صلاحية مثيلات متعددة
لا تتوفّر واجهة برمجة التطبيقات لتفعيل إبطال صلاحية فتح نسختين من التطبيق، RoomDatabase.Builder.enableMultiInstanceInvalidation، إلا على Android ولا تتوفّر في `common` أو المنصات الأخرى.
اقتراحات مخصصة لك
- ملاحظة: يتم عرض نص الرابط عندما تكون JavaScript غير مفعّلة
- الدرس التطبيقي حول نقل التطبيقات الحالية إلى Room KMP
- الدرس التطبيقي حول بدء استخدام KMP
- حفظ البيانات في قاعدة بيانات محلية باستخدام Room