دسترسی به داده‌ها با استفاده از Room DAOها

وقتی از کتابخانه‌ی پایداری Room برای ذخیره‌ی داده‌های برنامه‌ی خود استفاده می‌کنید، با تعریف اشیاء دسترسی به داده یا DAOها با داده‌های ذخیره‌شده تعامل می‌کنید. هر DAO شامل توابعی است که دسترسی انتزاعی به پایگاه داده‌ی برنامه‌ی شما را ارائه می‌دهند. در زمان کامپایل، Room به‌طور خودکار پیاده‌سازی‌هایی از DAOهایی که تعریف می‌کنید را تولید می‌کند.

با استفاده از DAOها برای دسترسی به پایگاه داده برنامه خود به جای سازندگان پرس و جو یا پرس و جوهای مستقیم، می‌توانید جداسازی دغدغه‌ها ، یک اصل معماری حیاتی را حفظ کنید. DAOها همچنین به شما امکان می‌دهند هنگام آزمایش برنامه خود، دسترسی به پایگاه داده را شبیه‌سازی کنید.

آناتومی یک DAO

شما می‌توانید هر DAO را به عنوان یک رابط یا یک کلاس انتزاعی تعریف کنید. برای موارد استفاده اولیه، معمولاً از یک رابط استفاده می‌کنید. در هر صورت، همیشه باید DAO های خود را با @Dao حاشیه‌نویسی کنید. DAO ها خاصیت ندارند، اما یک یا چند تابع برای تعامل با داده‌های موجود در پایگاه داده برنامه شما تعریف می‌کنند.

کد زیر نمونه‌ای از یک 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 وجود دارد که تعاملات پایگاه داده را تعریف می‌کنند:

  • توابع راحتی که به شما امکان می‌دهند بدون نوشتن هیچ کد SQL، سطرها را در پایگاه داده خود وارد، به‌روزرسانی و حذف کنید.
  • توابع پرس‌وجو که به شما امکان می‌دهند پرس‌وجوی SQL خود را برای تعامل با پایگاه داده بنویسید.

بخش‌های بعدی نحوه‌ی استفاده از هر دو نوع توابع DAO را برای تعریف تعاملات پایگاه داده‌ای که برنامه‌ی شما به آن نیاز دارد، نشان می‌دهند.

عملکردهای راحتی

روم حاشیه‌نویسی‌های راحتی را برای تعریف توابعی فراهم می‌کند که عملیات درج، به‌روزرسانی و حذف را بدون نیاز به نوشتن دستور SQL انجام می‌دهند.

اگر نیاز به تعریف عملیات پیچیده‌تر درج، به‌روزرسانی یا حذف دارید، یا اگر نیاز به پرس‌وجو از داده‌ها در پایگاه داده دارید، به جای آن از یک تابع پرس‌وجو استفاده کنید.

درج

حاشیه‌نویسی @Insert به شما امکان می‌دهد توابعی را تعریف کنید که پارامترهای خود را در جدول مناسب در پایگاه داده وارد می‌کنند. کد زیر نمونه‌هایی از توابع معتبر @Insert را نشان می‌دهد که یک یا چند شیء User را در پایگاه داده وارد می‌کنند:

@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 به شما امکان می‌دهد توابعی را تعریف کنید که ردیف‌های خاصی را در یک جدول پایگاه داده به‌روزرسانی می‌کنند. مانند توابع @Insert ، توابع @Update نمونه‌های موجودیت داده را به عنوان پارامتر می‌پذیرند. کد زیر نمونه‌ای از یک تابع @Update را نشان می‌دهد که سعی در به‌روزرسانی یک یا چند شیء User در پایگاه داده دارد:

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

Room از کلید اصلی برای تطبیق نمونه‌های موجودیت در آرگومان‌ها با ردیف‌های پایگاه داده استفاده می‌کند. اگر هیچ ردیفی با کلید اصلی یکسان وجود نداشته باشد، Room هیچ تغییری ایجاد نمی‌کند.

یک تابع @Update می‌تواند به صورت اختیاری یک مقدار Int را برگرداند که نشان دهنده تعداد ردیف‌هایی است که با موفقیت به‌روزرسانی شده‌اند.

حذف

حاشیه‌نویسی @Delete به شما امکان می‌دهد توابعی را تعریف کنید که ردیف‌های خاصی را از یک جدول پایگاه داده حذف می‌کنند. مانند توابع @Insert ، توابع @Delete نمونه‌های موجودیت داده را به عنوان پارامتر می‌پذیرند. کد زیر نمونه‌ای از یک تابع @Delete را نشان می‌دهد که سعی در حذف یک یا چند شیء User از پایگاه داده دارد:

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

Room از کلید اصلی برای تطبیق نمونه‌های موجودیت در آرگومان‌ها با ردیف‌های پایگاه داده استفاده می‌کند. اگر هیچ ردیفی با کلید اصلی یکسان وجود نداشته باشد، Room هیچ تغییری ایجاد نمی‌کند.

یک تابع @Delete می‌تواند به صورت اختیاری یک مقدار Int را برگرداند که نشان دهنده تعداد ردیف‌هایی است که با موفقیت حذف شده‌اند.

آپسرت

