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

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

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

โครงสร้างของ DAO

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

โค้ดต่อไปนี้เป็นตัวอย่างของ DAO ที่กำหนดฟังก์ชันสำหรับการแทรก การลบ และการเลือกออบเจ็กต์ User ในฐานข้อมูล Room

@Dao
interface UserDao {
    @Insert
    suspend fun insertAll(vararg users: User)

    @Delete
    suspend fun delete(user: User)

    @Query("SELECT * FROM user")
    suspend fun getAll(): List<User>
}

ฟังก์ชัน DAO มี 2 ประเภทที่กำหนดการโต้ตอบกับฐานข้อมูล ได้แก่

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

ส่วนต่อไปนี้จะแสดงวิธีใช้ฟังก์ชัน DAO ทั้ง 2 ประเภทเพื่อกำหนดการโต้ตอบกับฐานข้อมูลที่แอปต้องการ

ฟังก์ชันอำนวยความสะดวก

Room มีคำอธิบายประกอบอำนวยความสะดวกสำหรับการกำหนดฟังก์ชันที่ทำการแทรก อัปเดต และลบโดยไม่จำเป็นต้องเขียนคำสั่ง SQL

หากต้องการกำหนดการแทรก อัปเดต หรือลบที่ซับซ้อนมากขึ้น หรือหากคุณ ต้องการค้นหาข้อมูลในฐานข้อมูล ให้ใช้ฟังก์ชันการค้นหาแทน

แทรก

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

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

    @Insert
    suspend fun insertBothUsers(user1: User, user2: User)

    @Insert
    suspend fun insertUsersAndFriends(user: User, friends: List<User>)
}

พารามิเตอร์แต่ละรายการสำหรับฟังก์ชัน @Insert ต้องเป็นอินสแตนซ์ของคลาสเอนทิตีข้อมูล Room ที่มีคำอธิบายประกอบ @Entity หรือคอลเล็กชันของอินสแตนซ์คลาสเอนทิตีข้อมูล เมื่อมีการเรียกใช้ฟังก์ชัน @Insert Room จะแทรกอินสแตนซ์เอนทิตีแต่ละรายการที่ส่งผ่านลงในตารางฐานข้อมูลที่เกี่ยวข้อง

หากฟังก์ชัน @Insert ได้รับพารามิเตอร์เดียว ฟังก์ชันจะแสดงผลค่า Long ซึ่งเป็น rowId ใหม่สำหรับรายการที่แทรก หากพารามิเตอร์เป็นอาร์เรย์หรือคอลเล็กชัน ฟังก์ชันควรแสดงผลอาร์เรย์หรือคอลเล็กชันของค่า Long แทน โดยแต่ละค่าจะเป็น rowId สำหรับรายการที่แทรกรายการใดรายการหนึ่ง ดูข้อมูลเพิ่มเติมเกี่ยวกับการแสดงผลค่า rowId ได้ในเอกสารอ้างอิง สำหรับคำอธิบายประกอบ @Insert และเอกสารประกอบ SQLite สำหรับตาราง rowid

อัปเดต

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

@Dao
interface UserDao {
    @Update
    suspend fun updateUsers(vararg users: User)
}

Room ใช้คีย์หลักเพื่อจับคู่อินสแตนซ์เอนทิตีในอาร์กิวเมนต์กับแถวใน ฐานข้อมูล หากไม่มีแถวที่มีคีย์หลักเดียวกัน Room จะไม่ทำการเปลี่ยนแปลงใดๆ

ฟังก์ชัน @Update สามารถแสดงผลค่า Int ซึ่งระบุจำนวนแถวที่อัปเดตสำเร็จได้

ลบ

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

@Dao
interface UserDao {
    @Delete
    suspend fun deleteUsers(vararg users: User)
}

Room ใช้คีย์หลักเพื่อจับคู่อินสแตนซ์เอนทิตีในอาร์กิวเมนต์กับแถวใน ฐานข้อมูล หากไม่มีแถวที่มีคีย์หลักเดียวกัน Room จะไม่ทำการเปลี่ยนแปลงใดๆ

