การอ้างอิงข้อมูลที่ซับซ้อนโดยใช้ Room

Room สามารถแปลงประเภทข้อมูลพื้นฐานและประเภทข้อมูลแบบกล่องได้ แต่ไม่อนุญาตให้มีการอ้างอิงออบเจ็กต์ระหว่างเอนทิตี ดูวิธีใช้ตัวแปลงประเภทข้อมูลและเหตุผลที่ Room ไม่รองรับการอ้างอิงออบเจ็กต์

ใช้ตัวแปลงประเภทข้อมูล

บางครั้งคุณอาจต้องการให้แอปจัดเก็บประเภทข้อมูลที่กำหนดเองในคอลัมน์ฐานข้อมูลเดียว คุณรองรับประเภทข้อมูลที่กำหนดเองได้โดยระบุ ตัวแปลงประเภทข้อมูล ซึ่งเป็นฟังก์ชันที่บอก Room ว่าจะแปลงประเภทข้อมูลที่กำหนดเองเป็นประเภทข้อมูลที่รู้จักและแปลงกลับจากประเภทข้อมูลที่รู้จักซึ่ง Room สามารถเก็บไว้ได้อย่างไร คุณระบุตัวแปลงประเภทข้อมูลได้โดยใช้คำอธิบายประกอบ @ColumnTypeConverter

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

object Converters {
    @ColumnTypeConverter
    fun fromTimestamp(value: Long?): Date? {
        return value?.let { Date(it) }
    }

    @ColumnTypeConverter
    fun dateToTimestamp(date: Date?): Long? {
        return date?.time
    }
}

ตัวอย่างนี้กำหนดฟังก์ชันตัวแปลงประเภทข้อมูล 2 ฟังก์ชัน ได้แก่ ฟังก์ชันที่แปลงออบเจ็กต์ Date เป็นออบเจ็กต์ Long และฟังก์ชันที่แปลงออบเจ็กต์ Long กลับเป็นออบเจ็กต์ Date เนื่องจาก Room สามารถเก็บออบเจ็กต์ Long ได้ จึงใช้ตัวแปลงเหล่านี้เพื่อเก็บออบเจ็กต์ Date ได้

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

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

เมื่อกำหนดตัวแปลงประเภทข้อมูลเหล่านี้แล้ว คุณจะใช้ประเภทข้อมูลที่กำหนดเองในเอนทิตีและ DAO ได้เช่นเดียวกับที่ใช้ประเภทข้อมูลพื้นฐาน ดังนี้

@Entity
data class User(
    @PrimaryKey val id: Long,
    val name: String,
    val birthday: Date?
)

@Dao
interface UserDao {
    @Query("SELECT * FROM user WHERE birthday = :targetDate")
    suspend fun findUsersBornOnDate(targetDate: Date): List<User>
}

เนื่องจากคุณใส่คำอธิบายประกอบ AppDatabase ด้วย @ColumnTypeConverters ในตัวอย่างนี้ Room จึงใช้ตัวแปลงประเภทข้อมูลที่กำหนดได้ทุกที่ หากต้องการกำหนดขอบเขตตัวแปลงประเภทข้อมูลให้เฉพาะเอนทิตีหรือ DAO ให้ใส่คำอธิบายประกอบคลาส @Entity หรือ @Dao ด้วย @ColumnTypeConverters

ควบคุมการเริ่มต้นตัวแปลงประเภทข้อมูล

โดยปกติแล้ว Room จะสร้างอินสแตนซ์ตัวแปลงประเภทข้อมูลให้คุณ อย่างไรก็ตาม หากคุณต้องส่งการขึ้นต่อกันเพิ่มเติมไปยังคลาสตัวแปลงประเภทข้อมูล แอปของคุณต้องควบคุมการเริ่มต้นของคลาสเหล่านั้นโดยตรง ในกรณีดังกล่าว ให้ใส่คำอธิบายประกอบคลาสตัวแปลง ด้วย @ProvidedColumnTypeConverter ดังนี้

@ProvidedColumnTypeConverter
class ExampleConverter {
    @ColumnTypeConverter
    fun stringToExample(string: String?): ExampleType? {
        return string?.let { ExampleType() }
    }

    @ColumnTypeConverter
    fun exampleToString(example: ExampleType?): String? {
        return example?.toString()
    }
}

นอกจากการประกาศคลาสตัวแปลงใน @ColumnTypeConverters แล้ว ให้ใช้ ฟังก์ชัน RoomDatabase.Builder.addColumnTypeConverter เพื่อส่งอินสแตนซ์ของคลาสตัวแปลงไปยังบิลเดอร์ RoomDatabase ดังนี้

val db = Room.databaseBuilder<MyDatabase>(applicationContext, "database-name")
    .addColumnTypeConverter(exampleConverterInstance)
    .build()

ทำความเข้าใจเหตุผลที่ Room ไม่อนุญาตให้มีการอ้างอิงออบเจ็กต์

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

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

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

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

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

Text(text = book.author.name)

อย่างไรก็ตาม การเปลี่ยนแปลงที่ดูเหมือนไม่มีพิษภัยนี้ทำให้มีการค้นหาตาราง Author ในเทรดหลัก

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

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