KMP için Room veritabanı oluşturma

Room kalıcılık kitaplığı, SQLite'in tüm gücünden yararlanırken daha sağlam veritabanı erişimine olanak tanımak için SQLite üzerinde bir soyutlama katmanı sağlar. Bu sayfada, Kotlin Multiplatform (KMP) projelerinde Room'un kullanımı ele alınmaktadır. Room'u kullanma hakkında daha fazla bilgi için Room'u kullanarak verileri yerel veritabanına kaydetme başlıklı makaleyi veya resmi örneklerimizi inceleyin.

Bağımlılıkları ayarlama

KMP projenizde Room'u ayarlamak için KMP modülünüzün build.gradle.kts dosyasındaki yapılarla ilgili bağımlılıkları ekleyin.

Bağımlılıkları libs.versions.toml dosyasında tanımlayın:

[versions]
room3 = "3.0.1"
sqlite = "2.7.0"
ksp = "<kotlinCompatibleKspVersion>"

[libraries]
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" }
androidx-room3-runtime = { module = "androidx.room3:room3-runtime", version.ref = "room3" }
androidx-room3-compiler = { module = "androidx.room3:room3-compiler", version.ref = "room3" }

# Optional SQLite Wrapper
androidx-room3-sqlite-wrapper = { module = "androidx.room3:room3-sqlite-wrapper", version.ref = "room3" }

[plugins]
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
androidx-room3 = { id = "androidx.room3", version.ref = "room3" }

Room şemalarını ve KSP eklentisini yapılandırmak için Room Gradle eklentisini ekleyin.

plugins {
  alias(libs.plugins.ksp)
  alias(libs.plugins.androidx.room3)
}

Room çalışma zamanı bağımlılığını ve paketlenmiş SQLite kitaplığını ekleyin:

commonMain.dependencies {
  implementation(libs.androidx.room3.runtime)
  implementation(libs.androidx.sqlite.bundled)
}

// Optional when using Room SQLite Wrapper
androidMain.dependencies {
  implementation(libs.androidx.room3.sqlite.wrapper)
}

KSP bağımlılıklarını kök dependencies bloğuna ekleyin. Uygulamanızın kullandığı tüm hedefleri eklemeniz gerekir. Daha fazla bilgi için Kotlin Multiplatform ile KSP başlıklı makaleyi inceleyin.

dependencies {
    add("kspAndroid", libs.androidx.room3.compiler)
    add("kspIosSimulatorArm64", libs.androidx.room3.compiler)
    add("kspIosX64", libs.androidx.room3.compiler)
    add("kspIosArm64", libs.androidx.room3.compiler)
    // Add any other platform target you use in your project, for example kspDesktop
}

Oda şeması dizinini tanımlayın. Daha fazla bilgi için Room Gradle eklentisini kullanarak şema konumunu ayarlama başlıklı makaleyi inceleyin.

room {
    schemaDirectory("$projectDir/schemas")
}

Veritabanı sınıflarını tanımlama

Paylaşılan KMP modülünüzün ortak kaynak grubunda DAO'lar ve öğelerle birlikte @Database ile açıklama eklenmiş bir veritabanı sınıfı oluşturmanız gerekir. Bu sınıfları ortak kaynaklara yerleştirmek, tüm hedef platformlarda paylaşılmalarını sağlar.

// shared/src/commonMain/kotlin/Database.kt

@Database(entities = [TodoEntity::class], version = 1)
@ConstructedBy(AppDatabaseConstructor::class)
abstract class AppDatabase : RoomDatabase() {
  abstract fun getDao(): TodoDao
}

// The Room compiler generates the `actual` implementations.
@Suppress("KotlinNoActualForExpect")
expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase> {
    override fun initialize(): AppDatabase
}

expect nesnesini RoomDatabaseConstructor arayüzüyle bildirdiğinizde Room derleyicisi actual uygulamalarını oluşturur. Android Studio aşağıdaki uyarıyı verebilir. Bu uyarıyı @Suppress("KotlinNoActualForExpect") ile bastırabilirsiniz:

