Published on

How to make Android Room database manual migrations!

Have you wanted to use a SQLite database in your Android applications? If so, then using the popular persistence library: Room would be your best bet. But what about when you have made changes to your Room entity classes and want those changes to be reflected in the SQLite database?

Android + SQLite = πŸ’₯

Figure 1: Android + SQLite = πŸ’₯

Well, you'd have to do run some database migrations! In this post we'll go over how to make and run Room migrations in Android development. So without further delay let's jump right into it!

Setup

First we will need a sample Android application in order to make some Room migrations. To do this, just create a new Android application in Android Studio(click here to download it if you don't already have it installed). Select an empty activity as the starter template for this Android project, as shown below:

New Project screen

Figure 2: New Project screen

Once our Android application is setup, we can turn our attention to the project level build.gradle file and adding the following dependencies to it:

dependencies {

    ...

    def room_version = "2.3.0"

    implementation "androidx.room:room-runtime:$room_version"
    annotationProcessor "androidx.room:room-compiler:$room_version"

    // optional - RxJava2 support for Room
    implementation "androidx.room:room-rxjava2:$room_version"

    // optional - RxJava3 support for Room
    implementation "androidx.room:room-rxjava3:$room_version"

    // optional - Guava support for Room, including Optional and ListenableFuture
    implementation "androidx.room:room-guava:$room_version"

    // optional - Test helpers
    testImplementation "androidx.room:room-testing:$room_version"

    // optional - Paging 3 Integration
    implementation "androidx.room:room-paging:2.4.0-beta01"

    ...
}

Next, we have to sync the project to the updated build.gradle file by clicking the 'Sync Project with Gradle Files' button, as shown here:

Sync Project using the highlighted button

Figure 3: Sync Project using the highlighted button

With that out of the way, we can create a database class which will hold database information that will be used in connecting to the SQLite database using the Room persistence library.

First, we will need a model class that we can use to map to a table in the database. Let's create a models package and add a file named Vehicle.java. Inside it, we'll need to following code to define the Vehicle class:


import androidx.annotation.NonNull;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;

@Entity(tableName="vehicle")
public class Vehicle {

    @PrimaryKey(autoGenerate=true)
    @ColumnInfo(name="id")
    @NonNull
    private int id;


    @ColumnInfo(name="name")
    @NonNull
    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Vehicle{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

Let's go through what we just added:

  • @Entity(tableName=""): The Entity annotation indicates that this class will be mapped to a table in the database
  • @PrimaryKey(autoGenerated=True): This annotation indicates that the id column would be the primary key. Also, the ID would be auto generated as is evident by the autoGenerated=True option
  • @ColumnInfo(name=""): For specifying the column name in the database table that would map to the field in this POJO
  • @NonNull: For ensuring that the field cannot contain null values in the database

Next, let's add a DAO class for this Vehicle class. In order to do so, we must first add a package called dao to the source root package then create a file called VehicleDAO.java inside it. After that, we can add the following content to this newly created file:

import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;

import com.coding.informer.roommanualmigrationexample.models.Vehicle;

import java.util.List;

@Dao
public interface VehicleDAO {
    @Query("SELECT * FROM vehicle")
    List<Vehicle> getAll();

    @Query("SELECT * FROM vehicle WHERE id in (:findVehicleId)")
    Vehicle findById(int findVehicleId);

    @Insert
    void insertAll(Vehicle... assetEntries);

    @Insert
    void insertAsset(Vehicle asset);

    @Delete
    void delete(Vehicle asset);
}

This DAO class is like a gateway and between the Vehicle class on the application side and the vehicle table on the database side. It allows us to do CRUD operations on the vehicle table(to learn more about CRUD operations please see this link) while ensuring data consistency between the Java class objects and database table entries.

After building our DAO class we can turn our attention to our database class. First, let's go to the database package and create a new class called AppDatabase. In it, let's the following code:

import androidx.room.AutoMigration;
import androidx.room.Database;
import androidx.room.RenameTable;
import androidx.room.RoomDatabase;
import androidx.room.migration.AutoMigrationSpec;

import com.coding.informer.roommanualmigrationexample.dao.VehicleDAO;
import com.coding.informer.roommanualmigrationexample.models.Vehicle;

@Database(version=4, entities={Vehicle.class})
public abstract class AppDatabase extends RoomDatabase{
    public abstract VehicleDAO getVehicleDAO();

    public static final String NAME = "example_db";

}

Basically we are adding the @Database annotation to declare our database. It contains the database version inside the annotation itself and the database name, assigned to the NAME field.

Next up we have to define the migrations that we'll run on the database. First create a package called migrations inside the database package. Then, create a file called Migration1 inside that package. Finally, add the following code to that file:

import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;

public class Migration1 {

    public static final Migration MIGRATION_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            database.execSQL("CREATE TABLE `vehicle` (`id` INTEGER NOT NULL, "
                    + "`name` TEXT NOT NULL, PRIMARY KEY(`id`))");
        }
    };
}

In the MIGRATION_1_2 Migration instance, the table vehicle is created with the specific columns above. Note that for every time a change occurs in the Java entity classes, a new migration reflecting that change to the database is required to be run. Also, the version of the migration must change using the following format: new Migration({CurrentVersion}, {CurrentVersion + 1}).

Great! so now that we have our model class(Vehicle.java), DAO class(VehicleDAO.java) and the database class(AppDatabase.java), we can now establish the database connection in the our Main activity.

Let's add the following code to our MainActivity.java file:

import androidx.appcompat.app.AppCompatActivity;
import androidx.room.Room;

import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;

import com.coding.informer.roommanualmigrationexample.dao.VehicleDAO;
import com.coding.informer.roommanualmigrationexample.database.AppDatabase;
import com.coding.informer.roommanualmigrationexample.models.Vehicle;

import java.util.List;

import static com.coding.informer.roommanualmigrationexample.database.migrations.Migration1.MIGRATION_1_2;
import static com.coding.informer.roommanualmigrationexample.database.migrations.Migration1.MIGRATION_2_3;
import static com.coding.informer.roommanualmigrationexample.database.migrations.Migration1.MIGRATION_3_4;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        AsyncTask.execute(() -> {
            AppDatabase db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "example_db")
                    .addMigrations(MIGRATION_1_2).build();
        });
    }
}

