پایگاه داده اتاق را برای KMP تنظیم کنید

کتابخانه‌ی پایداری Room یک لایه‌ی انتزاعی روی 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 را برای پیکربندی طرحواره‌های 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 مراجعه کنید.

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 را تولید می‌کند. اندروید استودیو ممکن است هشدار زیر را صادر کند که می‌توانید با @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 در هر پلتفرم تعریف کنید. این تنها بخشی از API است که به دلیل تفاوت در APIهای سیستم فایل، لازم است در مجموعه‌های منبع مخصوص پلتفرم باشد.

اندروید

در اندروید، مکان پایگاه داده معمولاً از طریق API مربوط به 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، با استفاده از 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 (دسکتاپ)

برای ایجاد نمونه پایگاه داده، با استفاده از API های جاوا یا کاتلین، مسیر پایگاه داده را ارائه دهید.

// 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 را فراخوانی می‌کند تا تعریف کند که پایگاه داده Room باید از چه درایور SQLite استفاده کند. این درایورها بر اساس پلتفرم هدف متفاوت هستند. قطعه کد قبلی از BundledSQLiteDriver استفاده می‌کند. این درایور پیشنهادی است که شامل SQLite کامپایل شده از منبع است، که سازگارترین و به‌روزترین نسخه SQLite را در تمام پلتفرم‌ها ارائه می‌دهد.

اگر می‌خواهید از SQLite ارائه شده توسط سیستم عامل استفاده کنید، از setDriver API در مجموعه‌های منبع مخصوص پلتفرم که یک درایور مخصوص پلتفرم را مشخص می‌کنند، استفاده کنید. برای توضیحات مربوط به پیاده‌سازی‌های درایور موجود، به پیاده‌سازی‌های درایور مراجعه کنید. می‌توانید از یکی از موارد زیر استفاده کنید:

برای استفاده از 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 در اندروید می‌تواند به صورت اختیاری با اجراکننده‌های برنامه مشترک با استفاده از RoomDatabase.Builder.setQueryExecutor() برای انجام عملیات پایگاه داده پیکربندی شود.

از آنجا که اجراکننده‌ها با KMP سازگار نیستند، API مربوط به setQueryExecutor() در Room در commonMain موجود نیست. در عوض، شیء RoomDatabase باید با یک CoroutineContext پیکربندی شود که می‌تواند با استفاده از RoomDatabase.Builder.setCoroutineContext() تنظیم شود. اگر هیچ زمینه‌ای تنظیم نشود، شیء RoomDatabase به طور پیش‌فرض از Dispatchers.IO استفاده خواهد کرد.

کوچک‌سازی و مبهم‌سازی

اگر پروژه کوچک‌سازی یا مبهم‌سازی شده است، باید قانون ProGuard زیر را وارد کنید تا Room بتواند پیاده‌سازی تولید شده از تعریف پایگاه داده را پیدا کند:

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

مهاجرت به کاتلین چند پلتفرمی

روم در ابتدا به عنوان یک کتابخانه اندروید توسعه داده شد و بعداً با تمرکز بر سازگاری با API به KMP منتقل شد. نسخه KMP روم تا حدودی بین پلتفرم‌ها و با نسخه مخصوص اندروید متفاوت است. این تفاوت‌ها به شرح زیر فهرست و شرح داده شده‌اند.

مهاجرت از SQLite پشتیبانی به SQLite درایور

هرگونه استفاده از SupportSQLiteDatabase و سایر APIها در androidx.sqlite.db باید با APIهای درایور SQLite بازسازی شود، زیرا APIهای موجود در androidx.sqlite.db فقط برای اندروید هستند (به بسته متفاوت از بسته KMP توجه داشته باشید).

برای سازگاری با نسخه‌های قبلی، و تا زمانی که RoomDatabase با SupportSQLiteOpenHelper.Factory پیکربندی شده باشد (برای مثال، هیچ SQLiteDriver تنظیم نشده باشد)، Room در «حالت سازگاری» رفتار می‌کند که در آن هر دو API مربوط به Support SQLite و SQLite Driver طبق انتظار کار می‌کنند. این امر مهاجرت‌های افزایشی را فعال می‌کند، به طوری که نیازی نیست تمام کاربردهای Support SQLite خود را در یک تغییر واحد به SQLite Driver تبدیل کنید.

استفاده از Room SQLite Wrapper (اختیاری)

آرتیفکت androidx.room:room-sqlite-wrapper رابط‌های برنامه‌نویسی کاربردی (API) را برای ایجاد پل ارتباطی بین SQLiteDriver و SupportSQLiteDatabase در طول مهاجرت فراهم می‌کند.

برای دریافت SupportSQLiteDatabase از RoomDatabase پیکربندی شده با SQLiteDriver ، از تابع افزونه جدید RoomDatabase.getSupportWrapper() استفاده کنید. این پوشش سازگاری به حفظ کاربردهای موجود SupportSQLiteDatabase (که اغلب از RoomDatabase.openHelper.writableDatabase به دست می‌آید) در حین پذیرش SQLiteDriver کمک می‌کند، به خصوص برای پایگاه‌های کد با کاربردهای گسترده API SupportSQLite که می‌خواهند از BundledSQLiteDriver استفاده کنند.

