Room Database callback not working after version update - android

I am building an application that uses the Room library db and I ran into a small issue. In the first version when I create my database , I included a callback to populate my database so that I do not start with an empty database :
#Provides
#Singleton
fun provideDatabase(app: Application , callback : MyDatabase.Callback) =
Room.databaseBuilder(app , MyDatabase::class.java, "home_database")
.fallbackToDestructiveMigration()
.addCallback(callback)
.build()
In this first version it worked fine then it got to a point whereby I had to add another table into the database. This meant that the schema changed and now I had to change the database version number from 1 to 2. After I changed the version number then ran the application , the callback I had seems not to work anymore , the database starts off empty. I initially thought the fallbackToDestructiveMigration() would prevent the database from losing its data and it will just recreate itself again with the callback working. Any clue of how I can get the callback back to working again?
Database code:
#Database(entities = [User::class , Result::class] , version = 2)
abstract class MyDatabase : RoomDatabase() {
abstract fun dbDao() : Dao
class Callback #Inject constructor(
private val database : Provider<MyDatabase>,
#ApplicationScope private val applicationScope: CoroutineScope
) : RoomDatabase.Callback(){
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
val dao = database.get().dbDao()
applicationScope.launch {
dao.addUser(
User(1 , "Larry" , "Android Developer","Boston" )
)
dao.addUser(
User(2 , "Garry" , "Javascript Developer","Casablanca" )
)
}
}
}
}

The onCreate() is not called on DestructiveMigration. You need to add onDestructiveMigration to your callback, just like onCreate:
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
super.onDestructiveMigration(db)
// Add your data
}
See: https://developer.android.com/reference/androidx/room/RoomDatabase.Callback#onDestructiveMigration(androidx.sqlite.db.SupportSQLiteDatabase)

Related

Adding second Room database to my app causes exception: " Caused by: java.lang.IllegalStateException: Room cannot verify the data integrity"

