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

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

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

تشریح سازمان خودگردان غیرمتمرکز

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

عملکردهای تسهیل‌کننده

‫Room برای تعریف توابعی که درج، به‌روزرسانی، و حذف را انجام می‌دهند، بدون اینکه نیاز باشد بیانیه 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

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

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

@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>

بخش‌های زیر نشان می‌دهد که چگونه این مثال را برای موارد استفاده معمول تغییر دهید.

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

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

‫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 شما باید پارامترها را بپذیرند تا بتوانند عملیات فیلتر کردن را انجام دهند. «اتاق» از استفاده از پارامترهای تابع به‌عنوان پارامترهای پیوند در پُرسمان‌هایتان پشتیبانی می‌کند.

برای مثال، کد زیر تابعی را تعریف می‌کند که همه کاربران بالای سن معینی را برمی‌گرداند:

@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)

برگرداندن چندنقشه

برای عملیات پیوستن، می‌توانید ستون‌های چند جدول را بدون تعریف کردن کلاس داده اضافی با نوشتن توابع پُرسمان که چندنقشه برمی‌گردانند نیز پُرسمان کنید.

مثال پُرسمان چند جدول را درنظر بگیرید. به‌جای برگرداندن فهرست نمونه‌های کلاس داده سفارشی که جفت‌های نمونه‌های User و Book را دربرمی‌گیرد، می‌توانید مستقیماً از تابع پُرسمان خودتان نگاشت User و Book را برگردانید:

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

وقتی تابع پُرسمان شما یک چندنقشه برمی‌گرداند، می‌توانید پُرسمان‌هایی بنویسید که از بندهای 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 ارائه می‌دهد.

پُرسمان‌های صفحه‌بندی‌شده با کتابخانه «صفحه‌بندی»

‫Room از پُرسمان‌های صفحه‌بندی‌شده ازطریق یکپارچه‌سازی با کتابخانه صفحه‌بندی پشتیبانی می‌کند. برای استفاده از ۳ نوع برگشتی «صفحه‌بندی»، باید مبدل‌های نوع برگشتی «صفحه‌بندی» را در پایگاه داده یا DAO ثبت کنید:

  1. عنصر androidx.room3:room3-paging را در پیکربندی ساخت خود بگنجانید.
  2. اعلان @Database یا @Dao خود را با @DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class) حاشیه‌نویسی کنید.

پس‌از ثبت، «سازمان‌های خودگردان غیرمتمرکز» شما می‌توانند اشیای PagingSource را برای استفاده با صفحه‌بندی ۳ برگردانند:

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

برای اطلاعات بیشتر درباره انتخاب انواع پارامتر برای PagingSource، به انتخاب انواع کلید و مقدار مراجعه کنید.

دسترسی به اتصال مستقیم پایگاه داده

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