Issue
My app is crashing because I am not handling migration properly. I'm looking for a solution to migrate the name of 1 column in my table.
In my project I a have a room table named 'content' with a Double attribute 'archivedCount'. In the latest version of the app the attribute archivedCount attribute is re-named to dismissCount, still as type Double.
Original Content model
#Entity(tableName = "content")
data class Content(#PrimaryKey var id: String, var archiveCount: Double) : Parcelable {...}
New Content model
#Entity(tableName = "content")
data class Content(#PrimaryKey var id: String, var dismissCount: Double) : Parcelable {...}
Attempted Solution
After reading a Google Developer Advocate's explanation Understanding migrations with Room, I attempted her solution outlined in the post's section Migrations with complex schema changes which entails making a copy of the original table, deleting the old table, then renaming the newly created table.
With the following approach below there is a runtime error on this line: database.execSQL("INSERT INTO content_new (id, dismissCount) SELECT id, archiveCount FROM users"); because I already cleared my app's cache so the old table no longer exists.
Can I update a single column without re-creating the entire table?
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
#Override
public void migrate(SupportSQLiteDatabase database) {
// Create the new table
database.execSQL(
"CREATE TABLE content_new (id TEXT, dismissCount REAL, PRIMARY KEY(id))");
// Copy the data
database.execSQL("INSERT INTO content_new (id, dismissCount) SELECT id, archiveCount FROM users");
// Remove the old table
database.execSQL("DROP TABLE content");
// Change the table name to the correct one
database.execSQL("ALTER TABLE content_new RENAME TO content");
}
};
Solution
Thanks to the guidance from #TimBiegeleisen we discovered that the Android implementation of SQLite 3.19 for API 27 and 28 has not yet upgraded to the version 3.25 SQLite which allows this feature outlined in this StackOverflow post.
Once Android upgrades a command such as this to alter a table column will be possible: database.execSQL("ALTER TABLE content RENAME COLUMN archiveCount TO dismissCount")
There is a solution without migration - use ColumnInfo:
data class Content(#PrimaryKey var id: String, #ColumnInfo(name = "archiveCount") var dismissCount: Double) : Parcelable{...}
Database column will be still archiveCount, but in Kotlin property will be renamed.
Related
I added a column to a table, then added the following migration (version 56 to 57):
private val MIGRATION_56_57 = object : Migration(56, 57) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `app_stage` ADD COLUMN hasSeenBusinessOwnerQuestion INTEGER DEFAULT 0 NOT NULL")
}
}
After building and releasing the app to our existing users, they get a migration error and the app crashes. To correct the error, I just need to change:hasSeenBusinessOwnerQuestion INTEGER DEFAULT 0 NOT NULL
to:hasSeenBusinessOwnerQuestion INTEGER DEFAULT 0 .
Should I just add another migration from version 57 to 58 as:
private val MIGRATION_57_58 = object : Migration(57, 58) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `app_stage` ADD COLUMN hasSeenBusinessOwnerQuestion INTEGER DEFAULT 0 ")
}
}
?
Will existing users upgrading from version 56 all the way to 58 get that same migration error? How do I add another migration to version 58 to avoid migration errors?
? Will existing users upgrading from version 56 all the way to 58 get that same migration error?
No Room will invoke all the migrations in sequence (56-57 and then 57-58) and then after all the migrations have been performed continue with the database build.
However you cannot use the ALTER command to ADD an already existing column, which would be the case going from 57-58. So the migration would fail.
Although later versions of SQLite (3.35.0) support ALTER TABLE .... DROP COLUMN ...., this version is not available at present on Android devices and that there is no ALTER COLUMN. You will have to use an alternative means to alter the column.
The DROP COLUMN is also quite restrictive
You could do the following (where ? represents the table in question):-
DROP TABLE IF EXISTS ?_old
this is just in-case it exists (it should not)
Use the ALTER TABLE ? RENAME TO ?_old (_old just a suggested name for what is to be a temporary version of the table)
Use CREATE TABLE IF NOT EXISTS ....
Room demands that the create table SQL creates the table according to how it interprets the class annotated with #Entity.
It is suggested that you retrieve the create table SQL from the generated java that is available after compiling the project. The SQL will be in the createAllTables method of the class that is the same as the class that is annotated with #Database but suffixed with _Impl
Use INSERT INTO ? SELECT * FROM ?_old to copy existing data into the newly created version of the table
Use DROP TABLE IF EXISTS ?
So apart from the CREATE TABLE .... (which would have to be altered, see points above) the following would cater for all scenarios (new users, users on 57 and users on 56) :-
private val MIGRATION_57_58 = object : Migration(57, 58) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE IF EXISTS `app_stage_old`")
database.execSQL("ALTER TABLE `app_stage` RENAME TO `app_stage_old`")
/* NOTE the CREATE TABLE IF NOT EXISTS .... SHOULD BE ALTERED ACCORDINGLY */
database.execSQL("CREATE TABLE IF NOT EXISTS `app_stage` (`id` INTEGER, `name` TEXT NOT NULL,`hasSeenBusinessOwnerQuestion` INTEGER DEFAULT 0, PRIMARY KEY(`id`))")
database.execSQL("INSERT INTO `app_stage` SELECT * FROM `app_stage_old`")
database.execSQL("DROP TABLE IF EXISTS `app_stage_old`")
}
}
Can someone help me here, As i wanted to alter existing room table(User) by adding new column which is object(address).
`
data class UserDetailsEntity (
#PrimaryKey
var id: Int = 0,
#ColumnInfo(name ="name")
val name:String? = null,
#ColumnInfo(name = "lastName")
val lastName: String ?= null,
#Embedded
val address:Address ?= null,
)
data class Address (
#ColumnInfo(name ="lat")
val lat : String? = null,
#ColumnInfo(name = "long")
val long: String ?= null,
)
`
How to write migration for above scenario? Thanks in advance.
Do I need to write migration for this? (from comment)
Yes as the table is being altered.
By Embedding the Address then room will consider that a UserDetailsEntity object will have an Address object within it. Room will therefore build the code to set the objects values.
Additionally room will expect the table UserDetailsEntity (or whatever value is associated with the table) to have two extra columns lat and long. Therefore the migration needs to add the 2 columns. You would use 2 ALTER TABLE <tablename> ADD COLUMN <column_definition>; in the migration.
Note if you compile (Ctrl + F9) with the changes made then look in the Java(generated) subfolders, whilst in Android view of the Project window, and then look at the class, followed by _impl where you have the #Database in the createAllTables method. The SQL for the creation of the table will exist. You can then copy and paste the column definitions.
Example screen shot where the class Database is where the #Database is coded and thus Database_impl is the generated java to look at that has the create table SQL for the tables :-
In my Android application I use Room library for persistency.
Assuming, I have an Entity defined like this:
#Entity(tableName = "my_entity")
public class MyEntity {
#ColumnInfo(name = "id")
#PrimaryKey(autoGenerate = true)
private int id;
//...
}
can I rely on the fact, that id will be increased monotonically, i.e. that for newly inserted row id will always be higher, than for all previously created rows?
I think, that it is unlikely, but I can imagine, that Room (or SQLite - I am not sure, who is responsible in this case) could e.g. try to reuse the IDs of the previously deleted rows...
As far as I can see, the official documentation does not tell anything about it PrimaryKey.AutoGenerate().
This answer is the expanded comment from JensV.
As suggested by JensV, the generated schema json file contains (among others):
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, ... <other fields>)"
So looking at the SQLite docs of AUTOINCREMENT we get, that it is guaranteed to be monotonic.
In fact, this flag serves exactly for this purpose: to ensure, that the generated value is monotonic (without this flag, the value still will be generated to be unique, but will not be necessarily monotonic). Taking into account, that Room uses the flag, it is strange, that they don't mention it in the documentation.
CertificateElementEntity entity had an embedded class ImgData.
I have divided CertificateElementEntityand ImageData into separate tables.
But now I can't figure out how to make the migration.
open class CertificateElementEntity(
#IgnoreJson
#PrimaryKey
#ColumnInfo(name = "local_id")
var localId: Long? = null,
var data: String? = null,
var imageData: ImgData? = null)
Maybe someone made similar migrations
You can try this general migration schema (honestly, I haven't experienced such a migration, so may be there is way easier):
Create temporary table [CertificateElementEntityTemp] with the same structure.
Copy all data from table [CertificateElementEntity] to [CertificateElementEntityTemp].
Drop table [CertificateElementEntity].
Create table [ImgData].
Create table [CertificateElementEntity] with new structure (with just imageId instead all fields from embedded table). Create Foreign Key for [imageId].
Copy needed data from [CertificateElementEntityTemp] to [ImgData].
Copy needed data from [CertificateElementEntityTemp] to [CertificateElementEntity].
Drop table [CertificateElementEntityTemp].
All this of course you should write in migration section with equivalent SQL statements.
While searching for this, I only came across people asking how to Avoid inserting duplicate rows using room db. But my app has a feature where the user may tap a copy button and the list item will get inserted again in the db. I could have simply achieved this if my table didn't have a primary key set on one of its fields. While I found this solution for SQLite, I don't know how I can achieve this in Room Db. Because while writing an insert query with custom queries in room would defeat the purpose of using room in the first place.
Let's say you have some entity
#Entity(tableName = "foo_table")
data class Foo (
#PrimaryKey(autoGenerate = true) var id: Int,
// or without autogeneration
// #PrimaryKey var id: Int = 0,
var bar:String
)
and you have some Dao with insert:
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(foo: Foo)
Then to copy your existing value (copiedValue: Foo) you need in some way to manage your primary key:
Scenario 1. Your Primary Key is autogenerated, you have to set it to default value to get new autogenerated one:
copiedValue.id = 0
yourDao.insert(copiedValue)
Scenario 2. Your Primary Key is not autogenerated, you have to set new primary key manually:
copiedValue.id = ... // some code to set new unique id
yourDao.insert(copiedValue)