When you develop an application, it's perfectly normal for the project's needs to evolve. As you add features, you'll realize that Room entity classes and their corresponding tables must be changed to adapt to the new requirements. The big challenge here is ensuring that the user doesn't lose any data when the app is updated and the database schema has changed.
To address this, Room offers a range of options, from the simplest to more elaborate implementations. Depending on whether the change is a simple column addition or a complete restructuring, you can choose from automatic or manual migrationsalways ensuring that the transition is seamless and does not cause the application to close unexpectedly.
The fast track: Automatic Migrations
Starting with version 2.4.0-alpha01, Room simplifies our lives with automatic migrations. Essentially, the framework analyzes the differences between the old and new versions and generates the migration plan for us. To activate it, simply add the annotation. @AutoMigration within the autoMigrations property in the @Database decorator.
Note, there's a crucial detail: for this to work, exportSchema must be set to trueIf you haven't exported the schema or compiled the database with the new version number, Room won't know what has changed and the migration will fail spectacularly.
Complex cases in automatic mode
Sometimes, Room falls a little short and detects ambiguous changes, such as when you decide to delete a table or rename a column. In these cases, the compiler will throw an error and ask you to implement a AutoMigrationSpecThis static class is where you give Room the extra information it needs to avoid getting lost.
Within this Spec, you can use annotations such as @RenameTable to guide the system. Additionally, if you need to run extra code once the automatic migration is complete, you have the following method available. onPostMigrate()which is ideal for making final data adjustments.
Total Control: Manual Migrations
There are situations where automation falls short, such as when you need to split the data from a table into two separate entities. This is where manual migrations come in, through the creation of a class that inherits from Migration and where you override the migrate() method.
In this method, you have a SupportSQLiteDatabase object that allows you to execute SQL statements directly. For example, to add a column, you would use a ALTER TABLEIt is vital that when registering these routes in the database builder you use the method addMigrations().
A golden tip: avoid using Kotlin constants within your SQL migration queries; write complete SQL queriesThis prevents catastrophic errors if in the future you change the value of a constant but the old migration still needs the original value.
Advanced strategies and error management
If you find yourself in a situation where you can't define a migration path and you don't mind the data being deleted (because it might just be a cache), you can use fallbackToDestructiveMigration()This causes Room to delete everything and recreate the tables from scratch, preventing the app from crashing with an IllegalStateException.
For finer control, there are alternatives such as fallbackToDestructiveMigrationFrom()which only deletes data if the user is coming from very specific versions, or fallbackToDestructiveMigrationOnDowngrade()This is useful when someone installs an older version of the app over a new one.
Pre-packaged databases
Sometimes we want the app to come with pre-installed data. Room allows you to do this through createFromAsset() o createFromFile()What's interesting is how this interacts with destructive migrations: if you have a pre-packaged file that matches the target version, Room will use it to populate the database after performing a destructive cleanup.
The art of renaming and handling NOT NULL
Renaming columns in SQLite can be a headache, especially in versions prior to API 29. The master trick here is the pattern create-copy-deleteYou create a new table with the correct name, pass the data with an INSERT INTO … SELECT, and finally delete the old table with a DROP TABLE.
Another critical scenario is adding a column NOT NULL with no default value in a table that already has records. Since SQLite doesn't allow this directly, you must create a temporary table with a provisional default value, move the data, delete the original, and rename the temporary table to the final one.
Ensuring stability: Testing and Schematics
Don't launch a migration to production without testing it. Room provides the tool room-testing and the MigrationTestHelper class. With this you can create a database in the old version, insert real data, and then run runMigrationsAndValidate() to verify that the final scheme is as expected and that the data is still there.
For all of this to be viable, it is essential to manage the schema's JSON files. Configuring the room.schemaLocation In the Gradle file, Room will generate a version history. These files are the absolute truth: the field createSql The JSON is exactly what you should replicate in your manual migrations to avoid discrepancies.
Transition from pure SQLite to Room
If you're coming from using SQLite in the traditional way, switching to Room is very beneficial. The process involves converting your models into @EntityDefine DAOs to replace your manual queries and create a class that extends RoomDatabase. It's crucial to increment the database version and define a migration path, even if it's empty, so that Room doesn't delete existing data when it takes control.
Mastering the schema lifecycle, from exporting JSON and writing precise SQL to validation through instrumented tests, allows the application to evolve without fear of breaking the user experience or losing valuable information on the device. Share this information so that more people can learn about the topic.