Новости
Миграция продакшен приложения из Room в SQLDelight
Эта статья — ваше руководство по миграции продакшен приложения с реальной локальной базой данных с Room на SQLDelight без потери сохраненных данных.
Kotlin multiplatform (KMP), ранее известный как KMM, является интересной темой для многих Android-разработчиков, стремящихся обеспечить работу своих приложений на других платформах, например iOS.
Но одной из первых проблем, с которой они сталкиваются, являются популярные библиотеки, используемые почти в каждом Android-приложении, которые, как выясняется, не поддерживают KMP, и которые необходимо заменить на чисто Kotlin-библиотеки, чтобы начать свое путешествие по KMP. Одной из таких библиотек является Room, которая используется для простого управления локальной базой данных.
Поэтому в данном случае единственным вариантом является переход на другую библиотеку, и одним из хороших вариантов является SQLDelight.
Эта статья — ваше руководство по миграции продакшен приложения с реальной локальной базой данных с Room на SQLDelight без потери сохраненных данных.
Я исхожу из реального опыта, полученного в моем проекте с открытым исходным кодом Prayer Companion.
1. Внедрение SQLDelight
Прежде всего, необходимо внедрить в проект зависимости SQLDelight.
В приложении build.gradle.kts добавьте следующие строки (обратитесь к документации по SQLDelight для получения правильных версий):
plugins {
id("app.cash.sqldelight") version "2.0.0"
}
dependencies {
implementation("app.cash.sqldelight:android-driver:2.0.0")
implementation("app.cash.sqldelight:coroutines-extensions:2.0.0")
}
Затем синхронизируйте проект и в разделе dependencies добавьте следующий код для инициирования класса Database, имя базы данных может быть любым подходящим, это будет имя сгенерированного класса, с которым вы будете взаимодействовать в своем коде, а затем имя пакета вашего приложения
Примечание: name — это не имя файла базы данных, оно будет задано позже.
sqldelight {
databases {
create("MyAppDatabase") {
packageName.set("com.domain.app")
}
}
}
Имя пакета можно найти в том же файле класса, что и значение applicationId в блоке defaultConfig
2. Создание таблиц
SQLDelight использует другой способ создания таблиц, чем Room, вместо создания класса Entity и автогенерации таблицы, здесь вам придется писать SQL-код самостоятельно, но он прост и предлагает автодополнение.
К счастью, переход из Room упрощает задачу, у нас уже где-то сгенерирована схема создания таблиц, чтобы найти ее, найдите в своем проекте класс с именем [имя класса вашей базы данных]_Impl.java, имя класса базы данных — это класс, который наследует RoomDatabase() в вашем проекте.
Чтобы увидеть сгенерированный класс, необходимо сначала собрать проект, он будет находиться в каталоге java (generated):
Открыв сгенерированный класс, вы найдете все схемы создания таблиц. В моем случае вот что я вижу для своего проекта:

Схемы создания таблиц базы данных Room
Убедитесь, что схемы в SQLDelight написаны точно так же, как и в Room, иначе, скорее всего, возникнут ошибки.
Теперь, чтобы написать схему создания для SQLDelight, откройте проект, затем в app -> src -> main создайте новый каталог sqldelight, а внутри него — каталог tables:
В каталоге tables создайте для каждой таблицы новый файл с расширением .sq.
Этот файл будет предназначен для создания таблицы и запросов к ней. Например, файл с именем my_table.sq будет выглядеть следующим образом:
CREATE TABLE PrayersInfo (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
prayer TEXT NOT NULL,
dateTime TEXT NOT NULL,
status TEXT
);
insert:
INSERT INTO PrayersInfo VALUES(:id,:prayer,:dateTime,:status);
getPrayers:
SELECT * FROM PrayersInfo WHERE dateTime >= :startDateTime
AND dateTime <= :endDateTime;
getPrayer:
SELECT * FROM PrayersInfo WHERE prayer = :prayer
AND dateTime >= :startOfDay AND dateTime <= :endOfDay;
getPrayersStatusesByDate:
SELECT status FROM PrayersInfo WHERE dateTime >= :startDateTime
AND dateTime <= :endDateTime AND prayer != :excludedPrayer;
updatePrayerStatus:
UPDATE PrayersInfo SET status = :status WHERE dateTime >= :startOfDay
AND dateTime <= :endOfDay AND prayer = :prayer;
delete:
DELETE FROM PrayersInfo WHERE dateTime >= :startDateTime
AND dateTime <= :endDateTime;
Логика:
- Схема создания таблицы
Будет выполняться только один раз, вам не нужно беспокоиться об этом - Все ваши запросы в следующей форме
Примечание: ключевые слова написаны заглавными буквами,fromне будет распознан, его нужно писать какFROM
Примечание2: SQLDelight сгенерирует Kotlin-функции для использования в вашем проекте для всех этих запросов
functionName: Query;
Затем создайте проект и приступайте к замене Room.
2.1. Перед заменой Room
Если вы работаете над уже выпущенным приложением, то, скорее всего, сейчас у вас номер версии больше 1.
В SQLDelight мы не указываем номер версии, как в Room,
SQLDelight генерирует номер версии в зависимости от количества файлов миграции + 1.
Чтобы разобраться с этим, в том же каталоге sqldelight, который мы создали выше, создайте новый каталог с названием migrations и создайте файл для каждой версии, с которой вы мигрировали, в следующей форме именования:
[version your migrating from].sqm
Таким образом, если вы находитесь на версии 3, вам необходимо создать два файла миграции 1.sqm и 2.sqm, и SQLDelight поймет, что вы находитесь на версии 3.
Убедитесь, что в этих файлах написаны утверждения миграции, аналогично тому, как ручная миграция работала в Room, только если вы использовали AutoMigration в Room, то вам придется написать и это.
Обратитесь к документации SQLDelight, если у вас возникли трудности с написанием миграции
В моем случае я создал новую таблицу при переходе на версию 2, поэтому вот как выглядел 1.sqm для меня:
3. Заменить инициализацию базы данных
Найдите место, где вы создаете базу данных Room databaseBuilder, и замените ее на SQLDelight, обязательно реализуя класс Database из сгенерированных файлов (имя класса Database будет таким же, как вы написали в файле build.gradle.kts), а не через вашу старую базу данных Room.
ВАЖНО: используйте такое же точное имя базы данных (в кавычках, например, «prayer-companion»), как и имя файла вашей базы данных, иначе SQLDelight создаст новую базу данных с пустыми данными.
val driver: SqlDriver = AndroidSqliteDriver(PrayerCompanionDatabase.Schema, applicationContext, "prayer-companion") return PrayerCompanionDatabase(driver)
Затем удалите класс RoomDatabase, он вам больше не нужен.
3.1. Перенос интерфейса Dao
Я хотел сохранить все в проекте неизменным, поэтому вместо того, чтобы заменять интерфейс классом, я просто создал новый класс Dao, наследующий от интерфейса Dao.
Таким образом, для этого Dao @Dao interface PrayersInfoDao я создал новый класс class PrayersInfoDaoImpl: PrayersInfo, затем я сгенерировал все функции точно так же, как они есть в интерфейсе.
В конструкторе можно инжектировать/передать сгенерированный класс Database:
class PrayersInfoDaoImpl @Inject constructor(
db: PrayerCompanionDatabase
) : PrayersInfoDao { ... }
Затем получить ссылку на запросы к таблице, которые генерирует для вас SQLDelight:
class PrayersInfoDaoImpl @Inject constructor(
db: PrayerCompanionDatabase
) : PrayersInfoDao {
private val queries = db.prayersInfoQueries
}
Далее использование запросов очень простое, запросы, которые вы написали в файле .sq, теперь готовы к использованию, для вставки строки, например, вы напишете что-то вроде этого:
override fun insert(prayerInfo: PrayerInfoEntity) {
queries.insert(
id = null,
prayer = prayerInfo.prayer.name,
dateTime = converters.localDateToString(prayerInfo.dateTime),
status = prayerInfo.status.name
)
}
Примечание: id является null, поэтому мы можем позволить SQLDelight выполнить автоинкремент.
Однако при работе с запросами возникают некоторые сложности
a. Чтение строк
Чтение строк может быть не совсем интуитивным:
override fun getPrayers(
startDateTime: LocalDateTime,
endDateTime: LocalDateTime
): List<PrayerInfoEntity> {
return queries.getPrayers(
startDateTime = converters.localDateToString(startDateTime),
endDateTime = converters.localDateToString(endDateTime)
)
}
В приведенном ниже случае вызов getPrayers вернет Query<PrayersInfo>, в то время как ожидаемым результатом будет List<PrayerInfoEntity>, чтобы сначала сопоставить класс данных с классом сущности, для каждого запроса есть две функции запроса, одна, как описано выше, только с необходимыми параметрами, а другая — с дополнительным параметром отображения функции, который можно использовать для сопоставления класса данных с классом сущности:
Я всегда предпочитаю не использовать сгенерированные классы данных, так как в дальнейшем становится сложнее что-либо изменить или добавить вспомогательные функции.
Использование второй функции будет выглядеть примерно так — обычная mapper функция на выбранный вами класс данных:
override fun getPrayers(
startDateTime: LocalDateTime,
endDateTime: LocalDateTime
): List<PrayerInfoEntity> {
return queries.getPrayers(
startDateTime = converters.localDateToString(startDateTime),
endDateTime = converters.localDateToString(endDateTime)
) { id, prayer, dateTime, status ->
// mapping PrayersInfo to PrayerInfoEntity
PrayerInfoEntity(
id = id.toInt(),
prayer = Prayer.valueOf(prayer),
dateTime = converters.stringToLocalDate(dateTime),
status = status?.let { status -> PrayerStatus.valueOf(status) }
?: PrayerStatus.None
)
}
}
А для превращения Query<T> в List<T> вызовем .executeAsList():
override fun getPrayers(
startDateTime: LocalDateTime,
endDateTime: LocalDateTime
): List<PrayerInfoEntity> {
return queries.getPrayers(
startDateTime = converters.localDateToString(startDateTime),
endDateTime = converters.localDateToString(endDateTime)
) { id, prayer, dateTime, status ->
PrayerInfoEntity(
id = id.toInt(),
prayer = Prayer.valueOf(prayer),
dateTime = converters.stringToLocalDate(dateTime),
status = status?.let { status -> PrayerStatus.valueOf(status) }
?: PrayerStatus.None
)
}.executeAsList()
// transforming Query into List 👆
}
b. Чтение строк в Flow
Чтобы читать строки из таблицы в потоке, убедитесь, что у вас реализована зависимость расширений корутинов для SQLDelight:
implementation("app.cash.sqldelight:coroutines-extensions:2.0.0")
Тогда вместо вызова executeAsList мы вызываем asFlow(), а затем mapToList(DispatcherContext):
override fun getPrayersFlow(
startDateTime: LocalDateTime,
endDateTime: LocalDateTime
): Flow<List<PrayerInfoEntity>> {
return queries.getPrayers(
startDateTime = converters.localDateToString(startDateTime),
endDateTime = converters.localDateToString(endDateTime)
) { id, prayer, dateTime, status ->
PrayerInfoEntity(
id = id.toInt(),
prayer = Prayer.valueOf(prayer),
dateTime = converters.stringToLocalDate(dateTime),
status = status?.let { PrayerStatus.valueOf(it) } ?: PrayerStatus.None
)
}.asFlow().mapToList(Dispatchers.IO)
// transform into a flow list 👆
}
c. Вставка списка
Обычно для вставки нескольких строк сразу у нас есть функция insertAll, но, к сожалению, SQLDelight не поддерживает ее из коробки, поэтому нам придется делать это самостоятельно.
В этом случае мы используем функцию queries.transaction { } для вставки всех строк или ни одной в случае ошибки, что будет выглядеть примерно так:
override fun insertAll(prayersInfo: List<PrayerInfoEntity>) {
queries.transaction {
prayersInfo.forEach {
queries.insert(
id = null,
prayer = it.prayer.name,
dateTime = converters.localDateToString(it.dateTime),
status = it.status.name
)
}
}
}
А если вы хотите получать информацию об ошибке, то можно добавить это:
override fun insertAll(prayersInfo: List<PrayerInfoEntity>) {
queries.transaction {
// if the transaction failed and rolled back, throw an exception
afterRollback { throw Exception("Failed to inserting prayers info ") }
prayersInfo.forEach {
queries.insert(
id = null,
prayer = it.prayer.name,
dateTime = converters.localDateToString(it.dateTime),
status = it.status.name
)
}
}
}
Вот и все, дальше вам нужно только:
- Удалить все аннотации Room из интерфейсов Dao
- Удалить все аннотации Room из классов данных Entities
- Удалить все зависимости Room из файлов gradle.build.kts
- Инжектировать/передать новый класс
[Dao]Implв репозиторий, который использовал интерфейс Dao, и все должно работать точно так же.
Запустите приложение и убедитесь, что все ваши данные сохранились!
Полный код реальной миграции приложения можно посмотреть в пул-реквесте PrayerCompanion.
-
Аналитика магазинов3 недели назад
Мобильный рынок Ближнего Востока: исследование Bidease и Sensor Tower выявляет драйверы роста
-
Видео и подкасты для разработчиков3 недели назад
Разбор кода: iOS-приложение для управления личными финансами на Swift. Часть 1
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2025.47
-
Разработка4 недели назад
100 уроков о том, как я довёл своё приложение до продажи за семизначную сумму