I am creating a pixel art editor app and I already have one Room database which stores the users' creations and another Room database which I want to add which will store some custom color palettes the user wants to add to the app.
To do this I added the following database:
#Database(entities = [ColorPalette::class], version = 3)
abstract class ColorPalettesDatabase: RoomDatabase() {
abstract fun colorPalettesDao(): ColorPalettesDao
companion object {
private var instance: ColorPalettesDatabase? = null
fun getDatabase(context: Context): ColorPalettesDatabase {
if (instance == null) {
synchronized(ColorPalettesDatabase::class) {
if (instance == null) instance = Room.databaseBuilder(context.applicationContext, ColorPalettesDatabase::class.java, AppData.colorPalettesDBFileName).allowMainThreadQueries().build()
}
}
return instance!!
}
}
}
And the DAO:
#Dao
interface ColorPalettesDao {
#Insert
suspend fun insertColorPalette(colorPalette: ColorPalette)
#Query("SELECT * FROM ColorPalette ")
fun getAllColorPalettes(): LiveData<List<ColorPalette>>
#Query("DELETE FROM ColorPalette WHERE objId=:colorPaletteId")
fun deleteColorPalette(colorPaletteId: Int)
}
I added a variable in AppData and initialized it in the MainActivity's onCreate method:
class AppData {
companion object {
var pixelArtDBFileName = "pixel_art_db"
lateinit var pixelArtDB: PixelArtDatabase
var colorPalettesDBFileName = "color_palettes_db"
lateinit var colorPalettesDB: ColorPalettesDatabase
}
}
AppData.colorPalettesDB = ColorPalettesDatabase.getDatabase(this)
And finally, I use get the ColorPalette data from the database in the ColorPalettesFragment.kt file:
class ColorPalettesFragment(private val lifecycleOwner: LifecycleOwner) : Fragment(), ColorPalettesListener {
private var _binding: FragmentColorPalettesBinding? = null
private val binding get() = _binding!!
private lateinit var caller: ColorPalettesFragmentListener
private fun setUpRecyclerView() {
binding.apply {
fragmentColorPalettesRecyclerView.layoutManager = LinearLayoutManager(this#ColorPalettesFragment.activity).apply {
orientation = LinearLayoutManager.HORIZONTAL
}
AppData.colorPalettesDB.colorPalettesDao().getAllColorPalettes().observe(lifecycleOwner) {
fragmentColorPalettesRecyclerView.adapter = ColorPalettesAdapter(it, this#ColorPalettesFragment)
}
}
}
companion object {
fun newInstance(lifecycleOwner: LifecycleOwner) = ColorPalettesFragment(lifecycleOwner)
}
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is ColorPalettesFragmentListener) caller = context
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentColorPalettesBinding.inflate(inflater, container, false)
setUpRecyclerView()
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onColorPaletteTapped(selectedColorPalette: ColorPalette) {
caller.onColorPaletteTapped(selectedColorPalette)
}
}
So far I'm testing it when the database is empty, what I expect is whenever the user taps the following button they will see a blank RecyclerView:
Unfortunately, I can't even get to the screen as when I run the app and try to navigate to the Canvas I get the following exception:
2021-12-22 08:55:57.251 24474-24554/com.realtomjoney.pyxlmoose E/AndroidRuntime: FATAL EXCEPTION: arch_disk_io_1
Process: com.realtomjoney.pyxlmoose, PID: 24474
java.lang.RuntimeException: Exception while computing database live data.
at androidx.room.RoomTrackingLiveData$1.run(RoomTrackingLiveData.java:92)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:920)
Caused by: java.lang.IllegalStateException: Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. You can simply fix this by increasing the version number.
at androidx.room.RoomOpenHelper.checkIdentity(RoomOpenHelper.java:154)
at androidx.room.RoomOpenHelper.onOpen(RoomOpenHelper.java:135)
at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.onOpen(FrameworkSQLiteOpenHelper.java:201)
at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:427)
at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:316)
at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.getWritableSupportDatabase(FrameworkSQLiteOpenHelper.java:151)
at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase(FrameworkSQLiteOpenHelper.java:112)
at androidx.room.RoomDatabase.inTransaction(RoomDatabase.java:706)
at androidx.room.RoomDatabase.assertNotSuspendingTransaction(RoomDatabase.java:483)
at androidx.room.RoomDatabase.query(RoomDatabase.java:526)
at androidx.room.util.DBUtil.query(DBUtil.java:86)
at com.realtomjoney.pyxlmoose.dao.ColorPalettesDao_Impl$4.call(ColorPalettesDao_Impl.java:108)
at com.realtomjoney.pyxlmoose.dao.ColorPalettesDao_Impl$4.call(ColorPalettesDao_Impl.java:105)
at androidx.room.RoomTrackingLiveData$1.run(RoomTrackingLiveData.java:90)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167) 
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) 
at java.lang.Thread.run(Thread.java:920) 
I've tried to look at other solutions for a fix but nothing seems to help.
This is coming from the ColorPalettesDatabase as when I remove all of its usages in code the exception goes away.
Updating the version unfortunately doesn't help and so doesn't uninstalling the app.
It sounds like you have an existing version of the database and have changed the schema (i.e. changed the ColorPalette class).
Room has detected this change and is therefore suggesting that you increase the version number. However, you don't appear to have any Migrations therefore changing the version number would then fail.
IF you have no data (sounds like the case as per So far I'm testing it when the database is empty, what I expect is whenever the user taps the following button they will see a blank RecyclerView:). Coding .fallbackToDestructiveMigration might overcome this issue as in the absence of a Migration it will drop the tables and then create the tables.
However, the above doesn't answer and so doesn't uninstalling the app.. So at a guess you then have an ongoing issue.
I would suggest first adding .fallbackToDestructiveMigration and then rerunning, if this fails then edit you question to include the failure.
Next, assuming a failure, try uninstalling and rerunning and then, assuming it fails, editing your question with the failure under that scenario.
Demo of problem as per question
ColorPalette class - see comments regarding runs
#Entity
data class ColorPalette(
/* original V3 code */
#PrimaryKey
#ColumnInfo(name = "objId")
val objId: Long? = null,
val description: String,
val color: Long
/* additional code */
,
val extra: String
)
ColorPaletteDao (same bar no LiveData)
#Dao
interface ColorPalettesDao {
#Insert
fun insertColorPalette(colorPalette: ColorPalette)
#Query("SELECT * FROM ColorPalette ")
fun getAllColorPalettes(): /*LiveData<*/List<ColorPalette>/*>*/ /* for testing without LiveData */
#Query("DELETE FROM ColorPalette WHERE objId=:colorPaletteId")
fun deleteColorPalette(colorPaletteId: Int)
}
ColorPalleteDatabase (identical but see later)
MainActivity to demonstrate
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val db = ColorPalettesDatabase.getDatabase(this)
val dao = db.colorPalettesDao()
dao.getAllColorPalettes()
}
}
Run 1 - only original code for ColorPalette i.e. no extra column.
Runs fine database created (as per AppInspection):-
Run 2 - introduce the extra column, do nothing else, and run
ooops!!!! java.lang.RuntimeException: Unable to start activity ComponentInfo{a.a.so70444736kotlinroomincreaseversion/a.a.so70444736kotlinroomincreaseversion.MainActivity}: java.lang.IllegalStateException: Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. You can simply fix this by increasing the version number.
Run 3 - Increase version number from 3 to 4 and run
ooops!!!! java.lang.RuntimeException: Unable to start activity ComponentInfo{a.a.so70444736kotlinroomincreaseversion/a.a.so70444736kotlinroomincreaseversion.MainActivity}: java.lang.IllegalStateException: A migration from 3 to 4 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. at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2913)
Run 4 add .fallbackToDestructiveMigration() to ColorPaletteDatabase and run
Runs OK, database :-
RESET EVERYTHING BACK to initial state (including uninstalling the App) and run Run 1 and Run 2
same issue as per Run 2 as expected.
Uninstall App but without any changes (so extra column and version 3)
runs fine and database has the extra column
Hence
Why it is suggested that there are other issues, as both suggested initial fixes :-
increase version along with .fallbackToDestructiveMigration, and
uninstall App and rerun
both work to correct the reported issue.