Expected object 'AppDatabaseConstructor' has no actual declaration in module`

Ardından, yeni bir DAO arayüzü tanımlayın veya mevcut bir arayüzü commonMain'ye taşıyın:

// shared/src/commonMain/kotlin/TodoDao.kt

@Dao
interface TodoDao {
  @Insert
  suspend fun insert(item: TodoEntity)

  @Query("SELECT count(*) FROM TodoEntity")
  suspend fun count(): Int

  @Query("SELECT * FROM TodoEntity")
  fun getAllAsFlow(): Flow<List<TodoEntity>>
}

Varlıklarınızı commonMain konumuna tanımlayın veya taşıyın:

// shared/src/commonMain/kotlin/TodoEntity.kt

@Entity
data class TodoEntity(
  @PrimaryKey(autoGenerate = true) val id: Long = 0,
  val title: String,
  val content: String
)

Platforma özel veritabanı oluşturucular oluşturma

Her platformda Room'u başlatmak için bir veritabanı oluşturucu tanımlamanız gerekir. Dosya sistemi API'lerindeki farklılıklar nedeniyle, API'nin yalnızca bu bölümünü platforma özgü kaynak kümelerinde tanımlamanız gerekir.

Android

Android'de, veritabanı konumunu genellikle Context.getDatabasePath API'sini kullanarak elde edersiniz. Veritabanı örneğini oluşturmak için bir Context ve veritabanı yolunu belirtin.

// shared/src/androidMain/kotlin/Database.android.kt

fun getDatabaseBuilder(context: Context): RoomDatabase.Builder<AppDatabase> {
  val appContext = context.applicationContext
  val dbFile = appContext.getDatabasePath("my_room.db")
  return Room.databaseBuilder<AppDatabase>(
    context = appContext,
    name = dbFile.absolutePath
  )
}

iOS

iOS'te veritabanı örneğini oluşturmak için NSFileManager kullanarak bir veritabanı yolu sağlayın. Bu yol genellikle NSDocumentDirectory içinde bulunur.

// shared/src/iosMain/kotlin/Database.ios.kt

fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
    val dbFilePath = documentDirectory() + "/my_room.db"
    return Room.databaseBuilder<AppDatabase>(
        name = dbFilePath,
    )
}

private fun documentDirectory(): String {
  val documentDirectory = NSFileManager.defaultManager.URLForDirectory(
    directory = NSDocumentDirectory,
    inDomain = NSUserDomainMask,
    appropriateForURL = null,
    create = false,
    error = null,
  )
  return requireNotNull(documentDirectory?.path)
}

JVM masaüstü

Veritabanı örneğini oluşturmak için Java veya Kotlin API'lerini kullanarak bir veritabanı yolu sağlayın.

// shared/src/jvmMain/kotlin/Database.desktop.kt

fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
    val dbFile = File(System.getProperty("java.io.tmpdir"), "my_room.db")
    return Room.databaseBuilder<AppDatabase>(
        name = dbFile.absolutePath,
    )
}

Veritabanını başlatma

Platforma özgü oluşturuculardan birinden RoomDatabase.Builder elde ettikten sonra, Oda veritabanının geri kalanını ortak kodda gerçek veritabanı örneği oluşturma işlemiyle birlikte yapılandırabilirsiniz.

// shared/src/commonMain/kotlin/Database.kt

fun getRoomDatabase(
    builder: RoomDatabase.Builder<AppDatabase>
): AppDatabase {
  return builder
      .setDriver(BundledSQLiteDriver())
      .setQueryCoroutineContext(Dispatchers.IO)
      .build()
}

SQLite sürücüsü seçme

Önceki kod snippet'i, Room veritabanının hangi SQLite sürücüsünü kullanması gerektiğini tanımlamak için setDriver oluşturucu işlevini çağırır. Bu sürücüler hedef platforma göre farklılık gösterir. Önceki kod snippet'lerinde BundledSQLiteDriver kullanılıyordu. Bu sürücü, kaynaktan derlenmiş SQLite'ı içerir ve tüm platformlarda en tutarlı ve güncel SQLite sürümünü sağlar. Bu nedenle, kullanılması önerilen sürücüdür.

İşletim sistemi tarafından sağlanan SQLite'ı kullanmak istiyorsanız platforma özgü bir sürücüyü belirten platforma özgü kaynak kümelerinde setDriver API'sini kullanın. Kullanılabilir sürücü uygulamalarının açıklamaları için Sürücü uygulamaları bölümüne bakın. Aşağıdakilerden birini kullanabilirsiniz:

NativeSQLiteDriver kullanmak için iOS uygulamasının sistem SQLite ile dinamik olarak bağlanması amacıyla bir bağlayıcı seçeneği -lsqlite3 sağlamanız gerekir.

// shared/build.gradle.kts

kotlin {
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach { iosTarget ->
        iosTarget.binaries.framework {
            baseName = "TodoApp"
            isStatic = true
            // Required when using NativeSQLiteDriver
            linkerOpts.add("-lsqlite3")
        }
    }
}

Coroutine bağlamı ayarlama

Android'deki bir RoomDatabase nesnesi, isteğe bağlı olarak RoomDatabase.Builder.setQueryExecutor kullanılarak paylaşılan uygulama yürütücüleriyle yapılandırılabilir. Bu sayede veritabanı işlemleri gerçekleştirilebilir.

Yürütücüler KMP ile uyumlu olmadığından Room'un setQueryExecutor API'si commonMain içinde kullanılamaz. Bunun yerine, RoomDatabase nesnesi CoroutineContext ile yapılandırılmalıdır. Bu nesneyi RoomDatabase.Builder.setCoroutineContext kullanarak ayarlayabilirsiniz. Bağlam ayarlamazsanız RoomDatabase nesnesi varsayılan olarak Dispatchers.IO olur.

Sadeleştirme ve karartma

Proje küçültülmüş veya karartılmışsa Room'un veritabanı tanımının oluşturulan uygulamasını bulabilmesi için aşağıdaki ProGuard kuralını eklemeniz gerekir:

-keep class * extends androidx.room3.RoomDatabase { <init>(); }

Kotlin Multiplatform'a taşıma

Room, başlangıçta bir Android kitaplığı olarak geliştirilmiş ve daha sonra API uyumluluğu odaklı bir şekilde KMP'ye taşınmıştır. Room'un KMP sürümü, platformlar arasında ve Android'e özel sürümden biraz farklıdır. Bu farklılıklar aşağıda listelenmiş ve açıklanmıştır.

Support SQLite'den SQLite sürücüsüne taşıma

androidx.sqlite.db içindeki SupportSQLiteDatabase ve diğer API'lerin tüm kullanımları, SQLite Driver API'leri ile yeniden düzenlenmelidir. Bunun nedeni, androidx.sqlite.db içindeki API'lerin yalnızca Android'e özel olmasıdır (KMP paketinden farklı bir paket olduğunu unutmayın).

Geriye dönük uyumluluk için ve RoomDatabase, SupportSQLiteOpenHelper.Factory ile yapılandırıldığı sürece (örneğin, SQLiteDriver ayarlanmamışsa) Room, hem Support SQLite hem de SQLite Driver API'lerinin beklendiği gibi çalıştığı "uyumluluk modunda" çalışır. Bu, artımlı taşımaları etkinleştirir. Böylece, tüm Support SQLite kullanımlarınızı tek bir değişiklikle SQLite sürücüsüne dönüştürmeniz gerekmez.

Room SQLite sarmalayıcısını kullanma

androidx.room3:room3-sqlite-wrapper yapısı, taşıma sırasında SQLiteDriver ile SupportSQLiteDatabase arasında köprü kurmak için API'ler sağlar.

SQLiteDriver ile yapılandırılmış bir RoomDatabase'den SupportSQLiteDatabase almak için yeni uzantı işlevi RoomDatabase.getSupportWrapper'yi kullanın. Bu uyumluluk sarmalayıcısı, SupportSQLiteDatabase'nın mevcut kullanımlarını korumaya yardımcı olur. SupportSQLiteDatabase'yı genellikle RoomDatabase.openHelper.writableDatabase'den elde edersiniz. Bu sarmalayıcı, özellikle BundledSQLiteDriver kullanmak isteyen ve kapsamlı SupportSQLite API kullanımlarına sahip kod tabanları için SQLiteDriver'nın benimsenmesini sağlar.

Taşıma alt sınıflarını dönüştürme

Taşıma alt sınıflarının SQLite sürücüsü karşılıklarına taşınması gerekir:

Kotlin Multiplatform

Taşıma alt sınıfları

object Migration_1_2 : Migration(1, 2) {
  override fun migrate(connection: SQLiteConnection) {
    // …
  }
}

Otomatik taşıma spesifikasyonu alt sınıfları

class AutoMigrationSpec_1_2 : AutoMigrationSpec {
  override fun onPostMigrate(connection: SQLiteConnection) {
    // …
  }
}

Yalnızca Android

Taşıma alt sınıfları

object Migration_1_2 : Migration(1, 2) {
  override fun migrate(db: SupportSQLiteDatabase) {
    // …
  }
}

Otomatik taşıma spesifikasyonu alt sınıfları

class AutoMigrationSpec_1_2 : AutoMigrationSpec {
  override fun onPostMigrate(db: SupportSQLiteDatabase) {
    // …
  }
}

Veritabanı geri çağırmasını dönüştürme

Veritabanı geri çağırmalarının SQLite sürücüsü karşılıklarına taşınması gerekir:

Kotlin Multiplatform

object MyRoomCallback : RoomDatabase.Callback() {
  override fun onCreate(connection: SQLiteConnection) {
    // …
  }

  override fun onDestructiveMigration(connection: SQLiteConnection) {
    // …
  }

  override fun onOpen(connection: SQLiteConnection) {
    // …
  }
}

Yalnızca Android

object MyRoomCallback : RoomDatabase.Callback() {
  override fun onCreate(db: SupportSQLiteDatabase) {
    // …
  }

  override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
    // …
  }

  override fun onOpen(db: SupportSQLiteDatabase) {
    // …
  }
}

@RawQuery DAO işlevlerini dönüştürme

Android dışı platformlar için derlenen @RawQuery ile açıklama eklenmiş işlevlerin, SupportSQLiteQuery yerine RoomRawQuery türünde bir parametre bildirmesi gerekir.

Kotlin Multiplatform

Ham sorguyu tanımlama

@Dao
interface TodoDao {
  @RawQuery
  suspend fun getTodos(query: RoomRawQuery): List<TodoEntity>
}

Daha sonra, çalışma zamanında sorgu oluşturmak için RoomRawQuery kullanılabilir:

suspend fun AppDatabase.getTodosWithLowercaseTitle(title: String): List<TodoEntity> {
    val query = RoomRawQuery(
        sql = "SELECT * FROM TodoEntity WHERE title = ?",
        onBindStatement = {
            it.bindText(1, title.lowercase())
        }
    )

    return todoDao().getTodos(query)
}

Yalnızca Android

Ham sorguyu tanımlama

@Dao
interface TodoDao {
  @RawQuery
  suspend fun getTodos(query: SupportSQLiteQuery): List<TodoEntity>
}

Daha sonra, çalışma zamanında sorgu oluşturmak için SimpleSQLiteQuery kullanılabilir:

suspend fun AndroidOnlyDao.getTodosWithLowercaseTitle(title: String): List<TodoEntity> {
  val query = SimpleSQLiteQuery(
      query = "SELECT * FROM TodoEntity WHERE title = ?",
      bindArgs = arrayOf(title.lowercase())
  )
  return getTodos(query)
}

Engelleme DAO işlevlerini dönüştürme

Room, Kotlin'in birden fazla platform için sunduğu özellik açısından zengin asenkron kotlinx.coroutines kitaplığından yararlanır. Optimum işlevsellik için, mevcut kod tabanıyla geriye dönük uyumluluğu korumak üzere androidMain içinde uygulanan DAO'lar hariç olmak üzere, KMP projesinde derlenen DAO'lar için suspend işlevleri zorunlu kılınır. Room for KMP kullanılırken Android dışı platformlar için derlenen tüm DAO işlevlerinin suspend işlevi olması gerekir.

Kotlin Multiplatform

Sorguları askıya alma

@Query("SELECT * FROM Todo")
suspend fun getAllTodos(): List<Todo>

İşlemleri askıya alma

@Transaction
suspend fun transaction() {  }

Yalnızca Android

Sorguları engelleme

@Query("SELECT * FROM Todo")
fun getAllTodos(): List<Todo>

İşlemleri engelleme

@Transaction
fun blockingTransaction() {  }

Reaktif türleri Flow'a dönüştürme

Tüm DAO işlevlerinin askıya alma işlevi olması gerekmez. LiveData veya RxJava'nın Flowable gibi reaktif türler döndüren DAO işlevleri, askıya alma işlevlerine dönüştürülmemelidir. Ancak LiveData gibi bazı türler KMP ile uyumlu değildir. Reaktif dönüş türlerine sahip DAO işlevleri, coroutine akışlarına taşınmalıdır.

Kotlin Multiplatform

Reactive türleri Flows

@Query("SELECT * FROM Todo")
fun getTodosFlow(): Flow<List<Todo>>

Yalnızca Android

LiveData veya RxJava'nın Flowable gibi reaktif türleri

@Query("SELECT * FROM Todo")
fun getTodosLiveData(): LiveData<List<Todo>>

Convert Transaction API'leri

Room KMP için veritabanı işlemi API'leri, yazma (useWriterConnection) ve okuma (useReaderConnection) işlemleri arasında ayrım yapabilir.

Kotlin Multiplatform

val database: RoomDatabase = 
database.useWriterConnection { transactor ->
  transactor.immediateTransaction {
    // perform database operations in transaction
  }
}

Yalnızca Android

val database: RoomDatabase = 
database.withTransaction {
  // perform database operations in transaction
}

Yazma işlemleri

Birden fazla sorgunun verileri atomik olarak yazmasını sağlamak için yazma işlemlerini kullanın. Böylece okuyucular verilere tutarlı bir şekilde erişebilir. Bu işlemi, üç işlem türünden herhangi biriyle useWriterConnection kullanarak yapabilirsiniz:

  • immediateTransaction: Önceden Yazma Günlüğü (WAL) modunda (varsayılan) bu tür işlemler başladığında kilitlenir ancak okuyucular okumaya devam edebilir. Çoğu durumda bu tercih edilen seçenektir.

  • deferredTransaction: İşlem, ilk yazma ifadesine kadar kilitlenmez. İşlem içinde yazma işlemi gerekip gerekmeyeceğinden emin olmadığınız durumlarda bu tür işlemleri optimizasyon olarak kullanın. Örneğin, yalnızca şarkı listesinin adı verilmiş bir şarkı listesinden şarkı silme işlemi başlatırsanız ve şarkı listesi mevcut değilse yazma (silme) işlemi gerekmez.

  • exclusiveTransaction: Bu mod, WAL modunda immediateTransaction ile aynı şekilde çalışır. Diğer günlük kaydı modlarında, işlem devam ederken diğer veritabanı bağlantılarının veritabanını okumasını engeller.

İşlemleri okuma

Veritabanından birden çok kez tutarlı bir şekilde okumak için okuma işlemlerini kullanın. Örneğin, iki veya daha fazla ayrı sorgunuz olduğunda ve JOIN tümcesini kullanmadığınızda. Okuyucu bağlantılarında yalnızca ertelenmiş işlemlere izin verilir. Okuyucu bağlantısında anında veya özel bir işlem başlatmaya çalışmak, bunlar 'yazma' işlemleri olarak kabul edildiğinden bir istisna oluşturur.

val database: RoomDatabase = 
database.useReaderConnection { transactor ->
  transactor.deferredTransaction {
      // perform database operations in transaction
  }
}

Kotlin Multiplatform'da kullanılamaz

Android'de kullanılabilen bazı API'ler Kotlin Multiplatform'da kullanılamaz.

Sorgu geri araması

Sorgu geri çağırmalarını yapılandırmak için aşağıdaki API'ler common'da kullanılamaz ve bu nedenle Android dışındaki platformlarda da kullanılamaz.

  • RoomDatabase.Builder.setQueryCallback
  • RoomDatabase.QueryCallback

Room'un gelecekteki bir sürümünde sorgu geri çağırma desteği eklemeyi planlıyoruz.

Geri çağırma arayüzü RoomDatabase.QueryCallback ile birlikte sorgu geri çağırması RoomDatabase yapılandırmak için kullanılan API, ortak olarak kullanılamadığından Android dışındaki diğer platformlarda da kullanılamaz.RoomDatabase.Builder.setQueryCallback

Otomatik kapanan veritabanı

Zaman aşımından sonra otomatik kapatmayı etkinleştiren API (RoomDatabase.Builder.setAutoCloseTimeout) yalnızca Android'de kullanılabilir ve diğer platformlarda kullanılamaz.

Önceden paketlenmiş veritabanı

Mevcut bir veritabanını (ör. önceden paketlenmiş bir veritabanı) kullanarak RoomDatabase oluşturmak için aşağıdaki API'ler genel olarak kullanılamaz ve bu nedenle Android dışındaki diğer platformlarda da kullanılamaz. Bu API'ler şunlardır:

  • RoomDatabase.Builder.createFromAsset
  • RoomDatabase.Builder.createFromFile
  • RoomDatabase.Builder.createFromInputStream
  • RoomDatabase.PrepackagedDatabaseCallback

Room'un gelecekteki bir sürümünde önceden paketlenmiş veritabanları için destek eklemeyi planlıyoruz.

Aynı anda çalışan birden çok kopya için geçersiz kılma

Aynı anda çalışan birden çok kopyanın geçersiz kılınmasını etkinleştirmek için kullanılan API, RoomDatabase.Builder.enableMultiInstanceInvalidation yalnızca Android'de kullanılabilir ve yaygın veya diğer platformlarda kullanılamaz.