حاشیه‌نویسی @Upsert به شما امکان می‌دهد توابعی تعریف کنید که نمونه‌های موجودیت را در صورت عدم وجود ردیف منطبق، درج کنند یا اگر ردیفی با کلید اصلی یکسان از قبل وجود داشته باشد، آنها را به‌روزرسانی کنند.

مانند توابع @Insert و @Update ، توابع @Upsert نمونه‌هایی از موجودیت داده را به عنوان پارامتر می‌پذیرند. کد زیر نمونه‌ای از یک تابع @Upsert را نشان می‌دهد که سعی می‌کند یک یا چند شیء User را در پایگاه داده upsert کند:

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

اگر تابع @Upsert یک پارامتر واحد دریافت کند، می‌تواند یک مقدار Long را برگرداند. اگر منجر به درج یک ردیف جدید شود، rowId ردیف تازه درج شده را برمی‌گرداند. اگر منجر به به‌روزرسانی یک ردیف موجود شود، -1 را برمی‌گرداند. اگر پارامتر یک آرایه یا یک مجموعه باشد، باید به جای آن یک آرایه یا مجموعه‌ای از مقادیر Long را برگرداند.

توابع پرس و جو

حاشیه‌نویسی @Query به شما امکان می‌دهد دستورات SQL بنویسید و آنها را به عنوان توابع DAO نمایش دهید. از این توابع پرس‌وجو برای پرس‌وجوی داده‌ها از پایگاه داده برنامه خود یا زمانی که نیاز به انجام عملیات پیچیده‌تر درج، به‌روزرسانی و حذف دارید، استفاده کنید.

روم کوئری‌های SQL را در زمان کامپایل اعتبارسنجی می‌کند. این بدان معناست که اگر مشکلی در کوئری شما وجود داشته باشد، به جای خرابی در زمان اجرا، یک خطای کامپایل رخ می‌دهد.

پرس‌وجوهای ساده

کد زیر تابعی را تعریف می‌کند که از یک پرس‌وجوی SELECT برای بازگرداندن تمام اشیاء User در پایگاه داده استفاده می‌کند:

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

بخش‌های بعدی نحوه‌ی اصلاح این مثال برای موارد استفاده‌ی معمول را نشان می‌دهند.

زیرمجموعه‌ای از ستون‌های یک جدول را برمی‌گرداند

اغلب اوقات، شما فقط نیاز دارید زیرمجموعه‌ای از ستون‌های جدولی که در حال پرس‌وجو هستید را برگردانید. برای مثال، رابط کاربری شما ممکن است فقط نام و نام خانوادگی یک کاربر را به جای تمام جزئیات مربوط به آن کاربر نمایش دهد. برای صرفه‌جویی در منابع و ساده‌سازی اجرای پرس‌وجو، فقط ویژگی‌هایی را که نیاز دارید پرس‌وجو کنید.

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 برای راحتی در مواقعی که یک پرس‌وجو دقیقاً دو یا سه ستون را برمی‌گرداند، پشتیبانی می‌کند. هنگام استفاده از این نوع‌ها، ستون‌ها به ترتیبی که در دستور پرس‌وجو تعریف شده‌اند، نگاشت می‌شوند، بنابراین ترتیب ستون‌ها در دستور 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 خود برای ارجاع به بیش از یک جدول استفاده کنید.

کد زیر تابعی را تعریف می‌کند که سه جدول را به هم متصل می‌کند تا کتاب‌هایی را که در حال حاضر به یک کاربر خاص امانت داده شده‌اند، برگرداند:

@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 برمی‌گردانند، پرس‌وجو کنید.

مثال مربوط به Query multiple tables را در نظر بگیرید. به جای برگرداندن لیستی از نمونه‌های یک کلاس داده سفارشی که جفت نمونه‌های User و Book را در خود نگه می‌دارد، می‌توانید نگاشتی از User و Book را مستقیماً از تابع query خود برگردانید:

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

وقتی تابع کوئری شما یک multimap برمی‌گرداند، می‌توانید کوئری‌هایی بنویسید که از عبارت‌های GROUP BY استفاده می‌کنند و به شما امکان می‌دهند از قابلیت‌های SQL برای محاسبات پیشرفته و فیلتر کردن بهره ببرید. برای مثال، می‌توانید تابع loadUserAndBookNames خود را طوری تغییر دهید که فقط کاربرانی را برگرداند که سه یا چند کتاب را بررسی کرده‌اند:

@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

روم از طریق ادغام با کتابخانه Paging از کوئری‌های صفحه‌بندی‌شده پشتیبانی می‌کند. برای استفاده از انواع بازگشتی Paging 3، باید مبدل‌های نوع بازگشتی Paging را در پایگاه داده یا DAO خود ثبت کنید:

  1. فایل androidx.room3:room3-paging را در پیکربندی ساخت خود وارد کنید.
  2. اعلان @Database یا @Dao خود را با @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))
                }
            }
        }
    }

اگر نیاز دارید تراکنش‌های سطح پایین پایگاه داده را مستقیماً روی اتصال انجام دهید، می‌توانید از توابع کمکی immediateTransaction ، deferredTransaction یا exclusiveTransaction روی یک نمونه Transactor درون بلوک useWriterConnection استفاده کنید:

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

از طرف دیگر، اگر فقط نیاز به اجرای عملیات سطح بالای DAO در یک تراکنش دارید، از توابع افزونه کمکی 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)
}