Chapters

Hide chapters

Saving Data on Android

Second Edition · Android 11 · Kotlin 1.5 · Android Studio 4.2

Using Firebase

Section 3: 11 chapters
Show chapters Hide chapters

11. Data Migration
Written by Subhrajyoti Sen

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

In the last chapter, you finally learned how to integrate your Room components with other architecture components like LiveData and ViewModel to make your app display a nice set of questions to your users.

But what happens if you want to modify your database schema to organize questions by category or difficulty?

Well, in this chapter you’ll learn how Room helps you predictably change your database schema by providing migrations that help you deal with your data.

Along the way you’ll learn:

  • How to create a migration.
  • How to add a migration to your database.
  • How to perform SQLite queries.
  • How to fall back to a destructive migration.

Ready? It’s time to get started.

Getting started

To begin, open the starter project in Android Studio 4.2 or greater by going to File ▸ Open and selecting the project from this chapter’s attachments.

If you’ve been following along up to this point, you should already be familiar with the code. If you’re just getting started, here’s a quick recap:

  • The data package contains two packages: db and model. db contains QuestionDatabase, which implements your Room database. The model package contains your entities, Question and Answer. It also includes Repository, which helps your ViewModels interact with your DAOs.
  • The view package contains all your activities: SplashActivity, MainActivity, QuestionActivity and ResultActivity.
  • The viewmodel package contains ViewModels of your classes: MainViewModel and QuestionViewModel.

Build and run the app on a device or emulator.

The Main Screen.
The Main Screen.

Important: Prepopulate the database and then tap START to start a quiz.

Cool! Now, it’s time to start working with migrations.

Migrations

Before creating your first migrations with Room, you need to learn what migrations are, right?

Understanding Room migrations

SQLite handles database migrations by specifying a version number for each database schema you create. In other words, each time you modify your database schema by creating, deleting or updating a table, you have to increase the database version number and modify SQLiteOpenHelper.onUpgrade(). onUpGrade() will tell SQLite what to do when one database version changes to another.

Exporting schemas

It’s considered good practice to start exporting the schema before you start writing your first migration. The export is a JSON representation of the database schema. This representation comes in very handy when you want to understand the changes taking place over various database versions.

@Database(entities = [(Question::class), (Answer::class)],
  version = 1,
  exportSchema = true
)
kapt {
  arguments {
    arg("room.schemaLocation", "$projectDir/schemas".toString())
  }
}

Creating Room migrations

Right now, you have a very nice app that displays a series of random questions to your users. You store these questions in a questions table, which is represented as a Question entity class in your code.

@Entity(tableName = "questions", indices = [Index("question_id")])
data class Question(
  @PrimaryKey(autoGenerate = true)
  @ColumnInfo(name = "question_id")
  var questionId: Int,
  val text: String,
  val difficulty: Int = 0 	// Only this line changes
)
A Dialog informing that the app crashed.
I Vearid odvekrizx zwut vzu ogd mcowbiz.

The App Crash error.
Mto Ics Xhuxh uplug.

Upgrading the database version

Each time you change the database schema, you need to change the database version. This will help Room know which migrations to run when building the database.

@Database(
  entities = [(Question::class), (Answer::class)], 
  version = 2, // version change
  exportSchema = true
) 
App has crashed again.
Ufz vew pnebfez esuiz.

The App Crash error.
Bda Elt Ybedb umqux.

Implementing a migration

Create a new package under the db package and name it migrations.

class Migration1To2 : Migration(1, 2) {
}
class Migration1To2 : Migration(1, 2) {
  override fun migrate(database: SupportSQLiteDatabase) {
    TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
  }
}
override fun migrate(database: SupportSQLiteDatabase) {
  database.execSQL("ALTER TABLE questions ADD COLUMN difficulty INTEGER NOT NULL DEFAULT 0")
}
companion object {
  val MIGRATION_1_TO_2 = Migration1To2()
}
database = Room.databaseBuilder(this, QuizDatabase::class.java, "question_database")
    .addMigrations(QuizDatabase.MIGRATION_1_TO_2) // Only this line changes
    .build()
Migration works correctly.
Xejfuvaun fucww vivboptpg.

Changing column type in the schema

What happens if you need to modify a previously created column?