Android database (Room) not updating after application update

I'm kind of new to Android development, but I'm taking over some project somebody more experimented than me did.
I'm making an application that retrieves data from the internet at startup and creates (or updates) a room database to store the information.
So far, it works. However, I noticed that when I push an update of the application, for some users, the database doesn't update anymore. They can reinstall the app, it doesn't change anything. The only thing that works is to clear the cache and restart the application. Then everything goes back to normal, when data are retrieved from the internet, they are properly inserted in the database. But the problem comes back with the next update.
I added the 'fallbackToDestructiveMigration()' option but it doesn't help, because it's not a migration of the database per se, as the structure doesn't change here.
Ideally, I'd like to preserve the data already present in the database.
I'm using Room 2.2.5 and API 28.
I'm not sure why updating the app results in the database not updating anymore. Maybe the new version of the app creates another database and populates this one, but the app is still using the old one (not updated anymore).
The Storage Module:
val storageModule = module {
single {
Room.databaseBuilder(androidContext(), LessonDatabase::class.java, "MyDB")
.fallbackToDestructiveMigration().build()
}
single {
get<LessonDatabase>().lessonDao()
}
}
The LessonDatase:
#Database(entities = [Lesson::class], version = BuildConfig.DATABASE_VERSION, exportSchema = false)
abstract class LessonDatabase : RoomDatabase() {
abstract fun lessonDao(): LessonDao
}
The LessonDao:
#Dao
interface LessonDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertLesson(lesson: Lesson)
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertLessons(lessons: List<Lesson>)
#Update
fun updateLesson(lesson: Lesson)
#Delete
fun deleteLesson(lesson: Lesson)
#Query("DELETE FROM Lesson")
fun clearLessons()
#Query("SELECT * FROM Lesson WHERE id == :id")
fun getLessonById(id: String): Lesson
#Query("SELECT * FROM Lesson ORDER BY creation_date DESC")
fun getLessons(): List<Lesson>
#Query("SELECT * FROM Lesson WHERE favorite = 1")
fun getFavoriteLessons(): List<Lesson>
#Query("SELECT * FROM Lesson WHERE difficulty LIKE :level")
fun getLessonsByDifficulty(level: Int): List<Lesson>
}
And the code for the application startup:
class SplashscreenViewModel(
private val repository: LessonRepository,
private val lessonDao: LessonDao,
val context: Context
) : BaseViewModel() {
val nextScreenLiveData = MutableLiveData<Boolean>()
override fun start() {
ioScope.launch {
val lessons = repository.getLessons().filter {
it.site.contains("website")
}.filter {
DataUtils.isANumber(it.id)
}
lessonDao.insertLessons(lessons)
nextScreenLiveData.postValue(true)
}
}
override fun stop() {
}
}
A question I have is, if I update the application, I guess Room.databaseBuilder will be called again. But what happens if the name of the database is the same as the previous one? Will it retrieve the old one, or create a new one? Overwrite the old one?
Another question I have, in the Insert query, it says onConflictStrategy.IGNORE. But as I pass a list as parameters, what happens if some of the entries are already in the database and some not? Will it ignore all of them? Just the already existing ones?
Thank you.
Edit: I installed Android-Debug-Database (https://github.com/amitshekhariitbhu/Android-Debug-Database) and it seems the database is fine actually. The only problem is that when I update the app, the new entries I insert are returned at the end of the SELECT * FROM table query. So I tried to sort them by Id, and it seems to work.

Why does livedata return stale data from Room

I have come across a problem that I am not being able to solve without implementing fragile hacks.
I have a table Users.
And I am observing it via LiveData.
Everytime I launch an update on that table , my observer invokes twice. Once with the old value , and then with the newly updated one.
To illustrate the problem I have created a small example I would share below
UserDao.kt
#Dao
interface UserDao {
//region Inserts
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUser(user: User)
#Update
fun update(user:User)
#Query("select * from users ")
fun users(): LiveData<List<User>>
}
I observe the live data in my MainActivity.
observe(
database.usersDao().users()
){
Log.d("Here",it.name) // i first get the previous val then the new one
}
And this is how i am registering an update also in the MainActivity
GlobalScope.launch {
database.usersDao().update(
User(
102,
"John",
"asdas",
roleCsv = "aaa",
accessToken = AccessToken("asd", "asd", 0),
loggedIn = false
)
)
}
What transpires here is catastrophic for my system .
I get a user object that has a previous name , and then I get the updated "John"
the observe is just an extension method to easily register observers
fun <T : Any, L : LiveData<T>> LifecycleOwner.observe(liveData: L, body: (T) -> Unit) =
liveData.observe(this, Observer(body))
My question was is this by design ?. Can I do something such that only the final picture from the database invokes my observer?
I recommend observing the following liveData in your case:
Transformations.distinctUntilChanged(database.usersDao().users())
Source:
https://developer.android.com/reference/androidx/lifecycle/Transformations.html#distinctUntilChanged(androidx.lifecycle.LiveData%3CX%3E)
On the other note, hold the liveData reference inside androidx's viewModel.

Android RoomDatabase get SupportSQLiteDatabase exception

I'm using Room and I need to perform a database migration. I've migrated the data but I have a problem in one of the columns. When the migration is performed, the data for that column may still be unavailable.
When the user enters the data needed for that column, I have to get all rows that match a value in that column, update these values by the one provided by the user and drop all other rows that do not match.
I can have a method in my UserDao but the problem is that this does not seem correct because it's a one time only thing and I don't what to expose the method so my idea was to get the database instance and try to do the change myself.
When I use
var myDatabase = Room.databaseBuilder(....)
.addMigrations(... .build()
I keep a reference to it but then, when I do myDatabase.openHelper.writableDatabase I'm always getting an exception
getDatabase called recursively
Any idea how to handle this?
Your issue is that you are trying to use the MyDatabase's openHelper to try to get the database when building the instance of MyDatabase which is in the process of getting the database, so while getting the database you are then trying to get the database.
Instead you need to use the SupportSQLiteDatabase that is passed to the Migration.
As ean example :-
#Database(
version = 1,
entities = [
MyTableEntity::class
]
)
abstract class AppDatabase : RoomDatabase() {
abstract fun MyTableEntityDao(): MyTableEntityDao
companion object {
val MIGRATION_V1_V2: Migration = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
//........ code using the already opened database
database.execSQL(????????); //<<<<<<<<<< USES the support database
}
}
}
}
This would then be invoked using something similar to :-
var myDatabase = Room.databaseBuilder(applicationContext,AppDatabase::class.java,"mydatabase")
.allowMainThreadQueries()
.addMigrations(AppDatabase.MIGRATION_V1_V2)
.build()

