Jetpack: Room database upgrade detailed actual combat!

Keywords: Android Database kotlin jetpack

Catalogue of series articles

Related articles:
Jetpack: Room super detailed use pit guide!
Jetpack:Room+kotlin collaboration? Transaction problem analysis, detailed explanation of withTransaction API
Jetpack:Room use error FAQ
Jetpack:Room is optimized with LiveData/Flow, and Room+Flow uses principle analysis.

Introduction to Room upgrade

As business changes, the database may also need to make some adjustments, such as adding or modifying a field. At this time, you need to upgrade the database. Android provides a Migration class to upgrade the Room database.

	public Migration(int startVersion, int endVersion) {
        this.startVersion = startVersion;
        this.endVersion = endVersion;
    }

Migration has two parameters, startVersion and endVersion. startVersion indicates the current database version (the version installed on the device), and endVersion indicates the version to be upgraded to. If the application database version in the device is 1, the following migration will upgrade your database version from 1 to 2.

private val MIGRATION_1_2 = object :Migration(1,2){
    override fun migrate(database: SupportSQLiteDatabase) {
        //Perform operations related to database upgrade
    }
}

By analogy, if you need to upgrade the database from 2 to 3, make the following declaration.

private val MIGRATION_2_3 = object :Migration(2,3){
	override fun migrate(database: SupportSQLiteDatabase) {
		//Perform operations related to database upgrade
	}
}

If the application upgraded to version 3 is directly installed in the database of the current user, then Room will execute Migration (1,2) and Migration (2,3) in order to complete the upgrade.

Finally, add the upgrade scheme to room through addMigration.

Room.databaseBuilder(
    AppUtil.application,
    StudentDataBase::class.java,
    STUDENT_DB_NAME
)
.addMigrations(MIGRATION_1_2,MIGRATION_2_3)
.build()

exception handling

If we upgrade the database to 3 but do not write the corresponding migration, room will directly throw an IllagelStateException when using it. Because the room did not match the corresponding migration during the upgrade process. In order to prevent the application from crashing due to upgrade failure, you can add the fallbackToDestructiveMigration() method when creating the database. This method can recreate the data table in case of upgrade exception** It should be noted that although the application will not crash, all data will be lost because the data table is recreated** As follows:

Room.databaseBuilder(
AppUtil.application,
StudentDataBase::class.java,
STUDENT_DB_NAME
)
.addMigrations(MIGRATION_1_2,MIGRATION_2_3)
.fallbackToDestructiveMigration()
.build()

actual combat

This example is based on the student database table created in advance. It is upgraded twice. The first time, the Fruit table is added, and the second time, the field is added in the Fruit table. Can refer to Jetpack: Room super detailed use pit guide!Jetpack:Room is optimized with LiveData/Flow, and Room+Flow uses principle analysis.

1. Create Fruit table entity class

@Entity(tableName = FRUIT_TABLE_NAME)
data class FruitEntity(
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = FRUIT_TABLE_ID)
    val id: Int = 0,
    @ColumnInfo(name = FRUIT_TABLE_TEXT)
    val text: String?
)

/**
 * Table names are related and defined uniformly
 */
const val FRUIT_TABLE_NAME = "fruit"
const val FRUIT_TABLE_ID = "fruit_id"
const val FRUIT_TABLE_TEXT = "fruit_name"

2. In the Database annotation of the data upgrade declaration, add a new Fruit entity class. The upgraded version is 2.

//before 
@Database(entities = arrayOf(StudentEntity::class), version = 1)
abstract class StudentDataBase : RoomDatabase() 
//Added the upgraded version number of FruitEntity class
@Database(entities = arrayOf(StudentEntity::class,FruitEntity::class), version = 2)
abstract class StudentDataBase : RoomDatabase() 

3. Add a migration and set the colleague into the addMigration method.

/**
* Database upgrade 1 to 2
*/
private val MIGRATION_1_2 = object :Migration(1,2){
	override fun migrate(database: SupportSQLiteDatabase) {
		//New friit table
		database.execSQL("CREATE TABLE IF NOT EXISTS `$FRUIT_TABLE_NAME` 		(`$FRUIT_TABLE_ID` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `$FRUIT_TABLE_TEXT` TEXT)")
	}
}

