ספריית Room persistence מספקת שכבת הפשטה מעל 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 Plugin כדי להגדיר סכימות של Room ואת KSP plugin.
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 לבלוק root dependencies. חשוב לדעת שצריך להוסיף את כל היעדים שבהם האפליקציה משתמשת. מידע נוסף זמין במאמר KSP with 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 Gradle.
room {
schemaDirectory("$projectDir/schemas")
}
הגדרת מחלקות מסד הנתונים
צריך ליצור מחלקת מסד נתונים עם ההערה @Database, יחד עם DAOs וישויות בתוך קבוצת מקורות משותפת של מודול 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 Studio, שאפשר להשבית אותה באמצעות @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
)
יצירת builder של מסד נתונים ספציפי לפלטפורמה
צריך להגדיר כלי ליצירת מסד נתונים כדי ליצור מופע של Room בכל פלטפורמה. זהו החלק היחיד ב-API שחייב להיות במערכי מקור ספציפיים לפלטפורמה, בגלל ההבדלים ב-APIs של מערכת הקבצים.
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 (מחשב)
כדי ליצור את מופע מסד הנתונים, צריך לספק נתיב למסד הנתונים באמצעות ממשקי API של 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
בקטע הקוד הקודם מבוצעת קריאה לפונקציית ה-builder setDriver כדי להגדיר באיזה דרייבר של SQLite מסד הנתונים של Room צריך להשתמש. הדרייברים האלה שונים בהתאם לפלטפורמת היעד. בדוגמאות הקודמות השתמשנו ב-BundledSQLiteDriver.
זהו מנהל ההתקנים המומלץ שכולל את SQLite שעבר קומפילציה מהמקור, ומספק את הגרסה העדכנית והעקבית ביותר של SQLite בכל הפלטפורמות.
אם רוצים להשתמש ב-SQLite שסופק על ידי מערכת ההפעלה, צריך להשתמש ב-API 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() כדי לבצע פעולות במסד הנתונים.
מכיוון ש-executors לא תואמים ל-KMP, setQueryExecutor() API של Room לא זמין ב-commonMain. במקום זאת, צריך להגדיר את האובייקט RoomDatabase עם CoroutineContext, שאפשר להגדיר באמצעות RoomDatabase.Builder.setCoroutineContext(). אם לא מוגדר הקשר, אובייקט RoomDatabase ישתמש ב-Dispatchers.IO כברירת מחדל.
הקטנה והסתרה
אם הפרויקט עבר מיניפיקציה או טשטוש, צריך לכלול את כלל ProGuard הבא כדי שספריית Room תוכל למצוא את ההטמעה שנוצרה של הגדרת מסד הנתונים:
-keep class * extends androidx.room.RoomDatabase { <init>(); }
מעבר ל-Kotlin Multiplatform
ספריית Room פותחה במקור כספריית Android, ולאחר מכן הועברה ל-KMP עם התמקדות בתאימות ל-API. גרסת KMP של Room שונה מעט בין הפלטפורמות ומהגרסה הספציפית ל-Android. ההבדלים האלה מפורטים ומתוארים בהמשך.
מעבר מ-Support SQLite ל-SQLite Driver
כל השימושים ב-SupportSQLiteDatabase ובממשקי API אחרים ב-androidx.sqlite.db צריכים לעבור רפקטורינג עם ממשקי SQLite Driver API, כי ממשקי ה-API ב-androidx.sqlite.db הם רק ל-Android (שימו לב לחבילה השונה מחבילת KMP).
לצורך תאימות לאחור, כל עוד RoomDatabase מוגדר עם SupportSQLiteOpenHelper.Factory (לדוגמה, לא מוגדר SQLiteDriver), Room פועל ב 'מצב תאימות' שבו גם Support SQLite וגם ממשקי ה-API של 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.
המרת מחלקות משנה של מיגרציות
צריך להעביר את מחלקות המשנה של ההעברות למקבילות של דרייבר SQLite:
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) {
// …
}
}
המרת התקשרות חוזרת למסד נתונים
צריך להעביר את הקריאות החוזרות (callback) של מסד הנתונים למקבילות שלהן בדרייבר של SQLite:
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 לחסימה
החדר נהנה מהספרייה האסינכרונית 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 עם סוגי החזרה ריאקטיביים לזרימות של שגרות המשך (coroutine).
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>>
המרת ממשקי API של עסקאות
ממשקי API של טרנזקציות במסד נתונים ל-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: במצב כתיבה מראש ביומן (WAL) (ברירת מחדל), סוג העסקה הזה מקבל נעילה כשהוא מתחיל, אבל קוראים יכולים להמשיך לקרוא. זו האפשרות המועדפת ברוב המקרים.
deferredTransaction: העסקה לא תקבל נעילה עד להצהרת הכתיבה הראשונה. אפשר להשתמש בסוג הזה של טרנזקציה כאופטימיזציה כשלא בטוחים אם יהיה צורך בפעולת כתיבה בתוך הטרנזקציה. לדוגמה, אם מתחילים טרנזקציה למחיקת שירים מפלייליסט רק לפי שם הפלייליסט, והפלייליסט לא קיים, לא צריך לבצע פעולת כתיבה (מחיקה).
exclusiveTransaction: המצב הזה מתנהג בדיוק כמוimmediateTransactionבמצב WAL. במצבי רישום אחרים, היא מונעת מחיבורים אחרים למסד הנתונים לקרוא את מסד הנתונים בזמן שהטרנזקציה מתבצעת.
קריאת עסקאות
משתמשים בעסקאות קריאה כדי לקרוא מהמסד נתונים כמה פעמים באופן עקבי. לדוגמה, אם יש לכם שתי שאילתות נפרדות או יותר ואתם לא משתמשים בסעיף JOIN. אפשר להשתמש רק בעסקאות שנדחות בחיבורים של קוראים. ניסיון להתחיל עסקה מיידית או בלעדית בחיבור של קורא יגרום להקפצת חריג, כי אלה נחשבות פעולות 'כתיבה'.
val database: RoomDatabase = …
database.useReaderConnection { transactor ->
transactor.deferredTransaction {
// perform database operations in transaction
}
}
לא זמין ב-Kotlin Multiplatform
חלק מממשקי ה-API שהיו זמינים ל-Android לא זמינים ב-Kotlin Multiplatform.
שאילתת התקשרות חזרה
ממשקי ה-API הבאים להגדרת קריאות חוזרות לשאילתות לא זמינים ב-common, ולכן הם לא זמינים בפלטפורמות אחרות מלבד Android.
RoomDatabase.Builder.setQueryCallbackRoomDatabase.QueryCallback
אנחנו מתכוונים להוסיף תמיכה בהחזרת קריאה של שאילתה בגרסה עתידית של Room.
ה-API להגדרת RoomDatabase עם קריאה חוזרת לשאילתה RoomDatabase.Builder.setQueryCallback יחד עם ממשק הקריאה החוזרת RoomDatabase.QueryCallback לא זמינים ב-common, ולכן לא זמינים בפלטפורמות אחרות מלבד Android.
סגירה אוטומטית של מסד נתונים
ה-API שמאפשר סגירה אוטומטית אחרי זמן קצוב לתפוגה, RoomDatabase.Builder.setAutoCloseTimeout, זמין רק ב-Android ולא בפלטפורמות אחרות.
Pre-package Database
ממשקי ה-API הבאים ליצירת RoomDatabase באמצעות מסד נתונים קיים (כלומר, מסד נתונים מוכן מראש) לא זמינים בדרך כלל, ולכן הם לא זמינים בפלטפורמות אחרות מלבד Android. ממשקי ה-API האלה הם:
RoomDatabase.Builder.createFromAssetRoomDatabase.Builder.createFromFileRoomDatabase.Builder.createFromInputStreamRoomDatabase.PrepackagedDatabaseCallback
אנחנו מתכוונים להוסיף תמיכה במסדי נתונים מוכנים מראש בגרסה עתידית של Room.
ביטול תוקף של כמה מופעים
ממשק ה-API להפעלת ביטול תוקף של כמה מופעים במקביל, RoomDatabase.Builder.enableMultiInstanceInvalidation, זמין רק ב-Android ולא בפלטפורמות נפוצות או אחרות.
מומלץ בשבילך
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- Codelab העברת אפליקציות קיימות אל Room KMP
- Codelab תחילת העבודה עם KMP
- שמירת נתונים במסד נתונים מקומי באמצעות Room