В этом документе описывается, как перенести существующую реализацию Room на версию, использующую Kotlin Multiplatform (KMP).
Миграция использования Room из существующей кодовой базы Android в общий общий модуль KMP может сильно различаться по сложности в зависимости от используемых API Room или от того, использует ли кодовая база Coroutines. В этом разделе представлены некоторые рекомендации и советы при попытке перенести использование Room в общий модуль.
Важно сначала ознакомиться с различиями и недостающими функциями между версией Room для Android и версией KMP, а также с соответствующей настройкой. По сути, успешная миграция включает в себя рефакторинг использования API-интерфейсов SupportSQLite*
и замену их API-интерфейсами драйверов SQLite, а также перемещение объявлений Room (аннотированных классов @Database
, объектов DAO, сущностей и т. д.) в общий код.
Прежде чем продолжить, еще раз просмотрите следующую информацию:
В следующих разделах описаны различные шаги, необходимые для успешной миграции.
Переход с поддержки SQLite на драйвер SQLite
API-интерфейсы в androidx.sqlite.db
предназначены только для Android, и любые варианты использования необходимо реорганизовать с помощью API-интерфейсов драйверов SQLite. В целях обратной совместимости, а также до тех пор, пока RoomDatabase
настроена с помощью SupportSQLiteOpenHelper.Factory
(т. е. SQLiteDriver
не установлен), Room ведет себя в «режиме совместимости», где API-интерфейсы поддержки SQLite и драйвера SQLite работают должным образом. Это обеспечивает инкрементную миграцию, поэтому вам не нужно преобразовывать все используемые вами службы поддержки SQLite в драйвер SQLite за одно изменение.
Следующие примеры представляют собой распространенное использование поддержки SQLite и их аналогов драйвера SQLite:
Поддержка SQLite (от)
Выполнить запрос без результата
val database: SupportSQLiteDatabase = ...
database.execSQL("ALTER TABLE ...")
Выполнить запрос с результатом, но без аргументов
val database: SupportSQLiteDatabase = ...
database.query("SELECT * FROM Pet").use { cursor ->
while (cusor.moveToNext()) {
// read columns
cursor.getInt(0)
cursor.getString(1)
}
}
Выполнить запрос с результатом и аргументами
database.query("SELECT * FROM Pet WHERE id = ?", id).use { cursor ->
if (cursor.moveToNext()) {
// row found, read columns
} else {
// row not found
}
}
Драйвер SQLite (для)
Выполнить запрос без результата
val connection: SQLiteConnection = ...
connection.execSQL("ALTER TABLE ...")
Выполнить запрос с результатом, но без аргументов
val connection: SQLiteConnection = ...
connection.prepare("SELECT * FROM Pet").use { statement ->
while (statement.step()) {
// read columns
statement.getInt(0)
statement.getText(1)
}
}
Выполнить запрос с результатом и аргументами
connection.prepare("SELECT * FROM Pet WHERE id = ?").use { statement ->
statement.bindInt(1, id)
if (statement.step()) {
// row found, read columns
} else {
// row not found
}
}
API-интерфейсы транзакций базы данных доступны непосредственно в SupportSQLiteDatabase
с помощью beginTransaction()
, setTransactionSuccessful()
и endTransaction()
. Они также доступны через Room с помощью runInTransaction()
. Перенесите эти способы использования в API-интерфейсы драйверов SQLite.
Поддержка SQLite (от)
Выполнить транзакцию (используя RoomDatabase
)
val database: RoomDatabase = ...
database.runInTransaction {
// perform database operations in transaction
}
Выполнить транзакцию (используя SupportSQLiteDatabase
)
val database: SupportSQLiteDatabase = ...
database.beginTransaction()
try {
// perform database operations in transaction
database.setTransactionSuccessful()
} finally {
database.endTransaction()
}
Драйвер SQLite (для)
Выполнить транзакцию (используя RoomDatabase
)
val database: RoomDatabase = ...
database.useWriterConnection { transactor ->
transactor.immediateTransaction {
// perform database operations in transaction
}
}
Выполнить транзакцию (используя SQLiteConnection
)
val connection: SQLiteConnection = ...
connection.execSQL("BEGIN IMMEDIATE TRANSACTION")
try {
// perform database operations in transaction
connection.execSQL("END TRANSACTION")
} catch(t: Throwable) {
connection.execSQL("ROLLBACK TRANSACTION")
}
Различные переопределения обратного вызова также необходимо перенести на их аналоги в драйверах:
Поддержка SQLite (от)
Подклассы миграции
object Migration_1_2 : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
// ...
}
}
Подклассы спецификации автоматической миграции
class AutoMigrationSpec_1_2 : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
// ...
}
}
Подклассы обратного вызова базы данных
object MyRoomCallback : RoomDatabase.Callback {
override fun onCreate(db: SupportSQLiteDatabase) {
// ...
}
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
// ...
}
override fun onOpen(db: SupportSQLiteDatabase) {
// ...
}
}
Драйвер SQLite (для)
Подклассы миграции
object Migration_1_2 : Migration(1, 2) {
override fun migrate(connection: SQLiteConnection) {
// ...
}
}
Подклассы спецификации автоматической миграции
class AutoMigrationSpec_1_2 : AutoMigrationSpec {
override fun onPostMigrate(connection: SQLiteConnection) {
// ...
}
}
Подклассы обратного вызова базы данных
object MyRoomCallback : RoomDatabase.Callback {
override fun onCreate(connection: SQLiteConnection) {
// ...
}
override fun onDestructiveMigration(connection: SQLiteConnection) {
// ...
}
override fun onOpen(connection: SQLiteConnection) {
// ...
}
}
Подводя итог, замените использование SQLiteDatabase
на SQLiteConnection
, когда RoomDatabase
недоступна, например, в переопределениях обратного вызова ( onMigrate
, onCreate
и т. д.). Если RoomDatabase
доступна, получите доступ к базовому соединению с базой данных, используя RoomDatabase.useReaderConnection
и RoomDatabase.useWriterConnection
вместо RoomDatabase.openHelper.writtableDatabase
.
Преобразование блокирующих функций DAO в функции приостановки
Версия Room для KMP использует сопрограммы для выполнения операций ввода-вывода в настроенном CoroutineContext
. Это означает, что вам необходимо перенести все блокирующие функции DAO для приостановки функций.
Блокировка функции DAO (от)
@Query("SELECT * FROM Todo")
fun getAllTodos(): List<Todo>
Приостановка функции DAO (чтобы)
@Query("SELECT * FROM Todo")
suspend fun getAllTodos(): List<Todo>
Миграция существующих функций блокировки DAO для приостановки функций может оказаться сложной, если существующая кодовая база еще не включает сопрограммы. Обратитесь к разделу «Сопрограммы в Android» , чтобы начать использовать сопрограммы в вашей кодовой базе.
Преобразование реактивных типов возврата в Flow
Не все функции DAO должны быть приостанавливаемыми. Функции DAO, которые возвращают реактивные типы, такие как LiveData
или Flowable
RxJava, не следует преобразовывать в функции приостановки. Однако некоторые типы, такие как LiveData
, несовместимы с KMP. Функции DAO с реактивными типами возврата необходимо перенести в потоки сопрограмм.
Несовместимый тип KMP (от)
@Query("SELECT * FROM Todo")
fun getTodosLiveData(): LiveData<List<Todo>>
Совместимый тип КМП (к)
@Query("SELECT * FROM Todo")
fun getTodosFlow(): Flow<List<Todo>>
Обратитесь к разделу «Потоки в Android» , чтобы начать использовать потоки в своей кодовой базе.
Установите контекст сопрограммы (необязательно)
RoomDatabase
можно дополнительно настроить с помощью общих исполнителей приложений с помощью RoomDatabase.Builder.setQueryExecutor()
для выполнения операций с базой данных. Поскольку исполнители несовместимы с KMP, API-интерфейс Room setQueryExecutor()
недоступен для общих источников. Вместо этого RoomDatabase
необходимо настроить с помощью CoroutineContext
. Контекст можно установить с помощью RoomDatabase.Builder.setCoroutineContext()
, если он не установлен, то RoomDatabase
по умолчанию будет использовать Dispatchers.IO
.
Установите драйвер SQLite
После переноса использования поддержки SQLite в API-интерфейсы драйверов SQLite необходимо настроить драйвер с помощью RoomDatabase.Builder.setDriver
. Рекомендуемый драйвер — BundledSQLiteDriver
. См. Реализации драйверов для описания доступных реализаций драйверов.
Пользовательский SupportSQLiteOpenHelper.Factory
, настроенный с помощью RoomDatabase.Builder.openHelperFactory()
не поддерживается в KMP. Функции, предоставляемые пользовательским открытым помощником, необходимо будет повторно реализовать с помощью интерфейсов драйвера SQLite.
Объявления о перемещении помещений
После завершения большинства этапов миграции можно переместить определения помещений в общий исходный набор. Обратите внимание, что стратегии expect
/ actual
могут использоваться для постепенного перемещения определений, связанных с комнатой. Например, если не все блокирующие функции DAO можно перенести в функции приостановки, можно объявить expect
аннотированный интерфейс @Dao
, который пуст в общем коде, но содержит блокирующие функции в Android.
// shared/src/commonMain/kotlin/Database.kt
@Database(entities = [TodoEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun getDao(): TodoDao
abstract fun getBlockingDao(): BlockingTodoDao
}
@Dao
interface TodoDao {
@Query("SELECT count(*) FROM TodoEntity")
suspend fun count(): Int
}
@Dao
expect interface BlockingTodoDao
// shared/src/androidMain/kotlin/BlockingTodoDao.kt
@Dao
actual interface BlockingTodoDao {
@Query("SELECT count(*) FROM TodoEntity")
fun count(): Int
}