الوصول إلى البيانات باستخدام عناصر الوصول إلى البيانات في Room

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

باستخدام عناصر الوصول إلى البيانات للوصول إلى قاعدة بيانات تطبيقك بدلاً من أدوات إنشاء الاستعلامات أو الاستعلامات المباشرة ، يمكنك الحفاظ على فصل الاهتمامات، وهو مبدأ معماري أساسي. تتيح لك عناصر الوصول إلى البيانات أيضًا محاكاة الوصول إلى قاعدة البيانات عند اختبار تطبيقك.

بنية عنصر الوصول إلى البيانات

يمكنك تحديد كل عنصر من عناصر الوصول إلى البيانات كواجهة أو فئة مجرّدة. بالنسبة إلى حالات الاستخدام الأساسية، يمكنك عادةً استخدام واجهة. في كلتا الحالتين، عليك دائمًا إضافة التعليق التوضيحي إلى عناصر الوصول إلى البيانات @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>
}

هناك نوعان من دوال عناصر الوصول إلى البيانات التي تحدّد التفاعلات مع قاعدة البيانات:

  • الدوال الملائمة التي تتيح لك إدراج الصفوف وتعديلها وحذفها في قاعدة بياناتك بدون كتابة أي رمز SQL.
  • دوال الاستعلام التي تتيح لك كتابة استعلام SQL الخاص بك للتفاعل مع قاعدة البيانات.

توضّح الأقسام التالية كيفية استخدام كلا النوعَين من دوال عناصر الوصول إلى البيانات لتحديد التفاعلات مع قاعدة البيانات التي يحتاجها تطبيقك.

الدوال الملائمة

توفّر 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 وعرضها كدوال لعناصر الوصول إلى البيانات. استخدِم دوال الاستعلام هذه لطلب البيانات من قاعدة بيانات تطبيقك أو عندما تحتاج إلى إجراء عمليات إدراج وتعديل وحذف أكثر تعقيدًا.

تتحقّق 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.

تمرير مَعلمات بسيطة إلى استعلام

في معظم الأحيان، تحتاج دوال عناصر الوصول إلى البيانات إلى قبول مَعلمات حتى تتمكّن من إجراء عمليات الفلترة. تتيح 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>

تمرير مجموعة من المَعلمات إلى استعلام

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

على سبيل المثال، يحدّد الرمز البرمجي التالي دالة تعرض معلومات عن جميع المستخدمين من مجموعة فرعية من المناطق:

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

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

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 بعض أنواع العرض الخاصة للتكامل مع مكتبات واجهات برمجة التطبيقات الأخرى.

الطلبات المقسّمة إلى صفحات باستخدام مكتبة Paging

تتيح Room إجراء طلبات مقسّمة إلى صفحات من خلال التكامل مع الـ Paging library. لاستخدام أنواع القيمة التي تم إرجاعها في Paging 3، عليك تسجيل محوّلات نوع القيمة التي تم إرجاعها في Paging في قاعدة البيانات أو عنصر الوصول إلى البيانات:

  1. أدرِج العنصر androidx.room3:room3-paging في إعدادات الإصدار.
  2. أضِف التعليق التوضيحي @DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class) إلى إعلان @Database أو @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، اطّلِع على اختيار أنواع المفاتيح والقيم.

الوصول المباشر إلى اتصال قاعدة البيانات

إذا كانت منطق تطبيقك يتطلّب وصولاً مباشرًا ومنخفض المستوى إلى اتصال قاعدة البيانات، يمكنك استخدام واجهات برمجة التطبيقات للاتصال في 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
    }
}

بدلاً من ذلك، إذا كنت بحاجة فقط إلى تنفيذ عمليات عالية المستوى لعناصر الوصول إلى البيانات في معاملة، استخدِم الدوال المساعدة 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)
}