146

Let't assume, I have a simple Room database:

@Database(entities = {User.class}, version = 1)
abstract class AppDatabase extends RoomDatabase {
    public abstract Dao getDao();
}

Now, I'm adding a new entity: Pet and bumping version to 2:

@Database(entities = {User.class, Pet.class}, version = 2)
abstract class AppDatabase extends RoomDatabase {
    public abstract Dao getDao();
}

Of course, Room throws an exception: java.lang.IllegalStateException: A migration from 1 to 2 is necessary.

Assuming, I haven't changed User class (so all data is safe), I have to provide migration which just creates a new table. So, I'm looking into classes generated by Room, searching for generated query to create my new table, copying it and pasting into migration:

final Migration MIGRATION_1_2 =
        new Migration(1, 2) {
            @Override
            public void migrate(@NonNull final SupportSQLiteDatabase database) {
                database.execSQL("CREATE TABLE IF NOT EXISTS `Pet` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))");
            }
        };

However I find it inconvenient to do it manually. Is there a way to tell Room: I'm not touching any of the existing table, so data is safe. Please create migration for me?

  • Did you find a solution to this? – Mikkel Larsen Feb 07 '18 at 13:13
  • 4
    I had the same issue and fixed it the same way you did, and haven't found a solution neither. Glad I'm not alone then. :) – Mikkel Larsen Feb 08 '18 at 12:55
  • 3
    Same here. I find it very inconvenient that room is able to generate the create query inside of the database_impl, but can't just create the table, once the migration starts.... – JacksOnF1re Feb 20 '18 at 10:33
  • 1
    I would give so much for such a feature... It would also be nice to mix migrations and the fallback mechanism... – Appyx Feb 26 '18 at 19:49
  • 3
    I'm not sure if this would be helpful but Room does have the option to export the database schema into a JSON file. https://developer.android.com/training/data-storage/room/migrating-db-versions.html#export-schema Obviously this would still mean manually adding the migration script but you wouldn't need to route through the auto-generated classes to get your SQL statement. – James Lendrem Apr 03 '18 at 10:50
  • I came here only to find out that it's a manual process :/ – sneharc Jun 05 '18 at 09:22
  • Apparently there isn't such feature. How come they don't include such basic feature?! It gets really inconvenient if you have table with lots of fields. There is `createTable` method in `OrmLite` for example, but nothing in `Room`. What I do is just export schema and create corresponding `SQLite` query from generated schema. – musooff Dec 12 '18 at 02:11

7 Answers7

126

Room does NOT have a good Migration System, at least not until 2.1.0-alpha03.

So, until we have better Migration System, there are some workarounds to have easy Migrations in the Room.

As there is no such method as @Database(createNewTables = true) or MigrationSystem.createTable(User::class), which there should be one or other, the only possible way is running

CREATE TABLE IF NOT EXISTS `User` (`id` INTEGER, PRIMARY KEY(`id`))

inside your migrate method.

val MIGRATION_1_2 = object : Migration(1, 2){
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("CREATE TABLE IF NOT EXISTS `User` (`id` INTEGER, PRIMARY KEY(`id`))")
    }
}

In order to get above SQL script, you have 4 ways

1. Write by yourself

Basically, you have to write the above script that will match the script that Room generates. This way is possible, not feasible. (Consider you have 50 fields)

2. Export Schema

If you include exportSchema = true inside your @Database annotation, Room will generate database schema within /schemas of your project folder. The usage is

@Database(entities = [User::class], version = 2, exportSchema = true)
abstract class AppDatabase : RoomDatabase {
   //...
}

Make sure that you have included below lines in build.grade of your app module

kapt {
    arguments {
        arg("room.schemaLocation", "$projectDir/schemas".toString())
    }
} 