ฟังก์ชัน @Delete สามารถแสดงผลค่า Int ซึ่งระบุจำนวนแถวที่ลบสำเร็จได้

Upsert

คำอธิบายประกอบ @Upsert ช่วยให้คุณ กำหนดฟังก์ชันที่แทรกอินสแตนซ์เอนทิตีเมื่อไม่มีแถวที่ตรงกัน หรือ อัปเดตอินสแตนซ์เอนทิตีหากมีแถวที่มีคีย์หลักเดียวกันอยู่แล้ว

ฟังก์ชัน @Upsert จะยอมรับอินสแตนซ์เอนทิตีข้อมูลเป็นพารามิเตอร์เช่นเดียวกับฟังก์ชัน @Insert และ @Update โค้ดต่อไปนี้แสดงตัวอย่างฟังก์ชัน @Upsert ที่พยายาม upsert ออบเจ็กต์ User อย่างน้อย 1 รายการในฐานข้อมูล

@Dao
interface UserDao {
    @Upsert
    suspend fun upsertUsers(vararg users: User)
}

หากฟังก์ชัน @Upsert ได้รับพารามิเตอร์เดียว ฟังก์ชันจะแสดงผลค่า Long หากฟังก์ชันส่งผลให้มีการแทรกแถวใหม่ ฟังก์ชันจะแสดงผล rowId ของแถวที่แทรกใหม่ หากฟังก์ชันส่งผลให้มีการอัปเดตแถวที่มีอยู่ ฟังก์ชันจะแสดงผล -1 หากพารามิเตอร์เป็นอาร์เรย์หรือคอลเล็กชัน ฟังก์ชันควรแสดงผลอาร์เรย์หรือคอลเล็กชันของค่า Long แทน

ฟังก์ชันการค้นหา

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

Room จะตรวจสอบการค้นหา SQL เมื่อคอมไพล์ ซึ่งหมายความว่าหากการค้นหามีปัญหา ระบบจะแสดงข้อผิดพลาดในการคอมไพล์แทนที่จะแสดงข้อผิดพลาดขณะรันไทม์

การค้นหาแบบง่าย

โค้ดต่อไปนี้กำหนดฟังก์ชันที่ใช้การค้นหา SELECT เพื่อแสดงผลออบเจ็กต์ User ทั้งหมดในฐานข้อมูล

@Query("SELECT * FROM user")
suspend fun loadAllUsers(): List<User>

ส่วนต่อไปนี้จะแสดงวิธีแก้ไขตัวอย่างนี้สำหรับกรณีการใช้งานทั่วไป

แสดงผลคอลัมน์บางส่วนของตาราง

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

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

data class NameTuple(
    @ColumnInfo(name = "first_name") val firstName: String,
    @ColumnInfo(name = "last_name") val lastName: String
)

จากนั้นคุณจะแสดงผลออบเจ็กต์ข้อมูลดังกล่าวจากฟังก์ชันการค้นหาได้

@Query("SELECT first_name, last_name FROM user")
suspend fun loadFullName(): List<NameTuple>

เนื่องจากการค้นหาแสดงผลค่าสำหรับคอลัมน์ first_name และ last_name Room จึงแมปค่าเหล่านี้กับพร็อพเพอร์ตี้ในคลาส NameTuple หากการค้นหาแสดงผลคอลัมน์ที่ไม่ได้แมปกับพร็อพเพอร์ตี้ในออบเจ็กต์ที่แสดงผล Room จะแสดงคำเตือน

