Room (Kotlin Multiplatform)

ספריית מתמידים של חדר מספקת שכבת הפשטה מעל SQLite שמאפשרת לגישה חזקה יותר למסדי נתונים תוך ניצול כל העוצמה של SQLite. הזה להתמקד בשימוש ב'חדר' בפרויקטים של Kotlin Multiplatform (KMP). לקבלת מידע נוסף מידע על השימוש ב'חדר', ראו שמירת נתונים במסד נתונים מקומי באמצעות Room או הדוגמאות הרשמיות שלנו.

הגדרת יחסי תלות

הגרסה הנוכחית של Room שתומכת ב-KMP היא גרסה 2.7.0-alpha01 ואילך.

כדי להגדיר חדר בפרויקט KMP, צריך להוסיף את יחסי התלות של פריטי המידע שנוצרו בתהליך הפיתוח (Artifact) קובץ build.gradle.kts למודול שלך:

  • androidx.room:room-gradle-plugin – הפלאגין של Gradle להגדרת סכימות של חדרים
  • androidx.room:room-compiler - מעבד ה-KSP שיוצר קוד
  • androidx.room:room-runtime – החלק של סביבת זמן הריצה בספרייה
  • androidx.sqlite:sqlite-bundled – (אופציונלי) ספריית SQLite בחבילה

בנוסף, צריך להגדיר את מנהל התקן SQLite של Room. מנהלי ההתקנים האלה שונים בהתאם לפלטפורמת היעד. צפייה הטמעות של מנהלי ההתקנים לתיאורים של ההטמעות הזמינות של מנהלי התקנים.

למידע נוסף על ההגדרות, תוכלו להיעזר במאמרים הבאים:

הגדרת המחלקות של מסד הנתונים

עליך ליצור מחלקה של מסד נתונים עם הערות עם @Database יחד עם DAOs וישויות בתוך קבוצת המקור המשותף של מודול ה-KMP המשותף. מיקום המחלקות האלה במקורות משותפים יאפשרו שיתוף שלהן בין כל סוגי הטירגוט פלטפורמות שונות.

כשמצהירים על אובייקט expect עם הממשק RoomDatabaseConstructor, מהדר החדרים יוצר את 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
)

לתשומת ליבכם: אפשר להשתמש בהצהרות בפועל והצהרות צפויות כדי ליצור הטמעות של חדרים ספציפיות לפלטפורמה. לדוגמה, אפשר להוסיף DAO ספציפי לפלטפורמה, שמוגדר בקוד משותף באמצעות expect ואז לציין את ההגדרות של actual בשאילתות נוספות ספציפיות לפלטפורמה בקבוצות המקור.

יצירת הכלי לבניית מסדי נתונים

צריך להגדיר כלי לבניית מסדי נתונים כדי ליצור מופע של חדר בכל פלטפורמה. הזה הוא החלק היחיד ב-API שנדרש להיות במקור ספציפי לפלטפורמה בגלל ההבדלים בממשקי ה-API של מערכת הקבצים. לדוגמה, ב-Android, בדרך כלל המיקום של מסד הנתונים מתקבל ב-API של Context.getDatabasePath(). ב-iOS, המיקום של מסד הנתונים הוא באמצעות NSFileManager.

Android

כדי ליצור את מכונת מסד הנתונים, צריך לציין Context יחד עם מסד הנתונים נתיב.

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

JVM (מחשב)

כדי ליצור את מכונת מסד הנתונים, צריך לספק נתיב למסד נתונים באמצעות Java או Kotlin ממשקי API.

// 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,
    )
}

יצירת מודלים של מסד נתונים

לאחר קבלת 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 שסופק על ידי מערכת ההפעלה, צריך להשתמש ב-API של setDriver קבוצות מקור שמציינות מנהל התקן ספציפי לפלטפורמה. ב-Android, אפשר להשתמש ב AndroidSQLiteDriver. במכשירי iOS אפשר להשתמש בNativeSQLiteDriver. שפת תרגום להשתמש ב-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")
        }
    }
}

הבדלים

החדר פותח במקור כספריית Android, ומאוחר יותר עבר אל KMP תוך התמקדות בתאימות ל-API. גרסת ה-KMP של Room שונה במידה מסוימת בין פלטפורמות ומהגרסה הספציפית ל-Android. ההבדלים האלה ומתוארים בהמשך.

חסימת פונקציות DAO

כשמשתמשים ב- Room for 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() { // … }
}

ההטבות של החדר הן ספריית kotlinx.coroutines האסינכרונית עם מגוון התכונות והאפשרויות ש-Kotlin מציעה לכמה פלטפורמות. לפונקציונליות אופטימלית, suspend נאכפות עבור DAOs שנאספו בפרויקט KMP, מלבד DAOs ספציפיים ל-Android לשמירה על תאימות לאחור עם המודלים הקיימים ב-codebase.

הבדלים בתכונות ב-KMP

בקטע הזה מתוארים ההבדלים בתכונות בין KMP לבין פלטפורמת Android של Room.

פונקציות 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)
}

קריאה חוזרת לשאילתה

ממשקי ה-API הבאים להגדרת קריאה חוזרת של שאילתות אינם זמינים במשותף ולכן הן לא זמינות בפלטפורמות אחרות מלבד Android.

  • RoomDatabase.Builder.setQueryCallback
  • RoomDatabase.QueryCallback

אנחנו מתכוונים להוסיף תמיכה בקריאה חוזרת (callback) של שאילתה בגרסה עתידית של Room.

ה-API להגדרה של RoomDatabase עם קריאה חוזרת (callback) של שאילתה RoomDatabase.Builder.setQueryCallback וגם ממשק הקריאה החוזרת RoomDatabase.QueryCallback אינם זמינים במשותף ולכן אינם זמינים בפלטפורמות אחרות מלבד Android.

סגירה אוטומטית של מסד הנתונים

ה-API שמאפשר סגירה אוטומטית אחרי שתם הזמן הקצוב לתפוגה. RoomDatabase.Builder.setAutoCloseTimeout, זמין רק ב-Android ו לא זמין בפלטפורמות אחרות.

מסד נתונים לפני האריזה

ממשקי ה-API הבאים ליצירת RoomDatabase באמצעות מסד נתונים קיים (כלומר מסד נתונים מראש ארוז מראש) אינם זמינים במשותף ולכן אינם זמינים בפלטפורמות אחרות מלבד Android. אלו הם ממשקי ה-API:

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

אנחנו מתכוונים להוסיף תמיכה במסדי נתונים ארוזים מראש בגרסה עתידית של חדר.

ביטול תוקף של מכונות מרובות

את ה-API להפעלת ביטול תוקף של מספר מכונות, הניסוי RoomDatabase.Builder.enableMultiInstanceInvalidation זמין רק ב-YouTube Android ולא זמין בפלטפורמות משותפות או בפלטפורמות אחרות.