Search code examples
androidandroid-room

Remove room entity from database


After 14 versions of this entity, it was decided to remove it as its no longer needed. what is the safest way to remove this entity without affections previous versions that use it?

We have manual migrations with this entity, I removed them as they are no longer needed, but having an older version installed, and updating to this one is breaking the application. is there more stuff needed to be done to have it working?


Solution

  • You cannot simply remove a Migration. If the schema has changed then this is detected by Room and a Migration is required, an exception being if using fallbackToDestructiveMigration when all tables will be dropped, thus removing all data, the tables are then created according to the expected schema.

    Room detects a change to the schema by comparing a hash of the schema built at compile time against a hash that is stored in the room_master_table. If the schema has changed the hash will have changed.

    If the hash has changed then Room will then check to see if the version number has changed, if not then there will be a failure as a Migration is required but the version number has not increased.

    If the version number has been increased then Room will check to see if there is a Migration that covers the change from the old version number to the new version number, unless the call to fallbackToDestructiveMigration() has been specified, in which case Room invokes the dropAllTables method generated at compile time.

    If you are removing an Entity, and you wish to keep the data, then you must have a Migration (if you want to retain existing data). The Migration should DROP the table (however, as Room only cares that what it expects exists, it could be workable to have a Migration that does nothing and the table would be ignored, if this is the only change to the schema).

    Demo

    The following is a simple demonstration of some of the core aspects described above.

    First the @Entity annotated classes (2)

    MainTable the table that represents tables that are to be kept along with the data within:-

    @Entity
    class MainTable {
        @PrimaryKey
        Long id=null;
        String name;
    
        public MainTable(){}
        public MainTable(Long id, String name) {
            this.id=id;
            this.name=name;
        }
        public MainTable(String name) {
            this.name=name;
        }
    }
    

    OtherTable the table that will be removed

    @Entity
    class OtherTable {
        @PrimaryKey
        Long otherId=null;
        String otherName;
    }
    

    AllDAOs the interface for the DAO's:-

    @Dao
    interface AllDAOs {
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        long insert(MainTable mainTable);
        /* Not necessary as not used in demo but would likely exist before removal of OtherTable
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        long insert(OtherTable otherTable);
    
         */
    
        @Query("SELECT * FROM maintable")
        List<MainTable> getAllFromMainTable();
        /* Not necessary as not used in demo but would likely exist before removal of OtherTable
        @Query("SELECT * FROM othertable")
        List<OtherTable> getAllFromOtherTable();
    
         */
    }
    
    • the OtherTable DAO's commented out as they will not be used in the demo (included just to be thorough with checking)

    TheDatabase the @Database annotated class:-

    @Database(
            entities = {
                    MainTable.class
                    , OtherTable.class /*<<<<<<<<<< WILL BE REMOVED for V15 */
            },
            exportSchema = false,
            version = TheDatabase.DATABASE_VERSION
    )
    abstract class TheDatabase extends RoomDatabase {
        public final static String DATABASE_NAME = "the_database.db";
        public final static int DATABASE_VERSION = 14;
    
        abstract AllDAOs getAllDAOs();
    
        private static TheDatabase instance;
        public static TheDatabase getInstance(Context context) {
            if (instance==null) {
                instance = Room.databaseBuilder(context,TheDatabase.class,DATABASE_NAME)
                        .allowMainThreadQueries()
                        .addMigrations(mig14to15) /*<<<<<<<<<< The migration can be left in if not invoked */
                        .build();
            }
            return instance;
        }
    
        private static final Migration mig14to15 = new Migration(14,15) {
            @Override
            public void migrate(@NonNull SupportSQLiteDatabase supportSQLiteDatabase) {
    
            }
        };
    }
    

    MainActivity the activity code that uses the database

    public class MainActivity extends AppCompatActivity {
    
        TheDatabase db;
        AllDAOs dao;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            db = TheDatabase.getInstance(this);
            dao = db.getAllDAOs();
            if (TheDatabase.DATABASE_VERSION <= 14) populateMainTable();
            for(MainTable mt: dao.getAllFromMainTable()) {
                Log.d("DBINFO_V" + TheDatabase.DATABASE_VERSION,"MainTable row is " + mt.name);
            }
            logSchema(db);
        }
    
        private void populateMainTable() {
            dao.insert(new MainTable(1000L,"Name1"));
            dao.insert(new MainTable("Name2"));
            MainTable name3 = new MainTable();
            name3.name = "Name3";
            dao.insert(name3);
        }
    
        @SuppressLint("Range")
        private void logSchema(TheDatabase db) {
            SupportSQLiteDatabase sdb = db.getOpenHelper().getWritableDatabase();
            Cursor csr = db.query(new SimpleSQLiteQuery("SELECT * FROM sqlite_master"));
            while (csr.moveToNext()) {
                Log.d(
                        "LOGSCHEMA_V" + TheDatabase.DATABASE_VERSION,
                        "Component is " + csr.getString(csr.getColumnIndex("name"))
                        + " Type is " + csr.getString(csr.getColumnIndex("type"))
                );
            }
        }
    }
    
    • This will:-

      • instantiate the database and Daos
      • add some data if at or below version 14
      • extract the data from the MainTable and write it to the log.
      • extract the schema from sqlite_master writing the defined components (tables indexes view etc) to the log to show exactly what is in the database (as opposed to Room's idea of what exists in the database)

    Test 1

    When the above is compiled, this will be for the run before the removal of the table. As pert of the compile the hash, as mentioned above, is generated. This hash can be seen in the generated java e.g.

    db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a9dc621848cb6cf03f2ce9a3e93ee987')");
    

    When the App is run then the log includes:-

    2023-03-31 11:46:28.521 D/DBINFO_V14: MainTable row is Name1
    2023-03-31 11:46:28.521 D/DBINFO_V14: MainTable row is Name2
    2023-03-31 11:46:28.521 D/DBINFO_V14: MainTable row is Name3
    
    
    2023-03-31 11:46:28.522 D/LOGSCHEMA_V14: Component is android_metadata Type is table
    2023-03-31 11:46:28.522 D/LOGSCHEMA_V14: Component is MainTable Type is table
    2023-03-31 11:46:28.522 D/LOGSCHEMA_V14: Component is OtherTable Type is table
    2023-03-31 11:46:28.522 D/LOGSCHEMA_V14: Component is room_master_table Type is table
    
    • i.e. The 3 rows exist in the database as expected and 4 tables exist:-
      • android_metadata (and android API specific table that stores the locale)
      • MainTable and OtherTable as per Room
      • room_master_table which room uses to store the hash.

    If App Inspection is used then:-

    enter image description here

    • Notice that the hash has been stored.

    Stage 2 removal of OtherTable

    Rather than delete the class plus other parts, the list of entities is amneded to exclude the OtherTable.class. Additionally, to mimic the no Migration situation the .AddMigrations method invocation is commented out. So:-

    @Database(
            entities = {
                    MainTable.class
                    /*, OtherTable.class */ /*<<<<<<<<<< WILL BE REMOVED for V15 */
            },
            exportSchema = false,
            version = TheDatabase.DATABASE_VERSION
    )
    abstract class TheDatabase extends RoomDatabase {
        public final static String DATABASE_NAME = "the_database.db";
        public final static int DATABASE_VERSION = 14;
    
        abstract AllDAOs getAllDAOs();
    
        private static TheDatabase instance;
        public static TheDatabase getInstance(Context context) {
            if (instance==null) {
                instance = Room.databaseBuilder(context,TheDatabase.class,DATABASE_NAME)
                        .allowMainThreadQueries()
                        //.addMigrations(mig14to15) /*<<<<<<<<<< The migration can be left in if not invoked */
                        .build();
            }
            return instance;
        }
    
        private static final Migration mig14to15 = new Migration(14,15) {
            @Override
            public void migrate(@NonNull SupportSQLiteDatabase supportSQLiteDatabase) {
    
            }
        };
    }
    

    The project is compiled, the hash as per the generated java is '73c7fb7c7251909ab317f9dbc9309a80' obviously different from the previous hash.

    The App is run and fails with:-

    java.lang.IllegalStateException: Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. ....
    

    So obviously Room, at run time, has seen that the schema has changed (stored and compiled hash mismatch).

    Stage 3 insrease the version number to 15

    So the code is changed to use public final static int DATABASE_VERSION = 15; and recompiled.

    The hash is now '73c7fb7c7251909ab317f9dbc9309a80' i.e. the version number change does not change the hash.

    So when the App is rerun yet again then another failure this time:-

    java.lang.IllegalStateException: A migration from 14 to 15 was required but not found. 
    

    So the Migration is required (note purposefully not trying .fallbackToDestructiveMigration() as it will not preserve the data).

    Stage 4 introduce the dummy.noop Migration by uncommenting the .addMigrations invocation i.e.:-

    instance = Room.databaseBuilder(context,TheDatabase.class,DATABASE_NAME)
                    .allowMainThreadQueries()
                    .addMigrations(mig14to15) /*<<<<<<<<<< The migration can be left in if not invoked */
                    .build();
    

    And now run:-

    The log includes:-

    2023-03-31 12:05:37.430 D/DBINFO_V15: MainTable row is Name1
    2023-03-31 12:05:37.430 D/DBINFO_V15: MainTable row is Name2
    2023-03-31 12:05:37.430 D/DBINFO_V15: MainTable row is Name3
    
    
    2023-03-31 12:05:37.431 D/LOGSCHEMA_V15: Component is android_metadata Type is table
    2023-03-31 12:05:37.431 D/LOGSCHEMA_V15: Component is MainTable Type is table
    2023-03-31 12:05:37.431 D/LOGSCHEMA_V15: Component is OtherTable Type is table
    2023-03-31 12:05:37.431 D/LOGSCHEMA_V15: Component is room_master_table Type is table
    
    • So version 15 and all the tables remain so Room doesn't care that the rouge OtherTable exists even though it doesn't use or require it.

    If the App is uninstalled and then the steps above are repeated BUT the Migration is changed to:-

    private static final Migration mig14to15 = new Migration(14,15) {
        @Override
        public void migrate(@NonNull SupportSQLiteDatabase supportSQLiteDatabase) {
            supportSQLiteDatabase.execSQL("DROP TABLE IF EXISTS othertable");
        }
    };
    

    Then the log of the last step includes:-

    2023-03-31 12:12:10.955 D/DBINFO_V15: MainTable row is Name1
    2023-03-31 12:12:10.955 D/DBINFO_V15: MainTable row is Name2
    2023-03-31 12:12:10.955 D/DBINFO_V15: MainTable row is Name3
    
    
    2023-03-31 12:12:10.956 D/LOGSCHEMA_V15: Component is android_metadata Type is table
    2023-03-31 12:12:10.956 D/LOGSCHEMA_V15: Component is MainTable Type is table
    2023-03-31 12:12:10.956 D/LOGSCHEMA_V15: Component is room_master_table Type is table
    

    i.e. OtherTable has been dropped and Room doesn't care as it is not required.