When you run or build the project you will get a JSON file 2.json, which has all the queries within your Room database.

  "formatVersion": 1,
  "database": {
    "version": 2,
    "identityHash": "325bd539353db508c5248423a1c88c03",
    "entities": [
      {
        "tableName": "User",
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, PRIMARY KEY(`id`))",
        "fields": [
          {
            "fieldPath": "id",
            "columnName": "id",
            "affinity": "INTEGER",
            "notNull": true
          },

So, you can include the above createSql within you migrate method.

3. Get query from AppDatabase_Impl

If you don't want to export schema you can still get the query by running or building the project which will generate AppDatabase_Impl.java file. and within the specified file you can have.

@Override
public void createAllTables(SupportSQLiteDatabase _db) {
  _db.execSQL("CREATE TABLE IF NOT EXISTS `User` (`id` INTEGER, PRIMARY KEY(`id`))");

Within createAllTables method, there will be the create scripts of all the entities. You can get it and include in within you migrate method.

4. Annotation Processing.

As you might guess, Room generates all of the above mentioned schema, and AppDatabase_Impl files within compilation time and with Annotation Processing which you add with

kapt "androidx.room:room-compiler:$room_version"

That means you can also do the same and make your own annotation processing library that generates all the necessary create queries for you.

The idea is to make an annotation processing library for Room annotations of @Entity and @Database. Take a class that is annotated with @Entity for example. These are the steps you will have to follow

  1. Make a new StringBuilder and append "CREATE TABLE IF NOT EXISTS "
  2. Get the table name either from class.simplename or by tableName field of @Entity. Add it to your StringBuilder
  3. Then for each field of your class create columns of SQL. Take the name, type, nullability of the field either by the field itself or by @ColumnInfo annotation. For every field, you have to add id INTEGER NOT NULL style of a column to your StringBuilder.
  4. Add primary keys by @PrimaryKey
  5. Add ForeignKey and Indices if exists.
  6. After finishing convert it to string and save it in some new class that you want to use. For example, save it like below
public final class UserSqlUtils {
  public String createTable = "CREATE TABLE IF NOT EXISTS User (id INTEGER, PRIMARY KEY(id))";
}

Then, you can use it as

val MIGRATION_1_2 = object : Migration(1, 2){
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL(UserSqlUtils().createTable)
    }
}

I made such a library for myself which you can check out, and even use it in your project. Note that the library that I made is not full and it just fulfills my requirements for table creation.

RoomExtension for better Migration

Application that uses RoomExtension

Hope it was useful.

UPDATE

By the time of writing this answer, room version was 2.1.0-alpha03 and when I emailed developers I got a response of

It is expected to have better Migration System in 2.2.0

Unfortunately, we still lack better Migration System.

musooff
  • 6,412
  • 3
  • 36
  • 65
  • 3
    Can you point to where you read that Room 2.2.x will have better migration? I cannot find anything that makes that claim, and since we're currently working on 2.1.0 beta, what is in 2.2.0 seems to be unknown at the moment. – jkane001 May 13 '19 at 20:50
  • 1
    @jkane001 I emailed one of the room developers and got a response so. There is no such public notice regarding 2.2.x (yet?) – musooff May 14 '19 at 00:07
  • 7
    Currently on version 2.2.2 and still no better migration :( However, this is an excellent answer and saved me a ton of work so +1 for that. – jwitt98 Dec 18 '19 at 04:52
  • @androiddeveloper All except #4, which is Annotation Processing – musooff Feb 03 '20 at 07:48
  • 2
    @musooff I actually think it's ok to add the table creation. It's the safest way to copy the code from the "createAllTables" function. – android developer Feb 03 '20 at 11:00
  • 1
    It's finally coming on 2.4 - currently in alpha! – JaviCasa Oct 05 '21 at 07:37
6

Sorry, Room doesn't support auto-creation of tables without data loss.

It is mandatory to write the migration. Otherwise, it'll erase all the data and create the new table structure.

anothernode
  • 5,100
  • 13
  • 43
  • 62
Viswanath Kumar Sandu
  • 2,230
  • 2
  • 17
  • 34
4

For anybody out there still looking for solutions to this issue, I have some good news. Room is starting to support Auto-migrations with version 2.4.0

https://developer.android.com/jetpack/androidx/releases/room#2.4.0-alpha01

Onur D.
  • 515
  • 3
  • 11
1

You can add the following gradle command to your defaultConfig in your app.gradle:

javaCompileOptions {
        annotationProcessorOptions {
            arguments = ["room.schemaLocation":
                                 "$projectDir/schemas".toString()]
        }
    }

When you run this it will compile a list of table names with their relevant CREATE TABLE statements from which you can just copy and paste into your migration objects. You might have to change the table names.

For example this is from my generated schema:

"tableName": "assets",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`asset_id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `base` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`asset_id`))"

And so I copy paste the createSql statement and change the '${TABLE_NAME}' to 'assets' the table name, and voila auto generated Room create statements.

Larry Stent
  • 99
  • 2
  • 9
-3

Maybe in this case(if you've only created new table without changing others) you can do this not creating any migrations at all?

user1730694
  • 442
  • 4
  • 14
  • 1
    No, in this case room throws in logs: java.lang.IllegalStateException: A migration from {old_version} to {new_version} is necessary – Max Makeichik Jun 19 '18 at 14:41
-3

You can do this way-

@Database(entities = {User.class, Pet.class}, version = 2)

abstract class AppDatabase extends RoomDatabase {
public abstract Dao getDao();
public abstract Dao getPetDao();
}

Remaining will be same as you have mentioned above-

 db = Room.databaseBuilder(this, AppDatabase::class.java, "your_db")
        .addMigrations(MIGRATION_1_2).build()

Reference - For more

Sujeet Kumar
  • 256
  • 1
  • 11
-3

In this case, you don't need to do a migration, you can call .fallbackToDestructiveMigration() when you are creating database instance.

Example:

    instance = Room.databaseBuilder(context, AppDatabase.class, "database name").fallbackToDestructiveMigration().build();

And don't forget to change database version.

rudicjovan
  • 303
  • 2
  • 7