Home > Mobile >  How to migrate Room db from 1 to 2?
How to migrate Room db from 1 to 2?

Time:03-22

I'm trying to migrate my Room db to the next version and I keep getting the same error:

java.lang.IllegalStateException: A migration from 1 to 2 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration ...) or allow for destructive migrations via one of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.

The only difference between my database versions is that I have added a new column. The migration is handled like this:

@Database(
version = 2,
entities = [Note::class],
exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDAO

companion object {
    fun build(context: Context) = Room.databaseBuilder(context, AppDatabase::class.java, "NotesDatabase")
        .addMigrations(MIGRATION_1_2).build()
    }
}

val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
    database.execSQL("ALTER TABLE Notes ADD COLUMN image STRING")
    }
}

I'm not sure if I have implemented that correctly. The error tells me that I have to somehow call .build(). I tried that in the activity using the database, but the error was the same so I removed that call.

How can I fix this?

CodePudding user response:

It appears that you are not invoking the build function and probably have another means of building the database (invoking the databaseBuilder).

e.g. In an activity/fragment you have something like :-

class MainActivity : AppCompatActivity() {
    lateinit var db: AppDatabase
    lateinit var dao: NoteDAO
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        db = Room.databaseBuilder(this,AppDatabase::class.java,"NotesDatabase")
            .allowMainThreadQueries()
            .build()
        dao = db.noteDao()
        dao.getAllNotes()
    }
}

This after changing the Note entity and increasing the version to 2 yields the results e.g. :-

java.lang.IllegalStateException: A migration from 1 to 2 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration ...) or allow for destructive migrations via one of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.

Instead you need to invoke the AppDatabase's build function

So the above would become :-

class MainActivity : AppCompatActivity() {
    lateinit var db: AppDatabase
    lateinit var dao: NoteDAO
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        db = AppDatabase.build(this) //<<<<<<<<<< CHANGED
        dao = db.noteDao()
        dao.getAllNotes() // Forces database access/open
    }
}

HOWEVER, you will then get the Expected/Found issue something like :-

2022-03-21 09:15:20.385 14533-14533/a.a.so71549033kotlinroommigration E/AndroidRuntime: FATAL EXCEPTION: main
    Process: a.a.so71549033kotlinroommigration, PID: 14533
    java.lang.RuntimeException: Unable to start activity ComponentInfo{a.a.so71549033kotlinroommigration/a.a.so71549033kotlinroommigration.MainActivity}: java.lang.IllegalStateException: Migration didn't properly handle: notes(a.a.so71549033kotlinroommigration.Note).
     Expected:
    TableInfo{name='notes', columns={noteText=Column{name='noteText', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, image=Column{name='image', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, noteId=Column{name='noteId', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=1, defaultValue='null'}}, foreignKeys=[], indices=[]}
     Found:
    TableInfo{name='notes', columns={noteText=Column{name='noteText', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, image=Column{name='image', type='STRING', affinity='1', notNull=false, primaryKeyPosition=0, defaultValue='null'}, noteId=Column{name='noteId', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=1, defaultValue='null'}}, foreignKeys=[], indices=[]}
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3449)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

This is because STRING, as far as room is concerned, is not a valid column type.

In the above you can see/extract that room expects :-

image=Column{name='image', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}

BUT Room found :-

image=Column{name='image', type='STRING', affinity='1', notNull=false, primaryKeyPosition=0, defaultValue='null'}

Room only accepts column types of INTEGER, REAL, TEXT and BLOB (even though SQLite is far more flexible with column types).

The type depends upon the type of the variable, furthermore Room is also very specific about other parts of the definition, such as whether or not NOT NULL should be part and whether or not DEFAULT should be part of the definition or not.

However, Room allows you to ascertain exactly what the column definition should be. If you make the changes to the entity(ies) and compile the project then java code is generated by Room, this includes the SQL used to create the table(s) and within this SQL are the expected column definitions.

From the Android Studio Project View you will see Java (generated), within the classes/files under this will be some classes one of which will be AppDatabase_Impl. Within this class there will be a method createAlltables. Within this method is the SQL for all the tables.

e.g. :-

enter image description here

  • Note the image column, in this case, was coded as var image: String
    • String equates to TEXT and also NOT NULL. String? would not have NOT NULL

However, if ALTERing a table and adding a column with NOT NULL then SQLite requires that a DEFAULT is provided that is not null, as per:- f a NOT NULL constraint is specified, then the column must have a default value other than NULL. https://www.sqlite.org/lang_altertable.html

So in the case above then for the found to be what room expects then the ALTER SQL should be :-

ALTER TABLE Notes ADD COLUMN image TEXT NOT NULL DEFAULT 'unknown'
  • 'unknown' could be whatever suits
  • Related