@Entity(tableName = "questions", indices = [Index("question_id")])
data class Question(
  @PrimaryKey(autoGenerate = true)
  @ColumnInfo(name = "question_id")
  var questionId: Int,
  val text: String,
  val difficulty: String = "0", // Only this line changes  
)
class Migration2To3 : Migration(2, 3) {
  override fun migrate(database: SupportSQLiteDatabase) {
    database.execSQL(
      "CREATE TABLE question_new (question_id INTEGER NOT NULL, " +
          "text TEXT NOT NULL, " +
          "difficulty TEXT NOT NULL, " +
          "PRIMARY KEY (question_id))"
    ) //1

    database.execSQL("CREATE INDEX index_question_new_question_id ON question_new(question_id)") //2

    database.execSQL(
      "INSERT INTO question_new (question_id, text, difficulty) " +
          "SELECT question_id, text, difficulty FROM questions"
    )//3

    database.execSQL("DROP TABLE questions") //4

    database.execSQL("ALTER TABLE question_new RENAME TO questions") //5

  }
}
@Database(
  entities = [(Question::class), (Answer::class)], 
  version = 3, // Changes the db version
 	exportSchema = true
)	
abstract class QuizDatabase : RoomDatabase() {
  abstract fun quizDao(): QuizDao
  
   companion object{
    val MIGRATION_1_TO_2 = Migration1To2()
    val MIGRATION_2_TO_3 = Migration2To3() // Adds a reference to your new migration
  }
}
database = Room.databaseBuilder(this, QuizDatabase::class.java, DB_NAME)
  .addMigrations(QuizDatabase.MIGRATION_1_TO_2, QuizDatabase.MIGRATION_2_TO_3)
  .build()
Second migration works correctly.
Puzakq susqeloiy zatfz migjuftxd.

Creating a direct migration

You might have noticed that you now have two different migrations for three different versions of your database. If one of your users had the first version of your database installed and wanted to update the app to the latest version, Room would execute each migration one by one. Since 4 is still a relatively low number, the process should be quick, but imagine if you had 50 versions of your database! It would be much better to have a shortcut right?

class Migration1To3 : Migration(1, 3) {
  override fun migrate(database: SupportSQLiteDatabase) {
    database.execSQL("ALTER TABLE questions ADD COLUMN difficulty TEXT NOT NULL DEFAULT '0'")
  }
}
val MIGRATION_1_TO_3 = Migration1To3()
database = Room.databaseBuilder(this, QuizDatabase::class.java, DB_NAME)
  .addMigrations(
    QuizDatabase.MIGRATION_1_TO_2, 
    QuizDatabase.MIGRATION_2_TO_3,
    QuizDatabase.MIGRATION_1_TO_3
  )
  .build()

Automated migrations

The migrations that you wrote in the previous section can seem like a lot of code to achieve simple changes. Luckily, from version 2.4.0-alpha01 onwards, Room supports automatic migrations.

@RenameColumn(
  tableName = "questions", 
  fromColumnName = "difficulty", 
  toColumnName = "challengeLevel"
)
class Migration3To4 : AutoMigrationSpec
autoMigrations = [
  AutoMigration(
    from = 3,
    to = 4,
    spec = Migration3To4::class
  )
]
@Database(entities = [(Question::class), (Answer::class)],
  version = 4,
  exportSchema = true,
  autoMigrations = [
    AutoMigration(
      from = 3,
      to = 4,
      spec = Migration3To4::class
    )
  ]
)
Migration worked correctly
Notlemaul yiqqut pednalckw

Key points

  • Simply put, a database or schema migration is the process of moving your data from one database schema to another.
  • SQLite handles database migrations by specifying a version number for each database schema that you create.
  • Room provides an abstraction layer on top of the traditional SQLite migration methods with Migration.
  • Migration(startVersion, endVersion) is the base class for a database migration. It can move between any two migrations defined by the startVersion and endVersion parameters.
  • fallbackToDestructiveMigration() tells Room to destructively recreate tables if you haven’t specified a migration.

Where to go from here?

By now, you should have a very good idea of how Room migrations work. Of course, the process will differ from project to project, since the queries you’ll need to execute will depend on your database schema, but the basic idea is always the same:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now