Home Android & Kotlin Books Saving Data on Android

10
Data Migration Written by Aldo Olivares

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

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 change your database schema in a predictable way 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, which you can find in this chapter’s attachments. Then open the project in Android Studio 3.3 or greater by going to File ▸ New ▸ Import Project and selecting the build.gradle file in the root package.

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: MainActivity, QuestionActivity and ResultActivity.
  • The viewmodel package contains the ViewModels of your class: MainViewModel and QuestionViewModel.

Once the starter project finishes loading and building, run the app on a device or emulator.

Important: Tap the START button to start a quiz, then stop the app running in Android Studio.

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.

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 question table, which is represented as a Question entity class in your code.

@Entity(tableName = "question", indices = [Index("question_id")])
data class Question(
  @PrimaryKey(autoGenerate = true)
  @ColumnInfo(name = "question_id")
  var questionId: Int,
  val text: String
)
@Entity(tableName = "question", 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 lane changes
)

@Database(entities = [(Question::class), (Answer::class)], version = 2) //version change
abstract class QuizDatabase : RoomDatabase() {
  abstract fun questionsDao(): QuestionDao
}

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 question 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()

@Entity(tableName = "question", indices = [Index("question_id")])
data class Question(
  @PrimaryKey(autoGenerate = true)
  @ColumnInfo(name = "question_id")
  var questionId: Int,
  val text: String,
  val difficulty: Int = 0,
  val category: String = "android" //Only this line changes
)
class Migration2To3 : Migration(2, 3) {
  override fun migrate(database: SupportSQLiteDatabase) {
    database.execSQL("ALTER TABLE question ADD COLUMN category TEXT NOT NULL DEFAULT 'android'")
  }
}
@Database(entities = [(Question::class), (Answer::class)], version = 3) //change version to version 3 
abstract class QuizDatabase : RoomDatabase() {
  companion object{
    val MIGRATION_1_TO_2 = Migration1To2()
    val MIGRATION_2_TO_3 = Migration2To3() //adds migration
  }
  abstract fun questionsDao(): QuestionDao
}
database = Room.databaseBuilder(this, QuizDatabase::class.java, "question_database")
    .addMigrations(QuizDatabase.MIGRATION_1_TO_2, QuizDatabase.MIGRATION_2_TO_3)
    .build()

@Entity(tableName = "question", 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  
  val category: String = "android"
)
class Migration3To4 : Migration(3, 4) {
  override fun migrate(database: SupportSQLiteDatabase) {
    database.execSQL(
      "CREATE TABLE question_new (question_id INTEGER NOT NULL, " +
          "text TEXT NOT NULL, " +
          "difficulty TEXT NOT NULL, " +
          "category 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, category) " +
          "SELECT question_id, text, difficulty, category FROM question"
    )//3

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

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

  }
}
@Database(entities = [(Question::class), (Answer::class)], version = 4)//Changes the db version
abstract class QuizDatabase : RoomDatabase() {
  companion object{
    val MIGRATION_1_TO_2 = Migration1To2()
    val MIGRATION_2_TO_3 = Migration2To3()
    val MIGRATION_3_TO_4 = Migration3To4() //Adds a reference to your new migration
  }
  abstract fun questionsDao(): QuestionDao
}
database = Room.databaseBuilder(this, QuizDatabase::class.java, "question_database")
    .addMigrations(QuizDatabase.MIGRATION_1_TO_2, QuizDatabase.MIGRATION_2_TO_3, QuizDatabase.MIGRATION_3_TO_4)
    .build()

class Migration1To4 : Migration(1, 4) {
  override fun migrate(database: SupportSQLiteDatabase) {
    database.execSQL("ALTER TABLE question ADD COLUMN difficulty TEXT NOT NULL DEFAULT '0'")
    database.execSQL("ALTER TABLE question ADD COLUMN category TEXT NOT NULL DEFAULT 'android'")
  }
}
val MIGRATION_1_TO_4 = Migration1To4()
database = Room.databaseBuilder(this, QuizDatabase::class.java, "question_database")
    .addMigrations(
      QuizDatabase.MIGRATION_1_TO_2, 
      QuizDatabase.MIGRATION_2_TO_3,
      QuizDatabase.MIGRATION_3_TO_4, 
      QuizDatabase.MIGRATION_1_TO_4
    ).build()

Key points

  • Simply put, a database migration 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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.