Android
April 16, 2023

Как настроить автомиграцию в Room

В этой статье я расскажу про автомиграцию в Room (версия не ниже Room 2.4.0), варианты ее использования, преимущества, которые она дает, и проблемы, с которыми мы можем столкнуться.

В ходе развития продукта бывают ситуации, когда требуется изменить структуру таблиц в базе данных в приложении. В этом случае встает вопрос миграции данных. Есть два варианта решения:

  1. Не делаем миграцию данных. Удаляем старую базу данных (далее — БД), создаем БД с новой структурой и загружаем данные с сервера. Плюс решения – меньше времени на разработку. Минус решения – негативное впечатление для пользователя (клиента). На конкурентном зрелом рынке удержать клиента в несколько раз дешевле, чем привлечь новых. Поэтому стоит сделать другой вариант.
  2. Делаем миграцию данных. При изменении структуры базы данных разработчик пишет код для миграции данных из старых таблиц в новые. Для пользователя миграция данных произойдет незаметно. Минус решения — дополнительное время на написание кода. Чтобы уменьшить время на разработку, следует использовать автомиграцию.

Автомиграция поможет упростить процесс работы с базой данных, особенно если она большая, и в нее часто вносятся изменения. Она позволит не писать самостоятельно сложные SQL-запросы, а сгенерирует их автоматически в специально отведенной директории. Это позволит избежать опечаток при миграции и сэкономить время (если у нас запросы с огромным количеством полей).

Дисклеймер: в этой статье я буду ориентироваться на то, что мы уже умеем работать с Room и знаем, для чего нужна миграция. Материал будет полезен Android-разработчикам уровня Middle.

FallbackToDestructiveMigration

Прежде чем приступим к основной теме, хотелось бы упомянуть нежелательный способ fallbackToDestructiveMigration.

Вспомним, как он работает: если при запуске приложения Room поймет, что нужна миграция, то он пересоздаст базу в соответствии с новой структурой Entity классов, и все данные пропадут. А это уже сомнительный вариант с точки зрения пользователя, поскольку он потеряет все свои данные.

Когда может возникнуть проблема fallbackToDestructiveMigration:

  • Если база данных используется только для кэширования и для пользователя не несет особой ценности.
  • Если в какой-то из уже прошедших миграций обнаружен сбой, мы можем добавить fallbackToDestructiveMigrationFrom.

Если же этот способ нам не подходит, то необходимо настраивать миграцию. Для этого понадобится писать SQL-запросы. Но в SQL есть ограничения, которые усложняют написание запросов. Рассмотрим их в следующем пункте.

Migration – old way

Один из примеров ограничений в написании запросов: SQLite поддерживает ADD column, но не поддерживает RENAME column, REMOVE column.

То есть если понадобится переименовать или удалить столбцы, то нам придется писать несколько запросов.

Пример запроса по удалению столбца из таблицы:

Алгоритм действий:

  • создаем новую таблицу с другим названием;
  • переносим в нее нужные столбцы (кроме того, который хотели удалить) из старой;
  • избавляемся от старой таблицы;
  • переименовываем новую таблицу в старую.

Избавиться от подобных сложных цепочек запросов поможет автомиграция.

Преимущества автомиграции

Автомиграция не требует написания SQL-запросов и сочетается с обычной миграцией. При этом она позволяет делать следующие операции:

  • менять тип полей;
  • добавлять, удалять и переименовывать поля;
  • добавлять и удалять таблицы в database.

На примере ниже рассмотрим процесс добавления и изменения типа поля:

Добавляем health и меняем тип поля damage на Double:

Таким образом мы добавили массив автомиграций в классе Database.

Миграция готова, при этом она сама генерирует сложную цепочку SQL-запросов.

Но обратите внимание, что если мы не укажем дефолтное значение для новых полей, то столкнемся со следующей ошибкой:

error: New NOT NULL column'health' added with no default value specified. Please specify the default value using @ColumnInfo.

public abstract class AppDatabase extends androidx.room.RoomDatabase {

Это произошло потому, что в созданной ранее таблице не было поля health. Нам необходимо указать defaultValue для него. Если у этого поля нулабельное значение, то автоматически будет проставлено null.

Автоматическая генерация классов миграции

Может показаться, что сложно отследить, какие именно были изменения при повышении версии БД, особенно при добавлении полей.

Но это не так — всю необходимую информацию мы можем посмотреть в автоматически сгенерированных файлах по этому пути: build/generated/source/kapt

Внутри можем увидеть такие же запросы, которые мы бы писали:

• создание новой таблицы с другим названием;
• перенос в нее нужных столбцов (кроме того, который хотели удалить) из старой;
• удаление старой таблицы;
• переименование новой таблицы в старую.

Для новых полей (на этой картинке — это поле health) прописывается дефолтное значение, которые мы указали через аннотацию columInfo ранее.

Автоматическая генерация классов таблиц

При необходимости мы можем посмотреть автоматически сгенерированные классы всех таблиц:

Класс-аннотация AutoMigration

Класс AutoMigration имеет два метода для указания версии и метод Spec, который нужно переопределить при сложных миграциях. Последний разберем в следующем пункте.

Класс-аннотация AutoMigration позволяет легко менять поля и добавлять новые. Автомиграция сама определяет, какие поля были добавлены или изменены, и легко генерирует SQL-запросы.

Но если требуется удалить или переименовать название таблицы или столбца, библиотека не может определить, с какими именно полями идет работа. Чтобы дать ей эту информацию, необходимо использовать следующие аннотации. Названия говорят сами за себя.

@DeleteTable (tableName)
@RenameTable (fromTableName, toTableName)
@DeleteColumn (tableName, columnName)
@RenameColumn (tableName, fromColumnName, toColumnName)

Как переименовать таблицы с помощью аннотаций

Для этого нужно изменить название таблицы из Warriors на GreatWarriors. Вносим нужные изменения в классы Entity (вводим tableName) и Dao (везде меняем название таблицы, где оно использовалось):

Важные изменения, необходимые в Database классе

На этом этапе создаем свой класс AutomigraionFrom2to3 (желательно в названии класса отразить версию, чтобы было сразу понятно, о какой именно версии в нем идет речь), который должен обязательно наследоваться от AutimogrationSpec. Вводим дополнительные строки в наш массив автомиграций вверху класса, в параметр spec.

Как видите, мы использовали класс-аннотацию RenameTable и ввели параметры, которые сообщают библиотеке о том, в какой таблице изменится название.

Для операций удаления достаточно внести соответствующие аннотации.

AutoMigrationSpec

Что нужно учитывать при расширении AutoMigrationSpec:

  • наш класс должен быть с пустым конструктором (иначе не скомпилится);
  • нельзя указывать одинаковые аннотации (Entries).

Посмотрим на интерфейс изнутри:

Есть единственный метод onPostMigrate, который необязательно переопределять. Он может понадобиться в тех ситуациях, когда нам нужно совершить операции с базой данных после того, как выполнена миграция (например, получить курсор и через db.query делать запросы к БД).

Например, мы поменяли тип поля во время миграции со String на Int, а нам нужно поменять не только тип, но и значение. В этом случае нам поможет метод onPostMigrate. Через класс SupportSQLiteDatabase мы можем получить курсор и делать любые запросы к БД.

Сложный пример удаления полей из двух таблиц и удаление третьей таблицы в одной миграции

Мы не сможем указывать одинаковые аннотации (для работы с разными полями или таблицами), но внутри одной аннотации мы можем выполнить операции с разными таблицами (как DeleteColumn в примере).

Возможные проблемы при миграции

А)

Главное — не забыть в Gradle в блоке DefaultConfig добавить путь сохранения schemas (где мы и сможем смотреть автоматически сгенерированные таблицы).

Б) Не забудьте добавить в Gradle модуля JavaVersion.VERSION_1_8, иначе получим ошибку.

В) При первом добавлении автомиграции может возникнуть следующее:

  • Schema '.json' required for migration was not found at the schema out folder Cannot generate auto migrations

Чтобы такого не произошло, необходимо:

  • откатить версию database до последней, которая была перед добавлением автомиграции;
  • запустить проект;
  • проконтролировать, что появилась schema.json с версией-n в директории shemas;
  • поставить новую версию database. В результате должна сформироваться автомиграция в директории build/generated/source/kapt

Понижение версии

error: Downgrades are not supported in AutoMigration

В автомиграции понижение версии недоступно. Придется прибегнуть к обычной миграции.

Тестирование миграции

Добавляем в Gradle следующую информацию:

А) Пример тестирования old Migration 1 to 2 (обычная миграция, в которой, к примеру, добавляется поле damage):

  • С помощью класса MigrationTestHelper создаем таблицу TEST_DB.
  • Вставляем в нее наш класс "Warrior" при помощи SQL-команды. В таблице уже есть поля id и name.
  • При автомиграции должно добавиться поле damage с дефолтным значением 1.0 double. Проводим миграцию при помощи класса MigrationTestHelper.
  • Получаем курсор.
  • Проверяем количество позиций в таблице (должно быть 1, так как мы добавили только одну).
  • Получаем поле с ключом damage и проверяем его значение (должно быть равно дефолтному).

Б) Пример тестирования AutoMigration 2 to 3 (добавится поле weapon_name).

Всё почти то же самое, но при выполнении миграции классом MigrationTestHepler не добавляем класс Automigration, у нас же автомиграция. Главное — указать, к какой версии мы идем (в нашем случае — к версии 3).

И проверим здесь, к примеру, просто наличие поля weapon_name, а не его дефолтное значение.

В) Пример тестирования old Migration 1 to 2 + AutoMigration 2 to 3 в одном.

Схема действий похожа на предыдущие. При миграции указываем исходную версию 3, добавляем Migration_1_2 (обычную миграцию), автомиграцию (2-3) добавлять не нужно.

И проверяем наличие полей из обеих миграций damage и weapon_name.

Дело в том, что метод runMigrationsAndValidate «под капотом» сам находит автомиграции и добавляет к обычным, переданным через конструктор.

Переход с Kapt на KSP

Автогенерация кода для многих библиотек переходит с Kapt на KSP (Kotlin Symbol Processing). Room не стал исключением. Поэтому рассмотрим, какие нужно сделать изменения в Gradle для осуществления перехода.

Добавим одно изменение в Gradle проекта:

Остальные изменения касаются Gradle модуля. Добавим плагин:

Заменим в dependencies kapt на ksp

В defaultConfig изменим строки, обозначающие путь к schemas

Синхронизируем gradle и запускаем проект.

Подведем итоги

Итак, в этой статье мы разобрали процесс автомиграции в сравнении с обычной миграцией и особенности ее использования. Автомиграция позволяет не писать огромное количество кода, поскольку Room сгенерирует все сам, а также упрощает код в классе базы данных. Кроме того, она снижает риск ошибок при миграции, из-за чего могут посыпаться краши в приложении. На примерах я показал, что автомиграцию можно применять на любом этапе, если на проекте уже используется Room. Также я указал на проблемы, с которыми можно столкнуться при настройке автомиграции и привел примеры тестирования.

Удачного использования :)

Источник