บันทึกข้อมูลในฐานข้อมูลของเครื่องโดยใช้ Room 2.x

หากแอปจัดการข้อมูลที่มีโครงสร้างจำนวนมาก คุณจะได้รับประโยชน์อย่างมากจากการเก็บข้อมูลดังกล่าวไว้ในเครื่อง กรณีการใช้งานที่พบบ่อยที่สุดคือการแคชข้อมูลที่เกี่ยวข้องเพื่อให้คุณยังคงเรียกดูเนื้อหาได้แม้ว่าอุปกรณ์จะเข้าถึงเครือข่ายไม่ได้ก็ตาม

ไลบรารี Room Persistence มีเลเยอร์การแยกส่วนเหนือ SQLite เพื่อให้คุณเข้าถึงฐานข้อมูลได้อย่างราบรื่นพร้อมกับใช้ประโยชน์จากความสามารถทั้งหมดของ SQLite

ตั้งค่า Room 2.x

หากต้องการใช้ Room 2.x ในแอป ให้เพิ่มทรัพยากร Dependency ต่อไปนี้ลงในไฟล์ build.gradle ของแอป

dependencies {
    val room_version = "2.6.1"

    implementation("androidx.room:room-runtime:$room_version")
    annotationProcessor("androidx.room:room-compiler:$room_version")

    // To use Kotlin Symbol Processing (KSP)
    // ksp("androidx.room:room-compiler:$room_version")

    // optional - Kotlin Extensions and Coroutines support for Room
    implementation("androidx.room:room-ktx:$room_version")

    // optional - RxJava2 support for Room
    implementation("androidx.room:room-rxjava2:$room_version")

    // optional - Guava support for Room, including Optional and ListenableFuture
    implementation("androidx.room:room-guava:$room_version")

    // optional - Test helpers
    testImplementation("androidx.room:room-testing:$room_version")
}

คอมโพเนนต์หลัก

Room มีคอมโพเนนต์หลัก 3 อย่าง ได้แก่

  • คลาสฐานข้อมูล ที่เก็บฐานข้อมูลและทำหน้าที่เป็นจุดเข้าถึงหลักสำหรับการเชื่อมต่อพื้นฐานกับข้อมูลที่เก็บไว้ของแอป
  • เอนทิตีข้อมูล ที่แสดงตารางในฐานข้อมูลของแอป
  • ออบเจ็กต์การเข้าถึงข้อมูล (DAO) ที่มีเมธอดที่แอปใช้เพื่อค้นหา อัปเดต แทรก และลบข้อมูลในฐานข้อมูล

รูปที่ 1 แสดงความสัมพันธ์ระหว่างคอมโพเนนต์ต่างๆ ของ Room

รูปที่ 1 แผนภาพสถาปัตยกรรมไลบรารี Room

ตัวอย่างการใช้งาน

// Entity
@Entity
data class User(
    @PrimaryKey val uid: Int,
    @ColumnInfo(name = "first_name") val firstName: String?,
    @ColumnInfo(name = "last_name") val lastName: String?
)

// DAO
@Dao
interface UserDao {
    @Query("SELECT * FROM user")
    fun getAll(): List<User>

    @Insert
    fun insertAll(vararg users: User)

    @Delete
    fun delete(user: User)
}

// Database
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

// Usage
val db = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java, "database-name"
        ).build()

val userDao = db.userDao()
val users: List<User> = userDao.getAll()

กำหนดข้อมูลโดยใช้เอนทิตี

เอนทิตีแต่ละรายการของ Room แสดงตารางในฐานข้อมูล คุณกำหนดเอนทิตีแต่ละรายการเป็น คลาสที่มีคำอธิบายประกอบ @Entity

