اگر برنامه شما حجم قابل توجهی از دادههای ساختاریافته را مدیریت میکند، میتوانید از ذخیره محلی آن دادهها سود زیادی ببرید. رایجترین کاربرد آن، ذخیره دادههای مرتبط در حافظه پنهان است تا وقتی دستگاه به شبکه دسترسی ندارد، بتوانید در حالت آفلاین به مرور آن محتوا بپردازید.
کتابخانهی Room persistence یک لایهی انتزاعی روی SQLite فراهم میکند تا به شما امکان دسترسی روان به پایگاه داده را بدهد و در عین حال از تمام قدرت SQLite بهره ببرد.
برای استفاده از 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")
}
اتاق سه جزء اصلی دارد:
- کلاس پایگاه داده که پایگاه داده را در خود نگه میدارد و به عنوان نقطه دسترسی اصلی برای اتصال اساسی به دادههای دائمی برنامه شما عمل میکند.
- موجودیتهای دادهای که جداول موجود در پایگاه داده برنامه شما را نشان میدهند.
- اشیاء دسترسی به داده (DAO) که روشهایی را ارائه میدهند که برنامه شما میتواند برای پرسوجو، بهروزرسانی، درج و حذف دادهها در پایگاه داده از آنها استفاده کند.
شکل ۱ رابطه بین اجزای مختلف 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 ، را در یک ستون ذخیره کنید. برای تبدیل انواع داده سفارشی به و از انواع دادهای که Room میتواند ذخیره کند، از متدهای @TypeConverter استفاده کنید.
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)
}
- درج : متد درج میتواند یک
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>
}
در اتاق ۲.۴ و بالاتر، متدهای پرسوجو میتوانند مستقیماً با استفاده از نوع Map یک نقشه چندگانه (multimap) را برگردانند:
@Query("SELECT * FROM user JOIN book ON user.id = book.user_id")
fun loadUserAndBookNames(): Map<User, List<Book>>
برای جلوگیری از هنگ کردن رابط کاربری، کوئریهای پایگاه داده نمیتوانند روی ترد اصلی اجرا شوند. با استفاده از یکی از یکپارچهسازیهای زیر، کوئریهای خود را ناهمزمان کنید:
به وابستگی 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>
}
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>
}
برای ListenableFuture room-guava نیاز است.
@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>
}
برای جلوگیری از بارگذاری کند (lazy loading) در نخ رابط کاربری (UI thread)، نمیتوانید از ارجاع مستقیم به اشیاء بین موجودیتها استفاده کنید. در عوض، روابط را با استفاده از کلاسهای داده میانی با @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، مراحل زیر را انجام دهید:
- وابستگیها را بهروزرسانی کنید تا Room را نیز شامل شود.
- کلاسهای مدل را با
@Entity،@PrimaryKeyو@ColumnInfoحاشیهنویسی کنید. - DAOهایی ایجاد کنید تا جایگزین متدهای کمکی کوئری شما شوند.
- یک کلاس RoomDatabase ایجاد کنید که به موجودیتها و DAOهای شما ارجاع دهد. شماره نسخه را افزایش دهید.
- یک مسیر مهاجرت خالی تعریف کنید زیرا طرحواره تغییر نمیکند، فقط چارچوب تغییر میکند:
kotlin val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) {} } - نمونهسازی را بهروزرسانی کنید تا از
Room.databaseBuilderبا مسیر مهاجرت استفاده کند.