ห้อง (Kotlin Multiplatform)

ไลบรารีการคงข้อมูลของ Room มีเลเยอร์การแยกแยะระดับบน SQLite เพื่อให้เข้าถึงฐานข้อมูลได้มีประสิทธิภาพมากขึ้น ขณะเดียวกันก็ใช้ความสามารถของ SQLite ได้อย่างเต็มที่ หน้านี้มุ่งเน้นที่การใช้ Room ในโปรเจ็กต์ Kotlin Multiplatform (KMP) ดูข้อมูลเพิ่มเติมเกี่ยวกับการใช้ Room ได้ที่บันทึกข้อมูลในฐานข้อมูลของเครื่องโดยใช้ Room หรือตัวอย่างอย่างเป็นทางการ

การตั้งค่าทรัพยากร Dependency

เวอร์ชันปัจจุบันของ Room ที่รองรับ KMP คือ 2.7.0-alpha01 ขึ้นไป

หากต้องการตั้งค่า Room ในโปรเจ็กต์ KMP ให้เพิ่มการพึ่งพาสำหรับอาร์ติแฟกต์ในไฟล์ build.gradle.kts ของโมดูล ดังนี้

  • androidx.room:room-gradle-plugin - ปลั๊กอิน Gradle เพื่อกำหนดค่าสคีมา Room
  • androidx.room:room-compiler - โปรแกรมประมวลผล KSP ที่สร้างโค้ด
  • androidx.room:room-runtime - ส่วนรันไทม์ของไลบรารี
  • androidx.sqlite:sqlite-bundled - (ไม่บังคับ) ไลบรารี SQLite ที่รวมมา

นอกจากนี้ คุณยังต้องกำหนดค่าไดรเวอร์ SQLite ของ Room ด้วย โดยไดรเวอร์เหล่านี้จะแตกต่างกันไปตามแพลตฟอร์มเป้าหมาย ดูคำอธิบายการใช้งานไดรเวอร์ที่พร้อมใช้งานได้ที่หัวข้อการใช้งานไดรเวอร์

ดูข้อมูลการตั้งค่าเพิ่มเติมได้ที่หัวข้อต่อไปนี้

การกําหนดคลาสฐานข้อมูล

คุณต้องสร้างคลาสฐานข้อมูลที่กำกับด้วย @Database พร้อมกับ DAO และเอนทิตีภายในชุดแหล่งที่มาทั่วไปของโมดูล 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
)

โปรดทราบว่าคุณอาจใช้การประกาศจริง / คาดหวังเพื่อสร้างการใช้งาน Room สำหรับแพลตฟอร์มใดแพลตฟอร์มหนึ่งโดยเฉพาะ เช่น คุณสามารถเพิ่ม DAO สำหรับแพลตฟอร์มที่เฉพาะเจาะจงซึ่งกำหนดไว้ในโค้ดทั่วไปโดยใช้ expect จากนั้นระบุคำจำกัดความ actual ด้วยการค้นหาเพิ่มเติมในชุดแหล่งที่มาสำหรับแพลตฟอร์มที่เฉพาะเจาะจง

การสร้างเครื่องมือสร้างฐานข้อมูล

คุณต้องกำหนดเครื่องมือสร้างฐานข้อมูลเพื่อสร้างอินสแตนซ์ Room ในแต่ละแพลตฟอร์ม นี่เป็นส่วนที่เดียวของ API ที่ต้องอยู่ในชุดแหล่งที่มาเฉพาะแพลตฟอร์มเนื่องจากความแตกต่างของ File System API ตัวอย่างเช่น ใน Android ปกติแล้วตำแหน่งฐานข้อมูลจะได้รับผ่าน Context.getDatabasePath() API ส่วนใน 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 (เดสก์ท็อป)

หากต้องการสร้างอินสแตนซ์ฐานข้อมูล ให้ระบุเส้นทางฐานข้อมูลโดยใช้ 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,
    )
}

การลดขนาดและการสร้างความสับสน

หากโปรเจ็กต์ได้รับการย่อขนาดหรือสร้างความสับสน คุณต้องใส่กฎ 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 นี่คือไดรเวอร์ที่แนะนำซึ่งมี SQLite ที่คอมไพล์จากซอร์สโค้ด ซึ่งให้ SQLite เวอร์ชันล่าสุดที่สอดคล้องกันมากที่สุดในทุกแพลตฟอร์ม หากต้องการใช้ SQLite ที่ระบบปฏิบัติการให้มา ให้ใช้ setDriver API ในชุดแหล่งที่มาเฉพาะแพลตฟอร์มที่ระบุไดรเวอร์เฉพาะแพลตฟอร์ม สําหรับ 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 Room เวอร์ชัน KMP จะแตกต่างกันไปบ้างระหว่างแพลตฟอร์มต่างๆ และเวอร์ชันสำหรับ 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 ฟังก์ชันสําหรับ DAO ที่คอมไพล์ในโปรเจ็กต์ KMP เพื่อให้ฟังก์ชันทํางานได้อย่างมีประสิทธิภาพสูงสุด ยกเว้น DAO สําหรับ Android โดยเฉพาะเพื่อรักษาความเข้ากันได้ย้อนหลังกับฐานโค้ดที่มีอยู่

ความแตกต่างของฟีเจอร์กับ KMP

ส่วนนี้จะอธิบายความแตกต่างของฟีเจอร์ระหว่าง Room เวอร์ชันแพลตฟอร์ม KMP กับ Android

ฟังก์ชัน 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

เราตั้งใจที่จะเพิ่มการรองรับการเรียกกลับคําค้นหาใน Room เวอร์ชันในอนาคต

API เพื่อกําหนดค่า RoomDatabase ด้วยคอลแบ็กการค้นหา 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 เท่านั้น และไม่พร้อมใช้งานในแพลตฟอร์มทั่วไปหรือแพลตฟอร์มอื่นๆ