@Entity(tableName = "users")
data class User (
    @PrimaryKey val id: Int,
    @ColumnInfo(name = "first_name") val firstName: String?,
    @ColumnInfo(name = "last_name") val lastName: String?,
    @Ignore val picture: Bitmap? = null
)
  • ชื่อตารางและชื่อคอลัมน์ที่กำหนดเอง: โดยค่าเริ่มต้น Room จะใช้ชื่อคลาสเป็น ชื่อตารางและชื่อพร็อพเพอร์ตี้เป็นชื่อคอลัมน์ หากต้องการปรับแต่ง ให้ใช้ พร็อพเพอร์ตี้ tableName ใน @Entity และคำอธิบายประกอบ @ColumnInfo(name = "...")
  • แป้นหลัก: หากต้องการกำหนดแป้นหลัก ให้ใช้ @PrimaryKey สำหรับ แป้นผสม ให้ใช้พร็อพเพอร์ตี้ primaryKeys ของ @Entity: @Entity(primaryKeys = ["firstName", "lastName"])
  • ละเว้นฟิลด์: หากไม่ต้องการให้มีการเก็บฟิลด์ไว้ ให้ใช้ @Ignore

ตัวแปลงประเภท

บางครั้งคุณอาจต้องจัดเก็บประเภทที่กำหนดเอง เช่น Date ไว้ในคอลัมน์เดียว ให้ @TypeConverter เมธอดเพื่อแปลงประเภทที่กำหนดเองเป็นประเภทที่ Room จัดเก็บได้และแปลงกลับ

class Converters {
  @TypeConverter
  fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }

  @TypeConverter
  fun dateToTimestamp(date: Date?): Long? = date?.time
}

// Register in your Database class
@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { ... }

เข้าถึงข้อมูลโดยใช้ DAO

DAO กำหนดเมธอดสำหรับการโต้ตอบกับฐานข้อมูล ใส่คำอธิบายประกอบ @Dao ในอินเทอร์เฟซหรือคลาสแอบสแตรกต์

เมธอดอำนวยความสะดวก

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertUsers(vararg users: User)

    @Update
    fun updateUsers(vararg users: User)

    @Delete
    fun deleteUsers(vararg users: User)
}
  • แทรก: เมธอดแทรกสามารถแสดงผล Long ที่แสดงรหัสแถวที่แทรก หรือ List<Long> ที่มีรหัสของแถวที่แทรกทั้งหมด
  • อัปเดตหรือลบ: เมธอดอัปเดตหรือลบสามารถแสดงผล Int ที่แสดงจำนวนแถวที่ได้รับผลกระทบ

เมธอดค้นหา

ใส่คำอธิบายประกอบ @Query ในเมธอดเพื่อเขียนคำสั่ง SQL Room จะตรวจสอบการค้นหาในเวลาคอมไพล์

@Dao
interface UserDao {
    // Simple query
    @Query("SELECT * FROM user")
    fun loadAllUsers(): Array<User>

    // Return a subset of columns using a POJO or tuple
    @Query("SELECT first_name, last_name FROM user")
    fun loadFullName(): List<NameTuple>

    // Pass parameters
    @Query("SELECT * FROM user WHERE age > :minAge")
    fun loadAllUsersOlderThan(minAge: Int): Array<User>

    // Collection of parameters
    @Query("SELECT * FROM user WHERE region IN (:regions)")
    fun loadUsersFromRegions(regions: List<String>): List<User>

    // Join tables
    @Query("SELECT * FROM book INNER JOIN user ON user.id = book.user_id WHERE user.name = :userName")
    fun findBooksBorrowedByName(userName: String): List<Book>
}

ประเภทการแสดงผลแบบหลายค่าต่อคีย์

ใน Room 2.4 ขึ้นไป เมธอดค้นหาสามารถแสดงผลแบบหลายค่าต่อคีย์ได้โดยตรงโดยใช้ประเภท Map ดังนี้

@Query("SELECT * FROM user JOIN book ON user.id = book.user_id")
fun loadUserAndBookNames(): Map<User, List<Book>>

การค้นหา DAO แบบอะซิงโครนัส

การค้นหาฐานข้อมูลไม่สามารถทำงานในเธรดหลักได้เพื่อหลีกเลี่ยงไม่ให้ UI ค้าง ทำให้การค้นหาเป็นแบบอะซิงโครนัสโดยใช้การผสานรวมอย่างใดอย่างหนึ่งต่อไปนี้

โครูทีนและโฟลว์ Kotlin

ต้องมีทรัพยากร Dependency room-ktx