//Modify the database declaration and change the MIGRATION_1_2. Set addMigrations
fun getDataBase(): StudentDataBase {
    return INSTANT ?: synchronized(this) {
        INSTANT ?: Room.databaseBuilder(
            AppUtil.application,
            StudentDataBase::class.java,
            STUDENT_DB_NAME
        ).addMigrations(MIGRATION_1_2)
        .fallbackToDestructiveMigration()
        .build()
        .also {
            INSTANT = it
        }
    }
}

Write Dao interface, test:

@Dao
interface ConflateDao {
    @Query("select * from $FRUIT_TABLE_NAME")
    suspend fun obtainFruit() : List<FruitEntity>

    @Insert
    suspend fun insertFruit(fruitEntity: FruitEntity)
}
//Get Dao from StudentDataBase
abstract fun getConflateEntityDao():ConflateDao

//activity test code
btnTransactionInsertGet.text = "MIGRATION TEST"
btnTransactionInsertGet.setOnClickListener {
	val conflateEntityDao = StudentDataBase.getDataBase().getConflateEntityDao()
	lifecycleScope.launch {
		StudentDataBase.getDataBase().withTransaction {
            conflateEntityDao.insertFruit(FruitEntity(text = "apple")
            val obtainFruit = conflateEntityDao.obtainFruit()
            withContext(Dispatchers.Main.immediate){
                binding.text.text = obtainFruit.toString()
            }
		}
	}
}

In this way, you can upgrade the new table for the first time.

Next, upgrade the database from 2 to 3 and add a new field in FruitEntity.

1. Add text2 field in FruitEntity

@ColumnInfo(name = FRUIT_TABLE_OTHER_NAME)
    val text2: String?

const val FRUIT_TABLE_OTHER_NAME = "fruit_other_name"

2. The studentdatabase is upgraded to 3 and a new migration is added_ 2_ 3, and add it to addMigrations

//Upgrade version to 3
@Database(entities = arrayOf(StudentEntity::class,FruitEntity::class), version = 3)
abstract class StudentDataBase : RoomDatabase() 
//New assignment_ 2_ three
private val MIGRATION_2_3 = object :Migration(2,3){
	override fun migrate(database: SupportSQLiteDatabase) {
	//A new column is added to the friit table
	database.execSQL("ALTER TABLE `$FRUIT_TABLE_NAME` ADD COLUMN `$FRUIT_TABLE_OTHER_NAME` TEXT ")
    }
}
//Add to addMigrations
Room.databaseBuilder(
	AppUtil.application,
	StudentDataBase::class.java,
	STUDENT_DB_NAME
)
.addMigrations(MIGRATION_1_2,MIGRATION_2_3)
.fallbackToDestructiveMigration()
.build()

3. Modify the test code and pass in multiple values of text2 field.

 conflateEntityDao.insertFruit(FruitEntity(text = "apple",text2 = "other apple2"))
 val obtainFruit = conflateEntityDao.obtainFruit()
 withContext(Dispatchers.Main.immediate){
 	binding.text.text = obtainFruit.toString()
 }

The database has also been upgraded from 2 to 3. Note that the field is declared as an nullable type. Such data has no text2 field, and the corresponding return result is null.

Expand knowledge

It is troublesome to modify the table structure in Sqlite. For example, we want to change the age field type in the Student table from INTEGER to TEXT.

The best way is to adopt the destruction and reconstruction strategy, which is roughly divided into the following steps.

  1. Create a temporary table that meets the requirements, such as temp_student.
  2. Transfer the data from the old data table student value to the new temporary table temp_student.
  3. Delete old table student
  4. Set temporary table temp_ Rename student to student

As follows:

private val MIGRATION_3_4 = object :Migration(3,4){
	override fun migrate(database: SupportSQLiteDatabase) {
		database.execSQL("CREATE TABLE IF NOT EXISTS `temp_student` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT) ")
		database.execSQL("INSERT INTO temp_student (id,name) SELECT id , name FROM student")
		database.execSQL("DROP TABLE student")
		database.execSQL("ALTER TABLE temp_student RENAME TO student")
	}
}

Posted by falcon8253 on Sun, 19 Sep 2021 23:47:53 -0700