ספריית Room persistence מספקת שכבת הפשטה מעל SQLite כדי לאפשר גישה חזקה יותר למסד הנתונים תוך ניצול מלוא העוצמה של SQLite. בדף הזה מתמקדים בשימוש ב-Room בפרויקטים של Kotlin Multiplatform (KMP). למידע נוסף על שימוש ב-Room, אפשר לעיין במאמר שמירת נתונים במסד נתונים מקומי באמצעות Room או בדוגמאות הרשמיות שלנו.
הגדרת יחסי תלות
כדי להגדיר את Room בפרויקט KMP, מוסיפים את התלות בארטיפקטים בקובץ build.gradle.kts
של מודול ה-KMP.
מגדירים את יחסי התלות בקובץ libs.versions.toml
:
[versions]
room = "2.7.2"
sqlite = "2.5.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 עם 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 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 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
)
יצירת כלי לבניית מסד נתונים ספציפי לפלטפורמה
צריך להגדיר כלי ליצירת מסד נתונים כדי ליצור מופע של 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
ב-androidMain
NativeSQLiteDriver
ב-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
כברירת מחדל.
הקטנה וערפול קוד (obfuscation)
אם הפרויקט עבר מיניפיקציה או טשטוש, צריך לכלול את כלל 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 פועל ב 'מצב תאימות' שבו גם ממשקי ה-API של SQLite וגם של SQLite Driver פועלים כמצופה. כך אפשר לבצע העברות מצטברות, ולא צריך להמיר את כל השימושים ב-Support SQLite ל-SQLite Driver בשינוי אחד.
המרת מחלקות משנה של מיגרציות
צריך להעביר מחלקות משנה של העברות למקבילות של מנהל ההתקן 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) {
// …
}
}
המרת פונקציות של @RawQuery
DAO
פונקציות עם ההערה @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 עם סוגים של ערכים מוחזרים ריאקטיביים לזרימות של קורוטינות.
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
: במצב Write-Ahead Logging (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.
קריאה חוזרת (callback) של שאילתה
ממשקי ה-API הבאים להגדרת קריאות חוזרות לשאילתות לא זמינים ב-common, ולכן הם לא זמינים בפלטפורמות אחרות מלבד Android.
RoomDatabase.Builder.setQueryCallback
RoomDatabase.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.createFromAsset
RoomDatabase.Builder.createFromFile
RoomDatabase.Builder.createFromInputStream
RoomDatabase.PrepackagedDatabaseCallback
אנחנו מתכוונים להוסיף תמיכה במסדי נתונים מוכנים מראש בגרסה עתידית של Room.
ביטול תוקף של כמה מופעים
ה-API להפעלת ביטול תוקף של כמה מופעים, RoomDatabase.Builder.enableMultiInstanceInvalidation
, זמין רק ב-Android ולא בפלטפורמות נפוצות או אחרות.
מומלץ עבורך
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- העברת אפליקציות קיימות אל Room KMP Codelab
- Get Started with KMP Codelab
- שמירת נתונים במסד נתונים מקומי באמצעות Room