كتابة طلبات بحث DAO غير المتزامنة

لمنع الطلبات من حظر واجهة المستخدم، لا يتيح Room الوصول إلى قاعدة البيانات على سلسلة التعليمات الرئيسية. يعني هذا القيد أنّه يجب أن تكون طلبات بحث DAO غير متزامنة. تتضمّن مكتبة Room عمليات دمج مع العديد من الأُطر لتوفير تنفيذ غير متزامن للاستعلامات.

تندرج طلبات البحث في DAO ضمن ثلاث فئات:

  • استعلامات الكتابة لمرة واحدة التي تُدرج البيانات أو تعدّلها أو تحذفها في قاعدة البيانات
  • استعلامات القراءة لمرة واحدة التي تقرأ البيانات من قاعدة البيانات مرة واحدة فقط وتعرض نتيجة تتضمّن لقطة من قاعدة البيانات في ذلك الوقت
  • طلبات البحث القابلة للمراقبة التي تقرأ البيانات من قاعدة البيانات في كل مرة تتغير فيها جداول قاعدة البيانات الأساسية، وتُصدر قيمًا جديدة لتعكس هذه التغييرات.

خيارات اللغة وإطار العمل

توفّر Room إمكانية الدمج لتحقيق التشغيل التفاعلي مع ميزات ومكتبات لغات معيّنة. يعرض الجدول التالي أنواع القيم المرجَعة السارية استنادًا إلى نوع الطلب وإطاره:

نوع طلب البحث ميزات لغة Kotlin (أصلية) RxJava جوافة مراحل النشاط في Jetpack*
One-shot write الروتينات الفرعية (suspend) Single<T>، Maybe<T>، Completable ListenableFuture<T> لا ينطبق
قراءة لمرة واحدة الروتينات الفرعية (suspend) Single<T>، Maybe<T> ListenableFuture<T> لا ينطبق
القراءة القابلة للملاحظة Flow<T> Flowable<T>، Publisher<T>، Observable<T> لا ينطبق LiveData<T>

يوضّح هذا الدليل ثلاث طرق لاستخدام عمليات الدمج هذه من أجل تنفيذ طلبات بحث غير متزامنة في عناصر الوصول إلى البيانات (DAO).

‫Kotlin مع Flow وcoroutines

توفّر Kotlin ميزات لغوية مضمّنة تتيح لك كتابة طلبات بحث غير متزامنة بدون استخدام أُطر خارجية:

  • تتيح Room استخدام Flow من Kotlin مباشرةً لكتابة طلبات بحث قابلة للمراقبة.
  • تتطلّب Room الكلمة الرئيسية suspend لجعل طلبات بحث DAO التي يتم تنفيذها لمرة واحدة غير متزامنة مع روتينات Kotlin.

تتوفّر إمكانية استخدام الروتينات المشتركة وFlow مباشرةً في وقت التشغيل الأساسي لمكتبة Room، لذلك لا يلزم توفير عناصر إضافية.

‫RxJava للغة Kotlin وJava

يتوافق الإصدار 3.0 من Room مع أنواع الإرجاع في RxJava 3. لاستخدام أنواع القيمة التي تم إرجاعها RxJava، عليك تسجيل محوّلات أنواع القيمة التي تم إرجاعها RxJava في قاعدة البيانات أو كائن الوصول إلى البيانات (DAO):

  1. أدرِج العنصر androidx.room3:room3-rxjava3 في إعدادات الإصدار.
  2. أضِف التعليق التوضيحي @DaoReturnTypeConverters(RxDaoReturnTypeConverters::class) إلى بيان @Database أو @Dao.

تتيح مكتبة Room أنواع الإرجاع التالية في RxJava 3:

LiveData وGuava

يتوافق الإصدار 3.0 من Room مع أنواع الإرجاع LiveData وGuava ListenableFuture باستخدام المحوّلات:

  • LiveData: أدرِج العنصر androidx.room3:room3-livedata وأضِف التعليق التوضيحي @DaoReturnTypeConverters(LiveDataDaoReturnTypeConverter::class) إلى قاعدة البيانات أو عنصر الوصول إلى البيانات (DAO).
  • Guava: أدرِج العنصر androidx.room3:room3-guava وأضِف التعليق التوضيحي @DaoReturnTypeConverters(GuavaDaoReturnTypeConverter::class) إلى قاعدة البيانات أو كائن الوصول إلى البيانات (DAO).

كتابة طلبات لمرة واحدة غير متزامنة

الاستعلامات لمرة واحدة هي عمليات قاعدة بيانات يتم تنفيذها مرة واحدة فقط وتلتقط لقطة من البيانات في وقت التنفيذ. في ما يلي بعض الأمثلة على طلبات البحث غير المتزامنة التي يتم تنفيذها مرة واحدة:

@Dao
interface UserDao {
    @Query("SELECT * FROM user WHERE id = :id")
    suspend fun loadUserById(id: Int): User

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

كتابة طلبات قابلة للمراقبة

طلبات البحث القابلة للمراقبة هي عمليات قراءة تعرض قيمًا جديدة كلما تغيّرت الجداول المشار إليها. على سبيل المثال، يمكنك استخدام هذا السلوك لإبقاء قائمة معروضة من العناصر محدّثة عند تغيير قاعدة البيانات. في ما يلي بعض الأمثلة على طلبات البحث القابلة للمراقبة:

@Dao
interface ObservableUserDao {
    @Query("SELECT * FROM user WHERE id = :id")
    fun loadUserById(id: Int): Flow<User>