Android room persistent library - How to change database version

Im not clear on how to use room after i have updated the database version.
For example, lets say i originally had the following database defined in room:
#Database(entities = {Event.class}, version = 1)
#TypeConverters(DateTypeConverter.class)
public abstract class EventDatabase extends RoomDatabase {
public abstract EventDao eventDao();
}
and then i change the version so that it looks like this now:
#Database(entities = {Event.class}, version = 2)
#TypeConverters(DateTypeConverter.class)
public abstract class EventDatabase extends RoomDatabase {
public abstract EventDao eventDao();
}
when i saw change the version i mean that i may have added or deleted columns in the database so its not same. my questions are the following:
do i need to maintain two databases now ? v1 and v2 ? and is there a way to copy entities easily over to v2 ? also when changing the version is it enough to simply change it from 1 to 2 or do i have to create another class called EventDatabase2 for example ?
also here is the version of room i am using:android.arch.persistence.room:runtime:1.0.0-alpha1
So lets say i have a new app version and a new database version also. I simply need to change the version = 2 like this:
#Database(entities = {Event.class}, version = 2)
#TypeConverters(DateTypeConverter.class)
public abstract class EventDatabase extends RoomDatabase {
public abstract EventDao eventDao();
}
and then provide a migration policy like this:
Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
.addMigrations(MIGRATION_1_2).build();
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
#Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, "
+ "`name` TEXT, PRIMARY KEY(`id`))");
}
};
the key thing here is if a migration policy is not provided it seem the entire database is rebuilt (so your user would loose all previous data).
this is according to #commonsWare update link provided .
My answer may be late, but it may help someone like me who has only recently found the answer to the same question.
For some reason you need to upgrade the database version but do not need to adjust the database, as simple as editing the #Dao adapter or the #Entity attribute without affecting the structure of the database.
If you upgrade the Version in the Database as below:
From:
#Database(
entities = [ExampleClass::class],
version = 1,
exportSchema = false
)
To:
#Database(
entities = [ExampleClass::class],
version = 2,
exportSchema = false
)
If you do not add anything later, the database will be refreshed as if it were deleted. To avoid deletion, you can simply add an empty Migration as follows:
private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
}
}
build your database:
#Synchronized
private fun buildDatabase(context: Context, databaseName: String): AppDatabase {
return Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
databaseName
)
.addMigrations(MIGRATION_1_2)
.allowMainThreadQueries()
.fallbackToDestructiveMigration()
.build()
}
Database data will not be affected
Another option:
You can update the database version number after a modification in your DB schema, and at the same time you can use fallbackToDestructiveMigration() method in the construction of your database object if you don't want to provide a migration plan and just apply the changes you want, although it's always recommended to provide a migration trace:
#Provides
#Singleton
fun provideCryptocurrenciesDatabase(app: Application): Database = Room.databaseBuilder(app,
Database::class.java, "my_db").fallbackToDestructiveMigration().build()

Categories

Resources