Below the standard super.onCreate(savedInstanceState) line is the logic used to connect to the database using Room. Using databaseBuilder() results in fetching a new database instance. After that, the migrations specified in the Migration1 file is used to import the migrations to be run in updating the database to the current state of the application. Finally, the build() method is used to build the database instance.

Voila! The app running successfully means that the database setup and migrations completed successfully

Figure 4: Voila! The app running successfully means that the database setup and migrations completed successfully

Now we can add a second migration in order to add a new column to the vehicle database table. Add the following code to the Migration1 file to create the second migration:

   public static final Migration MIGRATION_2_3 = new Migration(2, 3) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            database.execSQL("ALTER TABLE `vehicle` ADD COLUMN `model` INTEGER NOT NULL DEFAULT 0");
        }
    };

What this migration is doing is adding a column called model to the vehicle table using the ALTER TABLE SQL statement. Note that in order to make the column NOT NULL we must specify a default value, which in this case is 0.

Before running this migration, we need to reflect this change in our Vehicle class because we need to keep our Java class objects reflecting the migration changes before running the migrations. So let's revisit the Vehicle class and add the model field to it, along with its corresponding getters and setters like so:

@ColumnInfo(name="model")
@NonNull
private String model;

@NonNull
public String getModel() {
    return model;
}

public void setModel(@NonNull String model) {
    this.model = model;
}

And as one final thing, we need to increment the database version to 3 in our AppDatabase class like so:

@Database(version=3, entities={Vehicle.class})
public abstract class AppDatabase extends RoomDatabase{
    ...
}

Now in order to run this migration we need to add that migration to the MainActivity class like so:

AsyncTask.execute(() -> {
    AppDatabase db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "example_db")
                    .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();
});

It is only by doing so that our migration will be picked up by Room and executed.

Great! As you can in the the Database Inspector view below we have successfully added the model field to the vehicle table now:

model field added to the vehicle table!πŸš€

Figure 5: model field added to the vehicle table!πŸš€

Now for our final migration in which we will drop the model column we just added. Head on over to the Migration1 class and add the following migration:

  public static final Migration MIGRATION_3_4 = new Migration(3, 4) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            database.execSQL("DROP TABLE `vehicle`");
            database.execSQL("CREATE TABLE `vehicle` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`id`))");
        }
    };

As you see above, there is no direct way to remove a column from a SQLite database using Room yet. So for now, we use the workaround solution of dropping the whole table then recreating it without the model field. It's not perfect but it gets the job doneπŸ˜…

Finally remember to add the MIGRATION_3_4 migration to the .addMigrations() list in the MainActivity class and to increment the database version to 4 in the AppDatabase class, just like we did for the second migration.

Finally, run the app and navigate to the Database Inspector view and find the model gone!

model field removed from vehicle table!βœ”

Figure 6: model field removed from vehicle table!βœ”

Well there you have it! If you make it this far pat yourselves on the back because you now know how to make manual migrations using Room for Android applicationsπŸš€πŸŽ‰

Conclusion

Congratulations! You have successfully learned how to make manual migrations using Room for Android applications. If you need access to the source code for this application you can access it by visiting it's GitHub link.

Well that's it for this post! Thanks for following along in this article and if you have any questions or concerns please feel free to post a comment in this post and I will get back to you when I find the time.

If you found this article helpful please share it and make sure to follow me on Twitter and GitHub, connect with me on LinkedIn and subscribe to my YouTube channel.