แม้ว่าตัวอย่างก่อนหน้าจะใช้คลาสข้อมูลที่กำหนดเองเพื่อดึงข้อมูลคอลัมน์บางส่วน แต่ Room ยังรองรับการแสดงผล kotlin.Pair และ kotlin.Triple เพื่อความสะดวกเมื่อการค้นหาแสดงผลคอลัมน์ 2 หรือ 3 คอลัมน์พอดี เมื่อใช้ประเภทเหล่านี้ ระบบจะแมปคอลัมน์ตามลำดับที่กำหนดไว้ในคำสั่งการค้นหา ดังนั้นลำดับของคอลัมน์ในคำสั่ง SELECT ต้องตรงกับลำดับของประเภทใน Pair หรือ Triple

ส่งพารามิเตอร์อย่างง่ายไปยังการค้นหา

ส่วนใหญ่แล้วฟังก์ชัน DAO ต้องยอมรับพารามิเตอร์เพื่อให้ฟังก์ชันดำเนินการกรองได้ Room รองรับการใช้พารามิเตอร์ฟังก์ชันเป็นพารามิเตอร์การผูกในการค้นหา

ตัวอย่างเช่น โค้ดต่อไปนี้กำหนดฟังก์ชันที่แสดงผลผู้ใช้ทั้งหมดที่มีอายุมากกว่าที่กำหนด

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

นอกจากนี้ คุณยังส่งพารามิเตอร์หลายรายการหรืออ้างอิงพารามิเตอร์เดียวกันหลายครั้งในการค้นหาได้ตามที่แสดงในโค้ดต่อไปนี้

@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
suspend fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<User>

@Query(
    """
    SELECT * FROM user
    WHERE first_name LIKE :search OR last_name LIKE :search
    """
)
suspend fun findUserWithName(search: String): List<User>

ส่งคอลเล็กชันของพารามิเตอร์ไปยังการค้นหา

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

ตัวอย่างเช่น โค้ดต่อไปนี้กำหนดฟังก์ชันที่แสดงข้อมูลเกี่ยวกับผู้ใช้ทั้งหมดจากภูมิภาคบางส่วน

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

ค้นหาตารางข้อมูลหลายรายการ

การค้นหาบางรายการอาจต้องเข้าถึงตารางข้อมูลหลายรายการเพื่อคำนวณผลลัพธ์ คุณสามารถใช้คําสั่ง JOIN ในการค้นหา SQL เพื่ออ้างอิงตารางข้อมูลมากกว่า 1 รายการ

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

@Query(
    """
    SELECT * FROM book
    INNER JOIN loan ON loan.book_id = book.id
    INNER JOIN user ON user.id = loan.user_id
    WHERE user.name LIKE :userName
    """
)
suspend fun findBooksBorrowedByName(userName: String): List<Book>

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

interface UserBookDao {
    @Query(
        """
        SELECT user.name AS userName, book.name AS bookName
        FROM user, book
        WHERE user.id = book.user_id
        """
    )
    fun loadUserAndBookNames(): Flow<List<UserBook>>
}

data class UserBook(val userName: String, val bookName: String)

แสดงผล Multimap

สำหรับการดำเนินการรวม คุณยังค้นหาคอลัมน์จากตารางข้อมูลหลายรายการได้โดยไม่ต้อง กำหนดคลาสข้อมูลเพิ่มเติมด้วยการเขียนฟังก์ชันการค้นหาที่แสดงผล Multimap

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

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

เมื่อฟังก์ชันการค้นหาแสดงผล Multimap คุณจะเขียนการค้นหาที่ใช้คําสั่ง GROUP BY ได้ ซึ่งจะช่วยให้คุณใช้ประโยชน์จากความสามารถของ SQL ในการคำนวณและการกรองขั้นสูงได้ ตัวอย่างเช่น คุณสามารถแก้ไขฟังก์ชัน loadUserAndBookNames เพื่อแสดงผลเฉพาะผู้ใช้ที่ยืมหนังสือ 3 เล่มขึ้นไป

@Query(
    """
    SELECT * FROM user
    JOIN book ON user.id = book.user_id
    GROUP BY user.name HAVING COUNT(book.id) >= 3
    """
)
suspend fun loadUserAndBookNamesGrouped(): Map<User, List<Book>>

