Room 2.x का इस्तेमाल करके, डेटा को किसी स्थानीय डेटाबेस में सेव करना

अगर आपका ऐप्लिकेशन, स्ट्रक्चर्ड डेटा की बड़ी मात्रा को मैनेज करता है, तो उस डेटा को स्थानीय तौर पर सेव करने से आपको काफ़ी फ़ायदा मिल सकता है. इसका सबसे आम इस्तेमाल, काम के डेटा को कैश मेमोरी में सेव करना है, ताकि जब डिवाइस नेटवर्क को ऐक्सेस न कर पाए, तब भी ऑफ़लाइन होने पर उस कॉन्टेंट को ब्राउज़ किया जा सके.

रूम परसिस्टेंस लाइब्रेरी, 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 के तीन मुख्य कॉम्पोनेंट होते हैं:

  • डेटाबेस क्लास , जिसमें डेटाबेस होता है. साथ ही, यह आपके ऐप्लिकेशन के सेव किए गए डेटा के लिए, मूल कनेक्शन का मुख्य ऐक्सेस पॉइंट होता है.
  • डेटा इकाइयां , जो आपके ऐप्लिकेशन के डेटाबेस में मौजूद टेबल को दिखाती हैं.
  • डेटा ऐक्सेस ऑब्जेक्ट (डीएओ) , जो ऐसे तरीके उपलब्ध कराते हैं जिनका इस्तेमाल करके, आपका ऐप्लिकेशन डेटाबेस में मौजूद डेटा के लिए क्वेरी कर सकता है, उसे अपडेट कर सकता है, उसमें डेटा जोड़ सकता है, और उसे मिटा सकता है.

पहली इमेज में, Room के अलग-अलग कॉम्पोनेंट के बीच का संबंध दिखाया गया है.

पहली इमेज. 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
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 के साथ एनोटेट करें. 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>>

डीएओ की एसिंक्रोनस क्वेरी

यूज़र इंटरफ़ेस (यूआई) के फ़्रीज़ होने से बचने के लिए, डेटाबेस क्वेरी मुख्य थ्रेड पर नहीं चल सकतीं. अपनी क्वेरी को एसिंक्रोनस बनाने के लिए, इनमें से किसी एक इंटिग्रेशन का इस्तेमाल करें:

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

RxJava के साथ Java

इसके लिए, 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>
}

LiveData और Guava के साथ Java

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

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

माइग्रेशन

स्कीमा में बदलाव करने पर, डेटाबेस का वर्शन बढ़ाएं और a 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 को कॉल करें.

माइग्रेशन टेस्ट करना

माइग्रेशन की पुष्टि करने के लिए, room-testing आर्टफ़ैक्ट से MigrationTestHelper का इस्तेमाल करें. इसके लिए, पक्का करें कि आपने अपने 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 पर माइग्रेट करने के लिए, यह तरीका अपनाएं:

  1. Room को शामिल करने के लिए, डिपेंडेंसी अपडेट करें.
  2. मॉडल क्लास को एनोटेट करें . इसके लिए, @Entity, @PrimaryKey, और @ColumnInfo का इस्तेमाल करें.
  3. हेल्पर क्वेरी के तरीकों की जगह डीएओ बनाएं.
  4. अपनी इकाइयों और डीएओ को रेफ़र करने वाली RoomDatabase क्लास बनाएं. वर्शन नंबर बढ़ाएं.
  5. खाली माइग्रेशन पाथ तय करें, क्योंकि स्कीमा में कोई बदलाव नहीं होता. सिर्फ़ फ़्रेमवर्क में बदलाव होता है: kotlin val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) {} }
  6. माइग्रेशन पाथ के साथ Room.databaseBuilder का इस्तेमाल करने के लिए, इंस्टैंशिएशन अपडेट करें.