Room 2.x kullanarak verileri yerel veritabanına kaydetme

Uygulamanız önemli miktarda yapılandırılmış veriyi işliyorsa bu verileri yerel olarak kalıcı hale getirmekten büyük ölçüde yararlanabilirsiniz. En yaygın kullanım alanı, cihaz ağa erişemediğinde çevrimdışı olarak göz atabilmeniz için ilgili veri parçalarını önbelleğe almaktır.

Room kalıcılık kitaplığı, SQLite'ın tüm gücünden yararlanırken veritabanına akıcı bir şekilde erişmenizi sağlamak için SQLite üzerinde bir soyutlama katmanı sunar.

Room 2.x'i ayarlama

Uygulamanızda Room 2.x'i kullanmak için uygulamanızın build.gradle dosyasına aşağıdaki bağımlılıkları ekleyin:

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

Birincil bileşenler

Room'un üç ana bileşeni vardır:

  • Veritabanını tutan ve uygulamanızın kalıcı verilerine yönelik temel bağlantı için ana erişim noktası olarak hizmet veren veritabanı sınıfı.
  • Uygulamanızın veritabanındaki tabloları temsil eden veri öğeleri.
  • Uygulamanızın veritabanındaki verileri sorgulamak, güncellemek, eklemek ve silmek için kullanabileceği yöntemler sağlayan veri erişimi nesneleri (DAO'lar).

Şekil 1'de Room'un farklı bileşenleri arasındaki ilişki gösterilmektedir.

Şekil 1. Room kitaplığı mimarisinin şeması.

Örnek uygulama

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

Varlıkları kullanarak verileri tanımlama

Her Room varlığı, veritabanındaki bir tabloyu temsil eder. Her varlığı @Entity ile açıklama eklenmiş bir sınıf olarak tanımlarsınız.

@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
)
  • Özel tablo ve sütun adları: Room, varsayılan olarak tablo adı olarak sınıf adını, sütun adları olarak da özellik adlarını kullanır. Bunları özelleştirmek için @Entity içindeki tableName özelliğini ve @ColumnInfo(name = "...") açıklamasını kullanın.
  • Birincil anahtar: Birincil anahtar tanımlamak için @PrimaryKey kullanın. Birleşik anahtarlar için @Entity öğesinin primaryKeys özelliğini kullanın: @Entity(primaryKeys = ["firstName", "lastName"]).
  • Alanları yoksayma: Alanların kalıcı olmasını önlemek için @Ignore simgesini kullanın.

Tür dönüştürücüler

Bazen Date gibi özel türleri tek bir sütunda saklamanız gerekir. Özel türleri türlere ve türlerden dönüştürmek için @TypeConverter yöntemleri sağlayın. Room kalıcı olabilir.

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'ları kullanarak verilere erişme

DAO'lar, veritabanı etkileşimi için yöntemler tanımlar. Arayüze veya soyut sınıfa @Dao ile açıklama ekleyin.

Kolaylık yöntemleri

@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: Ekleme yöntemi, eklenen satır kimliğini temsil eden bir Long veya eklenen tüm satırların kimliklerini içeren bir List<Long> döndürebilir.
  • Güncelleme veya Silme: Güncelleme veya silme yöntemi, etkilenen satır sayısını temsil eden bir Int döndürebilir.

Sorgu yöntemleri

SQL ifadeleri yazmak için yöntemlere @Query ekleyin. Room, derleme zamanında sorguları doğrular.

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

Çoklu harita dönüş türleri

Room 2.4 ve sonraki sürümlerde, sorgu yöntemleri Map türü kullanılarak doğrudan bir multimap döndürebilir:

@Query("SELECT * FROM user JOIN book ON user.id = book.user_id")
fun loadUserAndBookNames(): Map<User, List<Book>>

Eşzamansız DAO sorguları

Kullanıcı arayüzünün donmasını önlemek için veritabanı sorguları ana iş parçacığında çalıştırılamaz. Aşağıdaki entegrasyonlardan birini kullanarak sorgularınızı asenkron hale getirin:

Kotlin eş yordamları ve Flow