    @Query("SELECT * from user WHERE region IN (:regions)")
    fun loadUsersByRegion(regions: List<String>): Flow<List<User>>
}

تتبُّع إبطال قاعدة البيانات يدويًا

عندما تحتاج إلى إنشاء عمليات قاعدة بيانات قابلة للمراقبة يدويًا، يمكنك استخدام واجهة برمجة التطبيقات createFlow في InvalidationTracker. تتيح لك واجهة برمجة التطبيقات هذه إنشاء Flow يتتبّع التعديلات على جداول معيّنة ويرسل إشعارًا كلما تم تغيير هذه الجداول.

fun getArtistTours(db: RoomDatabase, from: Date, to: Date): Flow<Map<Artist, TourState>> {
    return db.invalidationTracker.createFlow("Artist").map { _ ->
        val artists = artistsDao.getAllArtists()
        val tours = tourService.fetchStates(artists.map { it.id })
        associateTours(artists, tours, from, to)
    }
}

بشكلٍ تلقائي، يرسل Flow قيمة أولية تحتوي على جميع الجداول المسجّلة لبدء البث. يمكنك إيقاف هذا السلوك من خلال ضبط المَعلمة emitInitialState على false.

محوّلات أنواع القيمة التي تم إرجاعها المخصّصة لوحدة الوصول إلى البيانات

بالنسبة إلى الأنواع التي لا تتوافق مباشرةً مع Room أو مكتبات الإضافات الخاصة به، يمكنك تحديد محوّلات مخصّصة لأنواع القيمة التي تم إرجاعها في كائن الوصول إلى البيانات (DAO) من أجل توفير أنواع القيمة التي تم إرجاعها إضافية. لتحويل نتيجة دالة DAO إلى نوع مخصّص، أضِف التعليق التوضيحي @DaoReturnTypeConverter إلى دالة المحوّل.

على سبيل المثال، يمكنك تحديد محوّل يستخدم androidx.tracing لإضافة أقسام تتبُّع حول تنفيذ طلب بحث من أجل مراقبة طلبات البحث الحساسة للأداء من خلال تضمين التنفيذ في نوع TracedQuery مخصّص:

class TracedQuery<T>(val result: T)

object TracingDaoReturnTypeConverter {
    @DaoReturnTypeConverter([OperationType.READ])
    suspend fun <T> convert(
        rawQuery: RoomRawQuery,
        executeAndConvert: suspend () -> T
    ): TracedQuery<T> {
        val result = trace("TracedQuery: ${rawQuery.sql}") {
            executeAndConvert()
        }
        return TracedQuery(result)
    }
}

لاستخدام المحوّل، أضِف تعليقًا توضيحيًا إلى قاعدة البيانات أو DAO باستخدام @DaoReturnTypeConverters:

@Dao
@DaoReturnTypeConverters(TracingDaoReturnTypeConverter::class)
interface MusicDao {
    @Query("SELECT * FROM Song")
    suspend fun getAllSongs(): TracedQuery<List<Song>>
}

تهيئة محوّل نوع الإرجاع Control DAO

عادةً، يتعامل Room مع إنشاء مثيل لمحوّلات نوع القيمة التي تم إرجاعها الخاصة بكائنات الوصول إلى البيانات (DAO). ومع ذلك، إذا كان عليك تمرير تبعيات إضافية إلى فئات المحوّل، يجب أن يتحكّم تطبيقك مباشرةً في عملية تهيئتها. إذا كان الأمر كذلك، أضِف تعليقًا توضيحيًا إلى فئة المحوّل باستخدام @ProvidedDaoReturnTypeConverter:

@ProvidedDaoReturnTypeConverter
class TracingDaoReturnTypeConverter(val tracer: Tracer) {
    @DaoReturnTypeConverter([OperationType.READ])
    suspend fun <T> convert(
        rawQuery: RoomRawQuery,
        executeAndConvert: suspend () -> T
    ): TracedQuery<T> {
        val result = tracer.trace("TracedQuery: ${rawQuery.sql}") {
            executeAndConvert()
        }
        return TracedQuery(result)
    }
}

بعد ذلك، بالإضافة إلى تعريف فئة المحوّل في @DaoReturnTypeConverters، استخدِم الدالة RoomDatabase.Builder.addDaoReturnTypeConverter لتمرير مثيل لفئة المحوّل إلى أداة إنشاء RoomDatabase:

val db = Room.databaseBuilder<MyDatabase>(applicationContext, "database-name")
    .addDaoReturnTypeConverter(TracingDaoReturnTypeConverter(myLoggerInstance))
    .build()

متطلبات وظيفة المحوّل

يجب أن تستوفي دالة @DaoReturnTypeConverter عدة متطلبات:

  • يجب أن تتضمّن مَعلمة وظيفية كآخر وسيطة، ويُطلق عليها عادةً الاسم executeAndConvert. هذه المَعلمة هي دالة lambda suspend ينشئها Room لتنفيذ طلب البحث وتحليل النتيجة.
    • إذا كان المحوّل بحاجة إلى تغيير الاستعلام، مثل التقسيم إلى صفحات، يمكن أن تأخذ الدالة lambda المَعلمة RoomRawQuery.
  • يمكن أن يقبل اختياريًا المَعلمات التالية قبل تعبير lambda:
    • db: RoomDatabase: للوصول إلى مثيل قاعدة البيانات، وهو أمر مفيد للحصول على نطاق الكوروتين أو تنفيذ عمليات إضافية.
    • tableNames: Array<String> أو List<String>: يقدّم هذا الحقل أسماء الجداول التي يصل إليها طلب البحث، وهو مفيد لأنواع البيانات القابلة للمراقبة.
    • rawQuery: RoomRawQuery: يوفّر مثيل وقت التشغيل للاستعلام.
    • inTransaction: Boolean: تشير إلى ما إذا كان طلب البحث يتم تنفيذه ضمن معاملة.