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?
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();
*/
}
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:-
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
If App Inspection is used then:-
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
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.