room-ktx bağımlılığı gerektirir.

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

room-rxjava2 veya room-rxjava3 gerektirir.

@Dao
interface UserDao {
    @Insert
    fun insertUsers(users: List<User>): Completable

    @Query("SELECT * FROM user WHERE id = :id")
    fun loadUserById(id: Int): Flowable<User>
}

LiveData ve Guava ile Java

ListenableFuture için room-guava gerektirir.

@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'te ilişkileri tanımlama

Kullanıcı arayüzü iş parçacığında geç yüklemeyi önlemek için doğrudan nesne başvurularını kullanamazsınız. Bunun yerine, @Relation ile ara veri sınıflarını kullanarak ilişkileri tanımlayın.

Bire bir

Her kullanıcının yalnızca bir kitaplığı vardır.

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

Bire-çok

Her kullanıcının birden fazla oynatma listesi olabilir.

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

Çoka-çok

Oynatma listelerinde çok sayıda şarkı olabilir ve şarkılar birden fazla oynatma listesinde yer alabilir. Birleştirme tablosu gerektirir.

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

İç içe yerleştirilmiş ilişkiler

Kullanıcıları, oynatma listelerini ve bu oynatma listelerindeki tüm şarkıları sorgulayın.

data class UserWithPlaylistsAndSongs(
      @Embedded val user: User,
      @Relation(
          entity = Playlist::class,
          parentColumn = "userId",
          entityColumn = "userCreatorId"
      )
      val playlists: List<PlaylistWithSongs> // Nesting PlaylistWithSongs
  )

Veritabanı yönetimi

Bu bölümde, veritabanı görünümleri, verileri önceden doldurma ve veritabanı taşımaları dahil olmak üzere Room veritabanınızı yönetmenin çeşitli yönleri ele alınmaktadır.

Veritabanı görünümleri

Karmaşık bir sorguyu @DatabaseView ile açıklama eklenmiş bir sınıfa yerleştirin.

@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() { ... }

Veritabanını önceden doldurma

Başlatma sırasında veritabanını bir öğe dosyasından veya dosya sisteminden doldurun.

Room.databaseBuilder(appContext, AppDatabase::class.java, "Sample.db")
    .createFromAsset("database/myapp.db")
    .build()

Taşıma İşlemleri

Şemayı değiştirdiğinizde veritabanı sürümünü artırın ve Migration nesnesi tanımlayın.

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()
  • Otomatik taşıma işlemleri: Room 2.4.0 veya sonraki bir sürümü kullanıyorsanız temel şema değişikliklerini otomatik olarak taşımak için @AutoMigration kullanabilirsiniz. Bunun için veritabanı yapılandırmanızda exportSchema değerini true olarak ayarlamanız gerekir: @Database(version = 2, autoMigrations = [AutoMigration(from = 1, to = 2)]).
  • Yıkıcı yedekleme: Taşıma yolları eksik olduğunda veri kaybı kabul edilebilir bir durumsa veri tabanını oluştururken .fallbackToDestructiveMigration işlevini çağırın.

Test taşıma işlemleri

Taşıma işlemlerini doğrulamak için room-testing yapıtından MigrationTestHelper kullanın. Bunu desteklemek için build.gradle yapılandırmanızda şemaları dışa aktardığınızdan emin olun.

@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'tan Room'a taşıma

Uygulamanızı SQLite'tan Room'a taşımak için aşağıdaki adımları tamamlayın:

  1. Room'u içerecek şekilde bağımlılıkları güncelleyin.
  2. @Entity, @PrimaryKey ve @ColumnInfo ile model sınıflarına açıklama ekleyin.
  3. Yardımcı sorgu yöntemlerinizin yerini alacak DAO'lar oluşturun.
  4. Varlıklarınıza ve DAO'larınıza referans veren bir RoomDatabase sınıfı oluşturun. Sürüm numarasını artırın.
  5. Şema değişmediği için boş bir taşıma yolu tanımlayın. Yalnızca çerçeve değişir: kotlin val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) {} }
  6. Taşıma yoluyla Room.databaseBuilder kullanmak için örnek oluşturmayı güncelleyin.