android-roomandroid-room-migration

How to merge multiple Room databases during migration


Problem I've two databases, mainDb and secondDb, which I need to merge into a single database during a database upgrade (migration).

Attempts I've looked into createFromAsset() which allows to import data from a database, but this only seems to work with fallbackToDestructiveMigration() enabled which would clear my mainDb when loading createFromAsset(secondDb).


Solution

  • Perhaps consider this example, noting that the answer you have given has some issues:-

    The Database code (@Daos,@Databases,@Entities) for the 2 databases

    :-

    const val DATABASE1_NAME = "db1.db"
    const val DATABASE2_NAME = "db2.db" /* Left in to make migration simpler (no hard coding BUT!!!!) */
    const val DATABASE1_VERSION = 1 /*<<<<<<<<<< CHANGED fro migration(merge) >>>>>>>>>>*/
    const val DATABASE2_VERSION = 1 /* NOT NEEDED FOR Conversion */
    
    /* Unchanged for migration (merge into 1 DB) */
    @Entity
    data class Table1(
        @PrimaryKey
        val table1Id: Long?=null,
        val table1Name: String
        // etc
    )
    
    /* Unchanged for migration (merge into 1 DB) */
    @Dao
    interface DB1DAOs {
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        fun insert(table1: Table1): Long
        @Query("SELECT * FROM table1")
        fun getAllFromTable1(): List<Table1>
        @Query("SELECT count(*) FROM table1")
        fun getDB1Table1RecordCount(): Long
    }
    
    /* Unchanged for migration (merge into 1 DB) */
    @Entity
    data class Table2(
        @PrimaryKey
        val table2Id: Long?=null,
        val table2Name: String
        // etc
    )
    
    /* Unchanged for migration (merge into 1 DB) */
    @Dao
    interface DB2DAOs {
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        fun insert(table2: Table2): Long
        @Query("SELECT * FROM table2")
        fun getAllFromTable2(): List<Table2>
    }
    @Database(entities = [Table1::class/*>>>>>>>>>> CHANGED TO ADD >>>>>>>>>>*//*,Table2::class*/], exportSchema = false, version = DATABASE1_VERSION)
    abstract class Database1: RoomDatabase() {
        abstract fun getDB1DAOs(): DB1DAOs
        /*<<<<<<<<<< ADDED although code from DB2DAOs could be moved into DB2DAOs >>>>>>>>>>*/
        //abstract fun getDB2DAOs(): DB2DAOs
        companion object {
            private var instance: Database1?=null
            fun getInstance(context: Context): Database1 {
                if (instance==null) {
                    val migration_From_V1_TO_V2 = object : Migration(1,2) {
                        override fun migrate(database: SupportSQLiteDatabase) {
                            doMigration1To2(context,database)
                        }
                    }
                    instance=Room.databaseBuilder(context,Database1::class.java, DATABASE1_NAME)
                        .allowMainThreadQueries()
                        .addMigrations(migration_From_V1_TO_V2) /* SEE BELOW for function invoked */
                        .build()
                }
                return instance as Database1
            }
            /* The migration - Frees up having the Old database Room code and
                would be a little more efficient due to additional overheads of opening Database via Room
                (no schema checking etc etc that Room undertakes )
            */
            @SuppressLint("Range")
            fun doMigration1To2(context: Context, database: SupportSQLiteDatabase) {
                val otherDBFile = context.getDatabasePath(DATABASE2_NAME)
                var otherDB: SQLiteDatabase? = null
                if (!otherDBFile.exists()) throw RuntimeException("Other Database not found at ${otherDBFile.path}.")
                try {
                    otherDB = SQLiteDatabase.openDatabase(
                        otherDBFile.path,
                        null,
                        SQLiteDatabase.OPEN_READWRITE
                    )
                } catch (e: Exception) {
                    e.printStackTrace()
                    throw RuntimeException("Unable to Open the Other Database ${otherDBFile}. See stack trace immediately prior to this.")
                }
                /* Need to create the table to be added to DB1 from DB2 */
                /* SQL copied from java(generated) */
                database.execSQL("CREATE TABLE IF NOT EXISTS `Table2` (`table2Id` INTEGER, `table2Name` TEXT NOT NULL, PRIMARY KEY(`table2Id`))")
                val csr = otherDB.query("Table2",null,null,null,null,null,null)
                val cv = ContentValues()
                while (csr.moveToNext()) {
                    cv.clear()
                    for (c in csr.columnNames) {
                        when (csr.getType(csr.getColumnIndex(c))) {
                            Cursor.FIELD_TYPE_BLOB -> cv.put(c,csr.getBlob(csr.getColumnIndex(c)))
                            Cursor.FIELD_TYPE_FLOAT -> cv.put(c,csr.getDouble(csr.getColumnIndex(c)))
                            Cursor.FIELD_TYPE_INTEGER -> cv.put(c,csr.getLong(csr.getColumnIndex(c)))
                            Cursor.FIELD_TYPE_STRING -> cv.put(c,csr.getString(csr.getColumnIndex(c)))
                            Cursor.FIELD_TYPE_NULL -> {} /* no-op */
                        }
                    }
                    database.insert("Table2",OnConflictStrategy.IGNORE,cv)
                }
                csr.close()
                otherDB.close()
                val renameOldFile = context.getDatabasePath("renamed_${DATABASE2_NAME}")
                if (renameOldFile.exists()) renameOldFile.delete()
                otherDBFile.renameTo(renameOldFile)
                //otherDBFile.delete() when tested
            }
        }
    }
    /* COMMENTED OUT for Migration(merge) as no longer needed */
    @Database(entities = [Table2::class], exportSchema = false,version = DATABASE2_VERSION)
    abstract class Database2: RoomDatabase() {
        abstract fun getDB2DAOs(): DB2DAOs
        companion object {
            private var instance: Database2?=null
            fun getInstance(context: Context): Database2 {
                if (instance==null) {
                    instance = Room.databaseBuilder(context,Database2::class.java, DATABASE2_NAME)
                        .allowMainThreadQueries()
                        .build()
                }
                return instance as Database2
            }
        }
    }
     /**/
    

    Some Activity Code to load some data into both databases for retention :-

    class MainActivity : AppCompatActivity() {
    
        lateinit var db1: Database1
        lateinit var dao1: DB1DAOs
        lateinit var db2: Database2 /* COMMENTED OUT for Migration(merge) as no longer needed */
        lateinit var dao2: DB2DAOs
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            db1 = Database1.getInstance(this)
            dao1 = db1.getDB1DAOs()
            db2 = Database2.getInstance(this)
            /* COMMENTED OUT for Migration(merge) as no longer needed */
            dao2 = db2.getDB2DAOs()
            //dao2 = db1.getDB2DAOs() /*<<<<<<<<<< changed from above commented out line >>>>>>>>>>*/
    
            if (DATABASE1_VERSION == 1 && dao1.getDB1Table1RecordCount() < 1) {
                dao1.insert(Table1(table1Name = "DB1Name11"))
                dao1.insert(Table1(table1Name = "DB1Name12"))
                dao1.insert(Table1(table1Name = "DB1Name13"))
                dao2.insert(Table2(table2Name = "DB2Name21"))
                dao2.insert(Table2(table2Name = "DB2Name22"))
            }
            for(t1 in dao1.getAllFromTable1()) {
                Log.d("DBINFO_V${DATABASE1_VERSION}_T1","ID=${t1.table1Id} NAME=${t1.table1Name}")
            }
            for (t2 in dao2.getAllFromTable2()) {
                Log.d("DBINFO_${DATABASE1_VERSION}_T2","ID=${t2.table2Id} NAME=${t2.table2Name}")
            }
        }
    }
    

    When run (1 or more times due the the version/row count test):-

    The Log shows:-

    2023-02-24 00:23:17.916 D/DBINFO_V2_T1: ID=1 NAME=DB1Name11
    2023-02-24 00:23:17.916 D/DBINFO_V2_T1: ID=2 NAME=DB1Name12
    2023-02-24 00:23:17.916 D/DBINFO_V2_T1: ID=3 NAME=DB1Name13
    2023-02-24 00:23:17.918 D/DBINFO_2_T2: ID=1 NAME=DB2Name21
    2023-02-24 00:23:17.918 D/DBINFO_2_T2: ID=2 NAME=DB2Name22
    

    App Inspection Shows:-

    enter image description here

    and also :-

    enter image description here

    The Merge

    The Database Code becomes :-

    const val DATABASE1_NAME = "db1.db"
    const val DATABASE2_NAME = "db2.db" /* Left in to make migration simpler (no hard coding BUT!!!!) */
    const val DATABASE1_VERSION = 2 /*<<<<<<<<<< CHANGED fro migration(merge) >>>>>>>>>>*/
    //const val DATABASE2_VERSION = 1 /* NOT NEEDED FOR Conversion */
    
    /* Unchanged for migration (merge into 1 DB) */
    @Entity
    data class Table1(
        @PrimaryKey
        val table1Id: Long?=null,
        val table1Name: String
        // etc
    )
    
    /* Unchanged for migration (merge into 1 DB) */
    @Dao
    interface DB1DAOs {
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        fun insert(table1: Table1): Long
        @Query("SELECT * FROM table1")
        fun getAllFromTable1(): List<Table1>
        @Query("SELECT count(*) FROM table1")
        fun getDB1Table1RecordCount(): Long
    }
    
    /* Unchanged for migration (merge into 1 DB) */
    @Entity
    data class Table2(
        @PrimaryKey
        val table2Id: Long?=null,
        val table2Name: String
        // etc
    )
    
    /* Unchanged for migration (merge into 1 DB) */
    @Dao
    interface DB2DAOs {
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        fun insert(table2: Table2): Long
        @Query("SELECT * FROM table2")
        fun getAllFromTable2(): List<Table2>
    }
    @Database(entities = [Table1::class/*>>>>>>>>>> CHANGED TO ADD >>>>>>>>>>*/,Table2::class], exportSchema = false, version = DATABASE1_VERSION)
    abstract class Database1: RoomDatabase() {
        abstract fun getDB1DAOs(): DB1DAOs
        /*<<<<<<<<<< ADDED although code from DB2DAOs could be moved into DB2DAOs >>>>>>>>>>*/
        abstract fun getDB2DAOs(): DB2DAOs
        companion object {
            private var instance: Database1?=null
            fun getInstance(context: Context): Database1 {
                if (instance==null) {
                    val migration_From_V1_TO_V2 = object : Migration(1,2) {
                        override fun migrate(database: SupportSQLiteDatabase) {
                            doMigration1To2(context,database)
                        }
                    }
                    instance=Room.databaseBuilder(context,Database1::class.java, DATABASE1_NAME)
                        .allowMainThreadQueries()
                        .addMigrations(migration_From_V1_TO_V2) /* SEE BELOW for function invoked */
                        .build()
                }
                return instance as Database1
            }
            /* The migration - Frees up having the Old database Room code and
                would be a little more efficient due to additional overheads of opening Database via Room
                (no schema checking etc etc that Room undertakes )
            */
            @SuppressLint("Range")
            fun doMigration1To2(context: Context, database: SupportSQLiteDatabase) {
                val otherDBFile = context.getDatabasePath(DATABASE2_NAME)
                var otherDB: SQLiteDatabase? = null
                if (!otherDBFile.exists()) throw RuntimeException("Other Database not found at ${otherDBFile.path}.")
                try {
                    otherDB = SQLiteDatabase.openDatabase(
                        otherDBFile.path,
                        null,
                        SQLiteDatabase.OPEN_READWRITE
                    )
                } catch (e: Exception) {
                    e.printStackTrace()
                    throw RuntimeException("Unable to Open the Other Database ${otherDBFile}. See stack trace immediately prior to this.")
                }
                /* Need to create the table to be added to DB1 from DB2 */
                /* SQL copied from java(generated) */
                database.execSQL("CREATE TABLE IF NOT EXISTS `Table2` (`table2Id` INTEGER, `table2Name` TEXT NOT NULL, PRIMARY KEY(`table2Id`))")
                val csr = otherDB.query("Table2",null,null,null,null,null,null)
                val cv = ContentValues()
                while (csr.moveToNext()) {
                    cv.clear()
                    for (c in csr.columnNames) {
                        when (csr.getType(csr.getColumnIndex(c))) {
                            Cursor.FIELD_TYPE_BLOB -> cv.put(c,csr.getBlob(csr.getColumnIndex(c)))
                            Cursor.FIELD_TYPE_FLOAT -> cv.put(c,csr.getDouble(csr.getColumnIndex(c)))
                            Cursor.FIELD_TYPE_INTEGER -> cv.put(c,csr.getLong(csr.getColumnIndex(c)))
                            Cursor.FIELD_TYPE_STRING -> cv.put(c,csr.getString(csr.getColumnIndex(c)))
                            Cursor.FIELD_TYPE_NULL -> {} /* no-op */
                        }
                    }
                    database.insert("Table2",OnConflictStrategy.IGNORE,cv)
                }
                csr.close()
                otherDB.close()
                val renameOldFile = context.getDatabasePath("renamed_${DATABASE2_NAME}")
                if (renameOldFile.exists()) renameOldFile.delete()
                otherDBFile.renameTo(renameOldFile)
                //otherDBFile.delete() when tested
            }
        }
    }
    /* COMMENTED OUT for Migration(merge) as no longer needed
    @Database(entities = [Table2::class], exportSchema = false,version = DATABASE2_VERSION)
    abstract class Database2: RoomDatabase() {
        abstract fun getDB2DAOs(): DB2DAOs
        companion object {
            private var instance: Database2?=null
            fun getInstance(context: Context): Database2 {
                if (instance==null) {
                    instance = Room.databaseBuilder(context,Database2::class.java, DATABASE2_NAME)
                        .allowMainThreadQueries()
                        .build()
                }
                return instance as Database2
            }
        }
    }
     */
    

    The Activity Code becomes:-

    class MainActivity : AppCompatActivity() {
    
        lateinit var db1: Database1
        lateinit var dao1: DB1DAOs
        //lateinit var db2: Database2 /* COMMENTED OUT for Migration(merge) as no longer needed */
        lateinit var dao2: DB2DAOs
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            db1 = Database1.getInstance(this)
            dao1 = db1.getDB1DAOs()
            //db2 = Database2.getInstance(this)
            /* COMMENTED OUT for Migration(merge) as no longer needed */
            //dao2 = db2.getDB2DAOs()
            dao2 = db1.getDB2DAOs() /*<<<<<<<<<< changed from above commented out line >>>>>>>>>>*/
    
            if (DATABASE1_VERSION == 1 && dao1.getDB1Table1RecordCount() < 1) {
                dao1.insert(Table1(table1Name = "DB1Name11"))
                dao1.insert(Table1(table1Name = "DB1Name12"))
                dao1.insert(Table1(table1Name = "DB1Name13"))
                dao2.insert(Table2(table2Name = "DB2Name21"))
                dao2.insert(Table2(table2Name = "DB2Name22"))
            }
            for(t1 in dao1.getAllFromTable1()) {
                Log.d("DBINFO_V${DATABASE1_VERSION}_T1","ID=${t1.table1Id} NAME=${t1.table1Name}")
            }
            for (t2 in dao2.getAllFromTable2()) {
                Log.d("DBINFO_${DATABASE1_VERSION}_T2","ID=${t2.table2Id} NAME=${t2.table2Name}")
            }
        }
    }
    

    Merged Results*

    The Log :-

    2023-02-24 00:52:05.617 D/DBINFO_V2_T1: ID=1 NAME=DB1Name11
    2023-02-24 00:52:05.617 D/DBINFO_V2_T1: ID=2 NAME=DB1Name12
    2023-02-24 00:52:05.617 D/DBINFO_V2_T1: ID=3 NAME=DB1Name13
    2023-02-24 00:52:05.621 D/DBINFO_2_T2: ID=1 NAME=DB2Name21
    2023-02-24 00:52:05.621 D/DBINFO_2_T2: ID=2 NAME=DB2Name22
    

    i.e. ALL data retained

    And via App Inspection:-

    enter image description here

    and

    enter image description here

    enter image description here