หากไม่จำเป็นต้องแมปออบเจ็กต์ทั้งหมด คุณยังแสดงผลการแมประหว่าง คอลัมน์ที่เฉพาะเจาะจงในการค้นหาได้ด้วยการใช้คำอธิบายประกอบ @MapColumn ใน พารามิเตอร์ทั่วไปของประเภทการแสดงผล

@Query(
    """
    SELECT user.name AS username, book.name AS bookname FROM user
    JOIN book ON user.id = book.user_id
    """
)
suspend fun loadUserAndBookNamesColumns(): Map<
    @MapColumn(columnName = "username") String,
    List<@MapColumn(columnName = "bookname") String>
    >

ประเภทการแสดงผลพิเศษ

Room มีประเภทการแสดงผลพิเศษบางประเภทสำหรับการผสานรวมกับไลบรารี API อื่นๆ

การค้นหาแบบแบ่งหน้าด้วยไลบรารี Paging

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

  1. รวมอาร์ติแฟกต์ androidx.room3:room3-paging ไว้ในการกำหนดค่าบิลด์
  2. ใส่คำอธิบายประกอบ @Database หรือ @Dao Declaration ด้วย @DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)

เมื่อลงทะเบียนแล้ว DAO จะแสดงผลออบเจ็กต์ PagingSource เพื่อใช้กับ Paging 3 ได้

@Dao
@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)
interface UserDao {
    @Query("SELECT * FROM users WHERE label LIKE :query")
    fun pagingSource(query: String): PagingSource<Int, User>
}

ดูข้อมูลเพิ่มเติมเกี่ยวกับการเลือกพารามิเตอร์ประเภทสำหรับ PagingSource ได้ที่ หัวข้อเลือกประเภทคีย์และค่า

สิทธิ์เข้าถึงการเชื่อมต่อฐานข้อมูลโดยตรง

หากตรรกะของแอปต้องเข้าถึงการเชื่อมต่อฐานข้อมูลโดยตรงในระดับต่ำ คุณสามารถใช้ API การเชื่อมต่อของ Room แทนได้ คุณสามารถรับการเชื่อมต่อได้โดยใช้useReaderConnectionสำหรับการดำเนินการแบบอ่านอย่างเดียว หรือuseWriterConnectionสำหรับการดำเนินการเขียนในอินสแตนซ์ RoomDatabase และใช้usePreparedเพื่อเรียกใช้คำสั่ง

val result: List<Pair<Long, String>> =
    roomDatabase.useReaderConnection { connection ->
        connection.usePrepared(
            "SELECT * FROM user WHERE age > :minAge LIMIT 5"
        ) { stmt ->
            // Bind arguments if needed
            stmt.bindLong(1, minAge.toLong())
            buildList {
                // Step through the results
                while (stmt.step()) {
                    add(stmt.getLong(0) to stmt.getText(1))
                }
            }
        }
    }

หากต้องการทำธุรกรรมฐานข้อมูลระดับต่ำในการเชื่อมต่อโดยตรง คุณสามารถใช้ฟังก์ชัน Helper immediateTransaction, deferredTransaction หรือ exclusiveTransaction ในอินสแตนซ์ Transactor ภายในบล็อก useWriterConnection

roomDatabase.useWriterConnection { transactor ->
    transactor.immediateTransaction {
        // Perform transactional database operations using transactor
    }
}

หรือหากต้องการเรียกใช้การดำเนินการ DAO ระดับสูงในธุรกรรม ให้ใช้ฟังก์ชันส่วนขยาย Helper withReadTransaction หรือ withWriteTransaction ในอินสแตนซ์ RoomDatabase

// Perform transactional read operations (DEFERRED transaction)
val userCount = roomDatabase.withReadTransaction {
    userDao.countUsers()
}

// Perform transactional write operations (IMMEDIATE transaction)
roomDatabase.withWriteTransaction {
    userDao.insert(newUser)
    userDao.update(existingUser)
}