Android RoomDatabase get SupportSQLiteDatabase exception - android

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()

Related

Room Database callback not working after version update

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)

Unlike Livedata, using Flow in Room query doesn't trigger a refresh when updating a table entry but works if I delete or insert an entry

When using Livedata as a return type for a select* query on a table in Room, then I observe on it, I get triggers if I update/insert/delete an entry in that table. However, when I tried using Kotlin Flow, I only get 2 triggers.
The first trigger gives a null value as the initial value of the stateflow is a null. The second trigger is the list of entries in the Room table.
If I perform an insert/delete action on the DB, I receive a trigger from the StateFlow.
However, If I update an entry, the Stateflow doesn't trigger.
N.B: The update operation works correctly on the DB. I checked using DB inspector.
Data class & DAO
#Entity
data class CartItem (
#PrimaryKey
val itemId: Int,
var itemQuantity: Int=1
)
#Dao
interface CartDao {
#Query("SELECT * FROM CartItem")
fun getAllItems(): Flow<List<CartItem>>
#Update
suspend fun changeQuantityInCart(cartItem:CartItem)
#Insert
suspend fun insert(item: CartItem)
#Delete
suspend fun delete(cartItem:CartItem)
}
ViewModel
val cartItems: StateFlow<List<CartItem>?> =
repo.fetchCartItems().stateIn(viewModelScope, SharingStarted.Lazily, null)
Fragment
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.cartItems.collect {
Log.e("Update","Update")
}
My pitfall was that I was updating the object like this:
currentItem.itemQuantity = currentItem.itemQuantity + 1
changeQuantity(currentItem)
(currentItem is an object of class CartItem which is received initially from the getAllItems Flow in the DAO.)
(changeQuantity fun calls the changeQuantityInCart fun in the DAO.
This caused the reference of the CartItem object in the StateFlow to hold the updated value of the object with the new itemQuantity value before calling the update on the DB.
After that, when calling the Update fun in the DAO, the DB entry is updated and the Flow value changes, but when putting it in the Stateflow no changes are detected. Thus, the stateflow doesn't trigger as it is how stateflows differ from livedata.
In the case of livedata, it will trigger regardless if the new value is the same or not.
Thus, to solve this bug do not change the value of the object in the stateFlow before calling a DB update operation like this:
val updatedCartItem = cartItem.copy(itemQuantity = cartItem.itemQuantity + 1)
changeQuantity(updatedCartItem)

Rename and delete Room Database

I am wondering how to rename or delete a Room Database which was instantiated by following code:
AppDatabase db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "database-name").build();
Should I expect a database-name.db file in getApplicationContext().getFilesDir(), which I could then rename or delete? Would this approach be safe?
My current workaround would be to use only one database with multiple tables, emulating multiple databases. I am not sure if this approach scales very well though.
I could not find any answers to this question in the documention. I am not asking how to rename or delete columns or rows.
To rename a room database you can do the following when getting your database instance :
//Kotlin
fun getDatabase(context: Context) : YourRoomDbClass {
return INSTANCE ?: synchronized(this){
val dbFile = context.applicationContext.getDatabasePath("your_old_db_name.db")
if(dbFile.exists()) dbFile.renameTo(File(dbFile.path + "your_new_db_name.db"))
val instance = Room.databaseBuilder(context.applicationContext, YourRoomDbClass::class.java, "your_new_db_name.db")
.build()
INSTANCE = instance
instance
}
}

android - How to get simple list from db using Room?

I am using Component libraries in my android app. in some case it is needed to use Livedata and observe its data but sometimes I just want to get some ordinary list not Livedata , How can I do that? query DB in simple way
p.s : I use getValue() but it returns null
Use query like this in DAO:
#Query("SELECT * FROM TABLE_NAME")
fun getListOfData(): List<Data>?
this will provide you list of data from your table, just like the select query passed in #Query parameter.
Edit:
When calling from main thread, you can use handler to do your job in background like below:
//Method from where you want your data from Db.
fun getMyList() {
Thread {
(your db object).(your dao).getListOfData()
}.start()
}
or you can allow your db to execute on main thread when building your room db like below (Though i wouldn't recommend this) :
Room.databaseBuilder(
...
)
.allowMainThreadQueries()
.build()
You can simply write query in your Dao which has return type as List and call from your ViewModel where you need those data.
Example :
//YourDao
#Query("SELECT * FROM YourTable")
List<YourModel> getAllYourTableData();
//YourRepo
public static List<YourModel> getAllData(){
return getYourModelDao.getAllYourTableData();
}
//Your ViewModel
public void someFunctionWhereYouNeedNormalData(){
//assign to list
YourRepo.getAllData();
}
Assuming you have knowledge about repo pattern in android arch components.

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