تبدیل مهاجرت‌ها

زیرکلاس‌های Migration باید به همتایان درایور SQLite منتقل شوند:

کاتلین چندسکویی

زیرکلاس‌های مهاجرت

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

زیرکلاس‌های مشخصات مهاجرت خودکار

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

فقط اندروید

زیرکلاس‌های مهاجرت

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

زیرکلاس‌های مشخصات مهاجرت خودکار

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

تبدیل فراخوانی پایگاه داده

فراخوانی‌های پایگاه داده باید به همتایان درایور SQLite منتقل شوند:

کاتلین چندسکویی

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

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

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

فقط اندروید

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

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

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

تبدیل توابع DAO @RawQuery

توابعی که با @RawQuery حاشیه‌نویسی شده‌اند و برای پلتفرم‌های غیر اندروید کامپایل می‌شوند، باید به جای SupportSQLiteQuery ، پارامتری از نوع RoomRawQuery تعریف کنند.

کاتلین چندسکویی

تعریف پرس و جوی خام

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

فقط اندروید

تعریف پرس و جوی خام

@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 که کاتلین برای پلتفرم‌های مختلف ارائه می‌دهد، بهره می‌برد. برای عملکرد بهینه، توابع suspend برای DAOهای کامپایل شده در یک پروژه KMP اعمال می‌شوند، به استثنای DAOهایی که در androidMain پیاده‌سازی شده‌اند تا سازگاری معکوس با کدبیس موجود حفظ شود. هنگام استفاده از Room برای KMP، تمام توابع DAO کامپایل شده برای پلتفرم‌های غیر اندروید باید توابع suspend باشند.

کاتلین چندسکویی

تعلیق کوئری‌ها

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

تعلیق تراکنش‌ها

@Transaction
suspend fun transaction() {  }

فقط اندروید

مسدود کردن کوئری‌ها

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

مسدود کردن تراکنش‌ها

@Transaction
fun blockingTransaction() {  }

تبدیل انواع واکنشی به جریان

لازم نیست همه توابع DAO توابع suspend باشند. توابع DAO که انواع واکنشی مانند LiveData یا Flowable از RxJava را برمی‌گردانند، نباید به توابع suspend تبدیل شوند. با این حال، برخی از انواع، مانند LiveData با KMP سازگار نیستند. توابع DAO با انواع بازگشتی واکنشی باید به جریان‌های coroutine منتقل شوند.

کاتلین چندسکویی

Flows واکنشی

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

فقط اندروید

انواع واکنشی مانند LiveData یا Flowable از RxJava

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

تبدیل APIهای تراکنش

رابط‌های برنامه‌نویسی کاربردی (API) تراکنش‌های پایگاه داده برای Room KMP می‌توانند بین تراکنش‌های نوشتن ( useWriterConnection ) و خواندن ( useReaderConnection ) تمایز قائل شوند.

کاتلین چندسکویی

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

فقط اندروید

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

در کاتلین چند پلتفرمی موجود نیست

برخی از APIهایی که برای اندروید در دسترس بودند، در کاتلین چندپلتفرمی در دسترس نیستند.

فراخوانی پرس و جو

API های زیر برای پیکربندی فراخوانی‌های کوئری به صورت عمومی در دسترس نیستند و بنابراین در پلتفرم‌های غیر از اندروید در دسترس نیستند.

  • RoomDatabase.Builder.setQueryCallback
  • RoomDatabase.QueryCallback

ما قصد داریم در نسخه بعدی Room پشتیبانی از فراخوانی پرس‌وجو (query callback) را اضافه کنیم.

API برای پیکربندی RoomDatabase با فراخوانی پرس‌وجوی RoomDatabase.Builder.setQueryCallback به همراه رابط فراخوانی RoomDatabase.QueryCallback به طور مشترک در دسترس نیستند و بنابراین در پلتفرم‌های دیگر غیر از اندروید نیز در دسترس نیستند.

بسته شدن خودکار پایگاه داده

API مربوط به فعال‌سازی بسته شدن خودکار پس از یک timeout، RoomDatabase.Builder.setAutoCloseTimeout ، فقط در اندروید موجود است و در پلتفرم‌های دیگر در دسترس نیست.

پایگاه داده پیش از بسته‌بندی

APIهای زیر برای ایجاد RoomDatabase با استفاده از یک پایگاه داده موجود (یعنی یک پایگاه داده از پیش بسته‌بندی شده) به صورت عمومی در دسترس نیستند و بنابراین در پلتفرم‌های دیگری غیر از اندروید نیز در دسترس نیستند. این APIها عبارتند از:

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

ما قصد داریم در نسخه بعدی Room، پشتیبانی از پایگاه‌های داده از پیش بسته‌بندی‌شده را اضافه کنیم.

ابطال چند نمونه‌ای

رابط برنامه‌نویسی کاربردی (API) برای فعال‌سازی نامعتبرسازی چند نمونه‌ای، RoomDatabase.Builder.enableMultiInstanceInvalidation ، فقط در اندروید موجود است و در پلتفرم‌های رایج یا سایر پلتفرم‌ها در دسترس نیست.

{% کلمه به کلمه %} {% فعل کمکی %} {% کلمه به کلمه %} {% فعل کمکی %}