Room (multiplateforme Kotlin)

La bibliothèque de persistance Room fournit une couche d'abstraction sur SQLite afin de permettre un accès plus robuste à la base de données, tout en exploitant toute la puissance de SQLite. Cette page se concentre sur l'utilisation de Room dans les projets Kotlin Multiplatform (KMP). Pour en savoir plus sur l'utilisation de Room, consultez Enregistrer des données dans une base de données locale à l'aide de Room ou nos exemples officiels.

Configurer des dépendances

Pour configurer Room dans votre projet KMP, ajoutez les dépendances pour les artefacts dans le fichier build.gradle.kts de votre module KMP.

Définissez les dépendances dans le fichier libs.versions.toml :

[versions]
room = "2.7.2"
sqlite = "2.5.2"
ksp = "<kotlinCompatibleKspVersion>"

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

# Optional SQLite Wrapper available in version 2.8.0 and higher
androidx-room-sqlite-wrapper = { module = "androidx.room:room-sqlite-wrapper", version.ref = "room" }

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

Ajoutez le plug-in Room Gradle pour configurer les schémas Room et le plug-in KSP.

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

Ajoutez la dépendance d'exécution Room et la bibliothèque SQLite groupée :

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

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

Ajoutez les dépendances KSP au bloc dependencies racine. Notez que vous devez ajouter toutes les cibles utilisées par votre application. Pour en savoir plus, consultez KSP avec Kotlin Multiplatform.

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

Définissez le répertoire du schéma Room. Pour en savoir plus, consultez Définir l'emplacement du schéma à l'aide du plug-in Room Gradle.

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

Définir les classes de base de données

Vous devez créer une classe de base de données annotée avec @Database, ainsi que des DAO et des entités dans l'ensemble de sources commun de votre module KMP partagé. En plaçant ces classes dans des sources communes, vous pourrez les partager sur toutes les plates-formes cibles.

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

Lorsque vous déclarez un objet expect avec l'interface RoomDatabaseConstructor, le compilateur Room génère les implémentations actual. Android Studio peut générer l'avertissement suivant, que vous pouvez supprimer avec @Suppress("KotlinNoActualForExpect") :

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

Ensuite, définissez une nouvelle interface DAO ou déplacez-en une existante vers commonMain :

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

Définissez ou déplacez vos entités vers commonMain :

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

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

Créer le générateur de base de données spécifique à la plate-forme

Vous devez définir un générateur de base de données pour instancier Room sur chaque plate-forme. Il s'agit de la seule partie de l'API qui doit se trouver dans des ensembles de sources spécifiques à la plate-forme en raison des différences entre les API du système de fichiers.

Android

Sur Android, l'emplacement de la base de données est généralement obtenu via l'API Context.getDatabasePath(). Pour créer l'instance de base de données, spécifiez un Context avec le chemin d'accès à la base de données.

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

Pour créer l'instance de base de données sur iOS, indiquez un chemin d'accès à la base de données à l'aide de NSFileManager, généralement situé dans NSDocumentDirectory.

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

Pour créer l'instance de base de données, fournissez un chemin d'accès à la base de données à l'aide des API Java ou Kotlin.

// 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,
    )
}

Instancier la base de données

Une fois que vous avez obtenu le RoomDatabase.Builder à partir de l'un des constructeurs spécifiques à la plate-forme, vous pouvez configurer le reste de la base de données Room dans un code commun, ainsi que l'instanciation de la base de données proprement dite.

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

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

Sélectionner un pilote SQLite

L'extrait de code précédent appelle la fonction de compilation setDriver pour définir le pilote SQLite que la base de données Room doit utiliser. Ces pilotes diffèrent selon la plate-forme cible. Les extraits de code précédents utilisent BundledSQLiteDriver. Il s'agit du pilote recommandé, qui inclut SQLite compilé à partir de la source. Il fournit la version la plus cohérente et la plus récente de SQLite sur toutes les plates-formes.

Si vous souhaitez utiliser SQLite fourni par l'OS, utilisez l'API setDriver dans les ensembles de sources spécifiques à la plate-forme qui spécifient un pilote spécifique à la plate-forme. Pour obtenir une description des implémentations de pilote disponibles, consultez Implémentations de pilote. Vous pouvez utiliser l'une des méthodes suivantes :

