إذا كان تطبيقك يتعامل مع كميات كبيرة من البيانات المنظَّمة، يمكنك الاستفادة بشكل كبير من تخزين هذه البيانات محليًا. وأكثر حالات الاستخدام شيوعًا هي تخزين أجزاء البيانات ذات الصلة مؤقتًا، حتى تتمكّن من تصفّح هذا المحتوى بلا إنترنت عندما يتعذّر على الجهاز الوصول إلى الشبكة.
توفر مكتبة Room لاستدامة البيانات طبقة تجريد فوق SQLite تتيح لك الوصول إلى قاعدة البيانات بسلاسة مع الاستفادة من إمكانات SQLite الكاملة.
إعداد Room 2.x
لاستخدام Room 2.x في تطبيقك، أضِف التبعيات التالية إلى ملف build.gradle في تطبيقك:
dependencies {
val room_version = "2.6.1"
implementation("androidx.room:room-runtime:$room_version")
annotationProcessor("androidx.room:room-compiler:$room_version")
// To use Kotlin Symbol Processing (KSP)
// ksp("androidx.room:room-compiler:$room_version")
// optional - Kotlin Extensions and Coroutines support for Room
implementation("androidx.room:room-ktx:$room_version")
// optional - RxJava2 support for Room
implementation("androidx.room:room-rxjava2:$room_version")
// optional - Guava support for Room, including Optional and ListenableFuture
implementation("androidx.room:room-guava:$room_version")
// optional - Test helpers
testImplementation("androidx.room:room-testing:$room_version")
}
المكوّنات الأساسية
يتضمّن Room ثلاثة مكوّنات رئيسية:
- فئة قاعدة البيانات التي تحتوي على قاعدة البيانات وتعمل كنقطة الوصول الرئيسية إلى الاتصال الأساسي بالبيانات الثابتة لتطبيقك
- عناصر البيانات التي تمثّل الجداول في قاعدة بيانات تطبيقك
- عناصر الوصول إلى البيانات (DAOs) التي توفّر طرقًا يمكن لتطبيقك استخدامها للاستعلام عن البيانات وتعديلها وإدراجها وحذفها في قاعدة البيانات
يوضّح الشكل 1 العلاقة بين المكوّنات المختلفة لمكتبة Room.
مثال على التنفيذ
// Entity
@Entity
data class User(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
// DAO
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): List<User>
@Insert
fun insertAll(vararg users: User)
@Delete
fun delete(user: User)
}
// Database
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
// Usage
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "database-name"
).build()
val userDao = db.userDao()
val users: List<User> = userDao.getAll()
تحديد البيانات باستخدام الكيانات
يمثّل كل عنصر Room جدولاً في قاعدة البيانات. يمكنك تعريف كل كيان كفئة
مزوّدة بالتعليق التوضيحي @Entity.
@Entity(tableName = "users")
data class User (
@PrimaryKey val id: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?,
@Ignore val picture: Bitmap? = null
)
- أسماء الجداول والأعمدة المخصّصة: تلقائيًا، يستخدم Room اسم الفئة كاسم للجدول وأسماء السمات كأسماء للأعمدة. لتخصيصها، استخدِم السمة
tableNameفي@Entityوالتعليق التوضيحي@ColumnInfo(name = "..."). - المفتاح الأساسي: لتحديد مفتاح أساسي، استخدِم
@PrimaryKey. بالنسبة إلى المفاتيح المركّبة، استخدِم السمةprimaryKeysالخاصة بـ@Entity:@Entity(primaryKeys = ["firstName", "lastName"]). - تجاهل الحقول: لمنع استمرار الحقول، استخدِم
@Ignore.
أدوات تحويل الأنواع
في بعض الأحيان، تحتاج إلى تخزين أنواع مخصّصة، مثل Date، في عمود واحد.
قدِّم طرق @TypeConverter لتحويل الأنواع المخصّصة إلى أنواع يمكن أن تحتفظ بها Room والعكس.
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }
@TypeConverter
fun dateToTimestamp(date: Date?): Long? = date?.time
}
// Register in your Database class
@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { ... }
الوصول إلى البيانات باستخدام عناصر DAO
تحدّد كائنات الوصول إلى البيانات طرقًا للتفاعل مع قاعدة البيانات. أضِف التعليق التوضيحي @Dao إلى الواجهة أو الفئة المجردة.
طُرق ملائمة
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUsers(vararg users: User)
@Update
fun updateUsers(vararg users: User)
@Delete
fun deleteUsers(vararg users: User)
}
- Insert: يمكن أن تعرض طريقة الإدراج
Longيمثّل رقم تعريف الصف الذي تم إدراجه، أوList<Long>يحتوي على أرقام تعريف جميع الصفوف التي تم إدراجها. - التعديل أو الحذف: يمكن أن تعرض طريقة التعديل أو الحذف
Intالتي تمثّل عدد الصفوف المتأثرة.
طُرق طلب البحث
أضِف تعليقات توضيحية إلى الطرق باستخدام @Query لكتابة عبارات SQL. تتحقّق Room من صحة طلبات البحث في وقت الترجمة البرمجية.
@Dao
interface UserDao {
// Simple query
@Query("SELECT * FROM user")
fun loadAllUsers(): Array<User>
// Return a subset of columns using a POJO or tuple
@Query("SELECT first_name, last_name FROM user")
fun loadFullName(): List<NameTuple>
// Pass parameters
@Query("SELECT * FROM user WHERE age > :minAge")
fun loadAllUsersOlderThan(minAge: Int): Array<User>
// Collection of parameters
@Query("SELECT * FROM user WHERE region IN (:regions)")
fun loadUsersFromRegions(regions: List<String>): List<User>
// Join tables
@Query("SELECT * FROM book INNER JOIN user ON user.id = book.user_id WHERE user.name = :userName")
fun findBooksBorrowedByName(userName: String): List<Book>
}
أنواع القيم التي يتم إرجاعها في الخرائط المتعددة
في Room 2.4 والإصدارات الأحدث، يمكن أن تعرض طرق طلب البحث خريطة متعدّدة مباشرةً باستخدام النوع Map:
@Query("SELECT * FROM user JOIN book ON user.id = book.user_id")
fun loadUserAndBookNames(): Map<User, List<Book>>
طلبات بحث غير متزامنة في DAO
ولتجنُّب توقّف واجهة المستخدم، لا يمكن تشغيل طلبات البحث في قاعدة البيانات على سلسلة التعليمات الرئيسية. اجعل طلبات البحث غير متزامنة باستخدام إحدى عمليات الدمج التالية:
الكوروتينات وFlow في Kotlin
يتطلّب التبعية room-ktx.
@Dao
interface UserDao {
// One-shot async query
@Insert
suspend fun insertUsers(vararg users: User)
// Observable query using Flow
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): Flow<User>
}
Java مع RxJava
يجب توفّر room-rxjava2 أو room-rxjava3.
@Dao
interface UserDao {
@Insert
fun insertUsers(users: List<User>): Completable
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): Flowable<User>
}
Java مع LiveData وGuava
يتطلب room-guava مقابل ListenableFuture.
@Dao
interface UserDao {
// LiveData for observable queries
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): LiveData<User>
// Guava ListenableFuture for one-shot queries
@Insert
fun insertUsers(users: List<User>): ListenableFuture<Integer>
}
تحديد العلاقات في Room 2.x
لمنع التحميل الكسول في سلسلة التعليمات الخاصة بواجهة المستخدم، لا يمكنك استخدام مراجع الكائنات المباشرة بين الكيانات. بدلاً من ذلك، حدِّد العلاقات باستخدام فئات البيانات الوسيطة مع @Relation.
واحد لواحد
يمكن لكل مستخدم امتلاك مكتبة واحدة فقط.
@Entity
data class User(@PrimaryKey val userId: Long, val name: String)
@Entity
data class Library(@PrimaryKey val libraryId: Long, val userOwnerId: Long)
// Intermediate class
data class UserAndLibrary(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userOwnerId"
)
val library: Library
)
// DAO Query
@Transaction
@Query("SELECT * FROM User")
fun getUsersAndLibraries(): List<UserAndLibrary>
واحد إلى متعدد
يمكن لكل مستخدم إنشاء العديد من قوائم التشغيل.
@Entity
data class Playlist(@PrimaryKey val playlistId: Long, val userCreatorId: Long)
data class UserWithPlaylists(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userCreatorId"
)
val playlists: List<Playlist>
)
متعدد إلى متعدد
يمكن أن تتضمّن قوائم التشغيل العديد من الأغاني، ويمكن أن تكون الأغاني في العديد من قوائم التشغيل. يتطلّب ذلك جدول ربط.
@Entity
data class Song(@PrimaryKey val songId: Long, val songName: String)
@Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(val playlistId: Long, val songId: Long)
data class PlaylistWithSongs(
@Embedded val playlist: Playlist,
@Relation(
parentColumn = "playlistId",
entityColumn = "songId",
associateBy = Junction(PlaylistSongCrossRef::class)
)
val songs: List<Song>
)
العلاقات المتداخلة
طلب البحث عن المستخدمين وقوائم التشغيل وكل الأغاني في تلك القوائم
data class UserWithPlaylistsAndSongs(
@Embedded val user: User,
@Relation(
entity = Playlist::class,
parentColumn = "userId",
entityColumn = "userCreatorId"
)
val playlists: List<PlaylistWithSongs> // Nesting PlaylistWithSongs
)
إدارة قاعدة البيانات
يتناول هذا القسم جوانب مختلفة من إدارة قاعدة بيانات Room، بما في ذلك طرق عرض قاعدة البيانات، وتعبئة البيانات مسبقًا، وعمليات نقل البيانات في قاعدة البيانات.
طرق عرض قاعدة البيانات
تغليف استعلام معقّد في فئة تمّت إضافة التعليق التوضيحي @DatabaseView إليها
@DatabaseView("SELECT user.id, user.name, department.name AS departmentName FROM user INNER JOIN department ON user.departmentId = department.id")
data class UserDetail(val id: Long, val name: String, val departmentName: String)
// Register in Database class
@Database(entities = [User::class], views = [UserDetail::class], version = 1)
abstract class AppDatabase : RoomDatabase() { ... }
تعبئة قاعدة البيانات مسبقًا
املأ قاعدة البيانات عند التهيئة من ملف مواد عرض أو نظام الملفات.
Room.databaseBuilder(appContext, AppDatabase::class.java, "Sample.db")
.createFromAsset("database/myapp.db")
.build()
عمليات الترحيل
عند تغيير المخطط، عليك زيادة رقم إصدار قاعدة البيانات وتحديد عنصر Migration.
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE User ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
}
}
Room.databaseBuilder(applicationContext, AppDatabase::class.java, "database-name")
.addMigrations(MIGRATION_1_2)
.build()
- عمليات النقل التلقائي: إذا كنت تستخدم Room 2.4.0 أو إصدارًا أحدث، يمكنك استخدام
@AutoMigrationلنقل التغييرات الأساسية في المخطط تلقائيًا. يتطلّب ذلك ضبطexportSchemaعلىtrueفي إعدادات قاعدة البيانات:@Database(version = 2, autoMigrations = [AutoMigration(from = 1, to = 2)]). - الرجوع إلى الإصدار السابق مع محو البيانات: إذا كان من المقبول فقدان البيانات عند عدم توفّر مسارات نقل البيانات، استخدِم
.fallbackToDestructiveMigrationعند إنشاء قاعدة البيانات.
اختبار عمليات نقل البيانات
للتحقّق من عمليات النقل، استخدِم MigrationTestHelper من العنصر room-testing. لإتاحة ذلك، تأكَّد من تصدير المخططات في إعدادات build.gradle.
@RunWith(AndroidJUnit4::class)
class MigrationTest {
@get:Rule
val helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory()
)
@Test
fun migrate1To2() {
var db = helper.createDatabase("test-db", 1).apply {
execSQL("INSERT INTO User VALUES (1, 'John')")
close()
}
db = helper.runMigrationsAndValidate("test-db", 2, true, MIGRATION_1_2)
// Verify data was migrated correctly
}
}
نقل البيانات من SQLite إلى Room
لنقل تطبيقك من SQLite إلى Room، أكمِل الخطوات التالية:
- تعديل الاعتماديات لتضمين Room
- أضِف تعليقات توضيحية إلى فئات النماذج باستخدام
@Entityو@PrimaryKeyو@ColumnInfo. - أنشئ كائنات الوصول إلى البيانات (DAO) لاستبدال طرق طلبات البحث المساعدة.
- أنشئ فئة RoomDatabase تشير إلى عناصرك وواجهات DAO. زيادة رقم الإصدار
- تحديد مسار ترحيل بيانات فارغ لأنّ المخطّط لا يتغيّر، بل يتغيّر إطار العمل فقط:
kotlin val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) {} } - تعديل عملية إنشاء مثيل التحديث لاستخدام
Room.databaseBuilderمع مسار نقل البيانات