@Dao
interface UserDao {
    // One-shot async query
    @Insert
    suspend fun insertUsers(vararg users: User)

    // Observable query using Flow
    @Query("SELECT * FROM user WHERE id = :id")
    fun loadUserById(id: Int): Flow<User>
}

Java กับ RxJava

ต้องมี room-rxjava2 หรือ room-rxjava3

@Dao
interface UserDao {
    @Insert
    fun insertUsers(users: List<User>): Completable

    @Query("SELECT * FROM user WHERE id = :id")
    fun loadUserById(id: Int): Flowable<User>
}

Java กับ LiveData และ Guava

ต้องมี room-guava สำหรับ ListenableFuture

@Dao
interface UserDao {
    // LiveData for observable queries
    @Query("SELECT * FROM user WHERE id = :id")
    fun loadUserById(id: Int): LiveData<User>

    // Guava ListenableFuture for one-shot queries
    @Insert
    fun insertUsers(users: List<User>): ListenableFuture<Integer>
}

กำหนดความสัมพันธ์ใน Room 2.x

คุณไม่สามารถใช้การอ้างอิงออบเจ็กต์โดยตรงระหว่างเอนทิตีได้เพื่อป้องกันการโหลดแบบ Lazy Loading ในเธรด UI แต่ให้กำหนดความสัมพันธ์โดยใช้คลาสข้อมูลระดับกลางที่มี @Relation

หนึ่งต่อหนึ่ง

ผู้ใช้แต่ละรายมีไลบรารีเพียงรายการเดียว

@Entity
data class User(@PrimaryKey val userId: Long, val name: String)

@Entity
data class Library(@PrimaryKey val libraryId: Long, val userOwnerId: Long)

// Intermediate class
data class UserAndLibrary(
    @Embedded val user: User,
    @Relation(
         parentColumn = "userId",
         entityColumn = "userOwnerId"
    )
    val library: Library
)

// DAO Query
@Transaction
@Query("SELECT * FROM User")
fun getUsersAndLibraries(): List<UserAndLibrary>

หนึ่งต่อหลาย

ผู้ใช้แต่ละรายมีเพลย์ลิสต์ได้หลายรายการ

@Entity
data class Playlist(@PrimaryKey val playlistId: Long, val userCreatorId: Long)

data class UserWithPlaylists(
    @Embedded val user: User,
    @Relation(
          parentColumn = "userId",
          entityColumn = "userCreatorId"
    )
    val playlists: List<Playlist>
)

หลายต่อหลาย

เพลย์ลิสต์มีเพลงได้หลายเพลง และเพลงอยู่ในเพลย์ลิสต์ได้หลายรายการ ต้องมีตาราง Junction

@Entity
data class Song(@PrimaryKey val songId: Long, val songName: String)

@Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(val playlistId: Long, val songId: Long)

data class PlaylistWithSongs(
    @Embedded val playlist: Playlist,
    @Relation(
         parentColumn = "playlistId",
         entityColumn = "songId",
         associateBy = Junction(PlaylistSongCrossRef::class)
    )
    val songs: List<Song>
)

ความสัมพันธ์ที่ซ้อนกัน

ค้นหาผู้ใช้ เพลย์ลิสต์ของผู้ใช้ และเพลงทั้งหมดในเพลย์ลิสต์เหล่านั้น

data class UserWithPlaylistsAndSongs(
      @Embedded val user: User,
      @Relation(
          entity = Playlist::class,
          parentColumn = "userId",
          entityColumn = "userCreatorId"
      )
      val playlists: List<PlaylistWithSongs> // Nesting PlaylistWithSongs
  )

การจัดการฐานข้อมูล

ส่วนนี้ครอบคลุมแง่มุมต่างๆ ของการจัดการฐานข้อมูล Room รวมถึงมุมมองฐานข้อมูล การป้อนข้อมูลล่วงหน้า และการย้ายข้อมูลฐานข้อมูล

มุมมองฐานข้อมูล

ห่อหุ้มการค้นหาที่ซับซ้อนไว้ในคลาสที่มีคำอธิบายประกอบ @DatabaseView