Pour utiliser NativeSQLiteDriver, vous devez fournir une option d'éditeur de liens -lsqlite3 afin que l'application iOS soit liée dynamiquement à SQLite du système.

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

Définir un contexte de coroutine (facultatif)

Un objet RoomDatabase sur Android peut éventuellement être configuré avec des exécuteurs d'application partagés à l'aide de RoomDatabase.Builder.setQueryExecutor() pour effectuer des opérations de base de données.

Étant donné que les exécuteurs ne sont pas compatibles avec KMP, l'API setQueryExecutor() de Room n'est pas disponible dans commonMain. L'objet RoomDatabase doit être configuré avec un CoroutineContext, qui peut être défini à l'aide de RoomDatabase.Builder.setCoroutineContext(). Si aucun contexte n'est défini, l'objet RoomDatabase utilisera par défaut Dispatchers.IO.

Minification et obscurcissement

Si le projet est minifié ou obscurci, vous devez inclure la règle ProGuard suivante pour que Room puisse trouver l'implémentation générée de la définition de la base de données :

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

Migrer vers Kotlin Multiplatform

Room a été initialement développé en tant que bibliothèque Android, puis migré vers KMP en mettant l'accent sur la compatibilité des API. La version KMP de Room diffère quelque peu entre les plates-formes et de la version spécifique à Android. Ces différences sont listées et décrites ci-dessous.

Migrer de Support SQLite vers le pilote SQLite

Toutes les utilisations de SupportSQLiteDatabase et d'autres API dans androidx.sqlite.db doivent être refactorisées avec les API du pilote SQLite, car les API dans androidx.sqlite.db sont réservées à Android (notez le package différent de celui de KMP).

