ספריית העקביות של 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 להגדרת סכימות של Roomandroidx.room:room-compiler
– מעבד KSP שיוצר קודandroidx.room:room-runtime
– החלק של זמן הריצה בספרייהandroidx.sqlite:sqlite-bundled
– (אופציונלי) ספריית SQLite בחבילה
בנוסף, צריך להגדיר את מנהל ה-SQLite של Room. מנהלי ההתקנים האלה משתנים בהתאם לפלטפורמת היעד. בהטמעות של מנהלי התקנים מפורטות תיאורים של הטמעות של מנהלי התקנים שזמינות.
מידע נוסף על הגדרה זמין במאמרים הבאים:
- הגדרת מיקום הסכימה באמצעות הפלאגין של Room ל-Gradle.
- KSP עם Kotlin Multiplatform.
- הוספת יחסי תלות בסביבת זמן הריצה.
הגדרת הכיתות של מסד הנתונים
צריך ליצור מחלקת מסד נתונים עם הערה @Database
, יחד עם ישויות ו-DAOs בתוך קבוצת המקור המשותפת של מודול ה-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
באמצעות שאילתות נוספות בקבוצות מקור ספציפיות לפלטפורמה.
יצירת ה-builder של מסד הנתונים
כדי ליצור מופע של Room בכל פלטפורמה, צריך להגדיר בונה מסדי נתונים. זהו החלק היחיד ב-API שצריך להופיע בקבוצות מקור ספציפיות לפלטפורמה בגלל ההבדלים ב-API של מערכות קבצים. לדוגמה, ב-Android, מיקום מסד הנתונים מתקבל בדרך כלל דרך ה-API 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)
}
JVM (מחשב)
כדי ליצור את מכונה של מסד הנתונים, צריך לספק נתיב של מסד נתונים באמצעות ממשקי API של 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
מאחד מהמגדירים הספציפיים לפלטפורמה, אפשר להגדיר את שאר מסדי הנתונים של 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
. זהו ה-driver המומלץ שכולל SQLite שנוצר מהקוד המקור, ומספק את הגרסה העקבית והעדכנית ביותר של SQLite בכל הפלטפורמות. אם רוצים להשתמש ב-SQLite שסופק על ידי מערכת ההפעלה, צריך להשתמש ב-API setDriver
בקבוצות מקור ספציפיות לפלטפורמה שמציינות מנהל התקן ספציפי לפלטפורמה. ב-Android אפשר להשתמש ב-AndroidSQLiteDriver
, וב-iOS אפשר להשתמש ב-NativeSQLiteDriver
. כדי להשתמש ב-NativeSQLiteDriver
, צריך לספק אפשרות לקישור (linker) כדי שהאפליקציה ל-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 עם דגש על תאימות ל-API. גרסת 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
נאכפות ב-DAOs שעבר הידור בפרויקט KMP, מלבד DAOs ספציפיים ל-Android, כדי לשמור על תאימות לאחור עם קוד הבסיס הקיים.
הבדלים בתכונות ב-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)
}
קריאה חוזרת (callback) של שאילתות
ממשקי ה-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
אנחנו מתכננים להוסיף תמיכה במסדי נתונים מוגדרים מראש בגרסה עתידית של Room.
ביטול תוקף של כמה מופעים
ה-API להפעלת ביטול תוקף בכמה מכונות, RoomDatabase.Builder.enableMultiInstanceInvalidation
, זמין רק ב-Android ולא זמין בפלטפורמות נפוצות או בפלטפורמות אחרות.