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 จะช่วยให้แอปใช้ทรัพยากรน้อยลงเมื่อโหลดข้อมูล ซึ่งจะช่วยปรับปรุงประสิทธิภาพและประสบการณ์ของผู้ใช้ของแอป