@DatabaseView("SELECT user.id, user.name, department.name AS departmentName FROM user INNER JOIN department ON user.departmentId = department.id")
data class UserDetail(val id: Long, val name: String, val departmentName: String)

// Register in Database class
@Database(entities = [User::class], views = [UserDetail::class], version = 1)
abstract class AppDatabase : RoomDatabase() { ... }

ป้อนข้อมูลล่วงหน้าในฐานข้อมูล

ป้อนข้อมูลในฐานข้อมูลเมื่อเริ่มต้นจากไฟล์ชิ้นงานหรือระบบไฟล์

Room.databaseBuilder(appContext, AppDatabase::class.java, "Sample.db")
    .createFromAsset("database/myapp.db")
    .build()

การย้ายข้อมูล

เมื่อคุณเปลี่ยนสคีมา ให้เพิ่มเวอร์ชันฐานข้อมูลและกำหนดออบเจ็กต์ Migration

val MIGRATION_1_2 = object : Migration(1, 2) {
  override fun migrate(database: SupportSQLiteDatabase) {
    database.execSQL("ALTER TABLE User ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
  }
}

Room.databaseBuilder(applicationContext, AppDatabase::class.java, "database-name")
  .addMigrations(MIGRATION_1_2)
  .build()
  • การย้ายข้อมูลอัตโนมัติ: หากคุณใช้ Room 2.4.0 ขึ้นไป คุณสามารถใช้ @AutoMigration เพื่อย้ายข้อมูลการเปลี่ยนแปลงสคีมาพื้นฐานโดยอัตโนมัติ ซึ่งกำหนดให้คุณต้องตั้งค่า exportSchema เป็น true ในการกำหนดค่าฐานข้อมูล: @Database(version = 2, autoMigrations = [AutoMigration(from = 1, to = 2)])
  • การย้อนกลับแบบทำลายล้าง: หากยอมรับได้ที่จะสูญเสียข้อมูลเมื่อไม่มีเส้นทางการย้ายข้อมูล ให้เรียก .fallbackToDestructiveMigration เมื่อสร้าง ฐานข้อมูล

ทดสอบการย้ายข้อมูล

หากต้องการยืนยันการย้ายข้อมูล ให้ใช้ MigrationTestHelper จากอาร์ติแฟกต์ room-testing หากต้องการรองรับการดำเนินการนี้ ให้ตรวจสอบว่าคุณได้ส่งออกสคีมาในการกำหนดค่า build.gradle แล้ว

@RunWith(AndroidJUnit4::class)
class MigrationTest {
    @get:Rule
    val helper: MigrationTestHelper = MigrationTestHelper(
            InstrumentationRegistry.getInstrumentation(),
            AppDatabase::class.java.canonicalName,
            FrameworkSQLiteOpenHelperFactory()
    )

    @Test
    fun migrate1To2() {
        var db = helper.createDatabase("test-db", 1).apply {
            execSQL("INSERT INTO User VALUES (1, 'John')")
            close()
        }
        db = helper.runMigrationsAndValidate("test-db", 2, true, MIGRATION_1_2)
        // Verify data was migrated correctly
    }
}

ย้ายข้อมูลจาก SQLite ไปยัง Room

หากต้องการย้ายข้อมูลแอปจาก SQLite ไปยัง Room ให้ทำตามขั้นตอนต่อไปนี้

  1. อัปเดตทรัพยากร Dependency เพื่อรวม Room
  2. ใส่คำอธิบายประกอบในคลาสโมเดล ด้วย @Entity, @PrimaryKey และ @ColumnInfo
  3. สร้าง DAO เพื่อแทนที่เมธอดค้นหาตัวช่วย
  4. สร้างคลาส RoomDatabase ที่อ้างอิงเอนทิตีและ DAO เพิ่มหมายเลขเวอร์ชัน
  5. กำหนดเส้นทางการย้ายข้อมูลที่ว่างเปล่า เนื่องจากสคีมาไม่เปลี่ยนแปลง แต่ เฟรมเวิร์ก kotlin val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) {} }
  6. อัปเดตการสร้างอินสแตนซ์ เพื่อใช้ Room.databaseBuilder กับเส้นทางการย้ายข้อมูล