Pour assurer la rétrocompatibilité, et tant que RoomDatabase est configuré avec un SupportSQLiteOpenHelper.Factory (par exemple, aucun SQLiteDriver n'est défini), Room se comporte en "mode compatibilité", où les API Support SQLite et SQLite Driver fonctionnent comme prévu. Cela permet des migrations incrémentielles afin que vous n'ayez pas à convertir toutes vos utilisations de Support SQLite en pilote SQLite en une seule fois.

Convertir les sous-classes de migrations

Les sous-classes de migrations doivent être migrées vers les équivalents du pilote SQLite :

Multiplateforme Kotlin

Sous-classes de migration

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

Sous-classes de spécification de la migration automatique

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

Android uniquement

Sous-classes de migration

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

Sous-classes de spécification de la migration automatique

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

Rappel de conversion de la base de données

Les rappels de base de données doivent être migrés vers les équivalents du pilote SQLite :

Multiplateforme Kotlin

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

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

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

Android uniquement

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

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

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

Convertir les fonctions DAO @RawQuery

Les fonctions annotées avec @RawQuery qui sont compilées pour des plates-formes autres qu'Android devront déclarer un paramètre de type RoomRawQuery au lieu de SupportSQLiteQuery.

Multiplateforme Kotlin

Définir la requête brute

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

Un RoomRawQuery peut ensuite être utilisé pour créer une requête au moment de l'exécution :

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

Android uniquement

Définir la requête brute

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

Un SimpleSQLiteQuery peut ensuite être utilisé pour créer une requête au moment de l'exécution :

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

Convertir les fonctions DAO de blocage

Room bénéficie de la bibliothèque asynchrone kotlinx.coroutines riche en fonctionnalités que Kotlin propose pour plusieurs plates-formes. Pour une fonctionnalité optimale, les fonctions suspend sont appliquées aux DAO compilés dans un projet KMP, à l'exception des DAO implémentés dans androidMain pour maintenir la rétrocompatibilité avec la base de code existante. Lorsque vous utilisez Room pour KMP, toutes les fonctions DAO compilées pour les plates-formes autres qu'Android doivent être des fonctions suspend.

Multiplateforme Kotlin

Suspendre des requêtes

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

Suspendre des transactions

@Transaction
suspend fun transaction() {  }

Android uniquement

Requêtes bloquantes

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

Bloquer des transactions

@Transaction
fun blockingTransaction() {  }

Convertir des types réactifs en Flow

Toutes les fonctions DAO n'ont pas besoin d'être des fonctions de suspension. Les fonctions DAO qui renvoient des types réactifs tels que LiveData ou Flowable de RxJava ne doivent pas être converties en fonctions de suspension. Toutefois, certains types, tels que LiveData, ne sont pas compatibles avec KMP. Les fonctions DAO avec des types renvoyés réactifs doivent être migrées vers des flux de coroutines.

Multiplateforme Kotlin

Types réactifs Flows

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

Android uniquement

Types réactifs tels que LiveData ou Flowable de RxJava

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

Convertir les API de transaction

Les API de transaction de base de données pour Room KMP peuvent faire la différence entre les transactions d'écriture (useWriterConnection) et de lecture (useReaderConnection).

Multiplateforme Kotlin

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

Android uniquement

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

Transactions d'écriture

Utilisez des transactions d'écriture pour vous assurer que plusieurs requêtes écrivent des données de manière atomique, afin que les lecteurs puissent accéder aux données de manière cohérente. Pour ce faire, utilisez useWriterConnection avec l'un des trois types de transactions :

  • immediateTransaction : en mode Write-Ahead Logging (WAL) (par défaut), ce type de transaction acquiert un verrou lorsqu'il démarre, mais les lecteurs peuvent continuer à lire. Il s'agit de l'option à privilégier dans la plupart des cas.

  • deferredTransaction : la transaction n'acquiert pas de verrou jusqu'à la première instruction d'écriture. Utilisez ce type de transaction comme optimisation lorsque vous ne savez pas si une opération d'écriture sera nécessaire dans la transaction. Par exemple, si vous lancez une transaction pour supprimer des titres d'une playlist en indiquant uniquement le nom de la playlist et que celle-ci n'existe pas, aucune opération d'écriture (suppression) n'est nécessaire.

  • exclusiveTransaction : ce mode se comporte de la même manière que immediateTransaction en mode WAL. Dans les autres modes de journalisation, il empêche les autres connexions à la base de données de lire la base de données pendant la transaction.

Transactions de lecture

Utilisez des transactions de lecture pour lire plusieurs fois les données de la base de données de manière cohérente. Par exemple, lorsque vous avez deux requêtes distinctes ou plus et que vous n'utilisez pas de clause JOIN. Seules les transactions différées sont autorisées dans les connexions de lecteur. Toute tentative de démarrage d'une transaction immédiate ou exclusive dans une connexion de lecteur générera une exception, car il s'agit d'opérations d'écriture.

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

Non disponible dans Kotlin Multiplatform

Certaines API qui étaient disponibles pour Android ne le sont pas dans Kotlin Multiplatform.

Rappel de requête

Les API suivantes permettant de configurer les rappels de requête ne sont pas disponibles dans les API communes et ne le sont donc pas sur les plates-formes autres qu'Android.

  • RoomDatabase.Builder.setQueryCallback
  • RoomDatabase.QueryCallback

Nous prévoyons d'ajouter la prise en charge du rappel de requête dans une prochaine version de Room.

L'API permettant de configurer un RoomDatabase avec un rappel de requête RoomDatabase.Builder.setQueryCallback ainsi que l'interface de rappel RoomDatabase.QueryCallback ne sont pas disponibles dans les éléments communs et ne le sont donc pas non plus sur les plates-formes autres qu'Android.

Base de données à fermeture automatique

L'API permettant d'activer la fermeture automatique après un délai d'inactivité, RoomDatabase.Builder.setAutoCloseTimeout, n'est disponible que sur Android et non sur d'autres plates-formes.

Base de données pré-packagée

Les API suivantes permettant de créer un RoomDatabase à l'aide d'une base de données existante (c'est-à-dire une base de données pré-empaquetée) ne sont pas disponibles dans les modules communs et ne le sont donc pas non plus sur les plates-formes autres qu'Android. Voici les API concernées :

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

Nous prévoyons d'ajouter la compatibilité avec les bases de données préemballées dans une future version de Room.

Invalidation multi-instances

L'API permettant d'activer l'invalidation multi-instance, RoomDatabase.Builder.enableMultiInstanceInvalidation, n'est disponible que sur Android. Elle n'est pas disponible sur les plates-formes courantes ni sur d'autres plates-formes.