Если вы работали с сохранением данных в приложениях Android, то вы, вероятно, знакомы с библиотеками ORM (Object-Relational Mapping).

ORM упрощают написание SQL-запросов с использованием объектно-ориентированной парадигмы, в отличие от непосредственного использования языка SQL. Марио Хойос написал отличную статью об ORM, которую вы можете прочитать здесь. Дальнейшее чтение по ORM можно найти здесь.

В Android одними из наиболее распространенных ORM являются Realm и Room. Я никогда не работал с Realm, но я предполагаю, что идея почти такая же, как их использовать.

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

В этой статье я расскажу вам о простой миграции с использованием Kotlin.

Первое, что нужно сделать при работе с Room, — это создать синглтон вашей базы данных. Используя шаблон Singleton, вы предотвращаете создание в приложении нескольких экземпляров одной и той же базы данных, что в противном случае могло бы вызвать утечку памяти.

@Database(
    entities = [
        Users::class
    ],
        version = 1,
        exportSchema = false)
@TypeConverters( )
abstract class AppDatabase : RoomDatabase(){
abstract fun usersDao(): UsersDao 
   
companion object {
        @Volatile private var instance: AppDatabase?=null
        private val LOCK = Any()
        operator fun invoke(context: Context) = instance ?: synchronized(LOCK){
            instance ?: buildDatabase(context).also { instance = it}
        }
        
private fun buildDatabase(context: Context) = androidx.room.Room.databaseBuilder(context,
            AppDatabase::class.java, "users.database")
            .build()
    }
}

Приведенный выше код показывает, как настроить один экземпляр вашей базы данных. Часть конфигурации включает настройку абстрактных классов для DAO (объектов доступа к данным), в которые я не буду вдаваться.

Далее мы создаем таблицу User. Это таблица, на которой мы будем делать чередования.

@Entity(tableName = "users_table")
data class Users(
@PrimaryKey(autoGenerate = true)
   var id: Int,
var name: String?,
var email: String?
)

Наша таблица пользователей настроена так, что она имеет автоматически сгенерированный первичный ключ, имя типа String и адрес электронной почты также типа String. Я не буду писать DAO для этой таблицы.

Предположим теперь, что мы изменили столбец «name» на «user_name» следующим образом:

...//
var user_name: String?,
..//

Если запустить наше приложение после этого, мы получим следующую ошибку:

Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. 
You can simply fix this by increasing the version number.

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

@Database(
    entities = [
        Users::class
    ],
        version = 2, // change this here to a number higher than 1
        exportSchema = false)

Это все равно приведет к ошибке, когда Room не сможет установить подлинность данных, поскольку схема все еще отличается, а миграции не были предоставлены.

Быстрое исправление с использованием деструктивной миграции

Быстрым решением для этого будет использование деструктивных миграций. Когда мы используем деструктивные миграции, по сути, мы говорим Room уничтожить и воссоздать все таблицы. Проблема в том, что наши пользователи потеряют все свои данные. Это сработает, если вы все еще находитесь на этапе разработки и практически единственный, кто использует/тестирует приложение.

...//
private fun buildDatabase(context: Context) = androidx.room.Room.databaseBuilder(context,
            AppDatabase::class.java, "users.database")
            .fallbackToDestructiveMigration() // destructive migration
            .build()
    }
...//

Внедрение миграций для сохранения предыдущих данных

Если вы не хотите, чтобы пользователь потерял свои данные при обновлении, лучший вариант — правильно выполнить миграцию. Я знаю, миграции…

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

Итак, давайте возьмем предыдущий пример изменения имени столбца с «name» на «user_name» в таблице «Пользователи».

Миграция будет выглядеть примерно так:

...//
// migration to alter the user_name in Users table
private val MIGRATION
Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. 
You can simply fix this by increasing the version number.
2 = object : Migration(1,2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE users_table RENAME name TO user_name") } } ...//

После этого вам нужно добавить миграции в базу данных.

Несколько замечаний:
1. На самом деле имя MIGRATION

Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. 
You can simply fix this by increasing the version number.
2 может быть любым, но принято называть его и добавлять к нему номер предыдущей и следующей версии базы данных
2. Однако Migration(1,2) должен совпадать с номером предыдущей и следующей версии вашей базы данных.

Итак, теперь давайте добавим миграцию в нашу базу данных:

...//
private fun buildDatabase(context: Context) = androidx.room.Room.databaseBuilder(context,
    AppDatabase::class.java, "users.database")
    .addMigrations(MIGRATION
Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. 
You can simply fix this by increasing the version number.
2) // ADD MIGRATION .build() ...//

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

Вот и все. Легко это делает.

Дополнительную информацию о миграции можно найти на ресурсах ниже:

  1. https://developer.android.com/reference/android/arch/persistence/room/RoomDatabase.Builder
  2. https://developer.android.com/training/data-storage/room/migrating-db-versions