I'm new in Room Database, and recently I faced a problem that has to do with modifying the App's database (update/insert/delete) without causing the callback in observer to be fired.
This is the Dao class for my model:
#Dao
interface ReceiptRowDao {
#Query("SELECT * FROM my_model ")
fun getMyModels(): LiveData<MutableList<MyModel>>
#Update
fun update(receiptRow: ReceiptRow): Int
}
My Database class:
#Database(
entities = [
(MyModel::class)
],
version = 1,
exportSchema = false
)
abstract class AppDatabase: RoomDatabase() {
abstract fun myModelDao(): MyModelDao
}
Usage:
class MyClass {
val db: AppDatabase = Room
.databaseBuilder(mContext, AppDatabase::class.java, "my_db")
.allowMainThreadQueries()
.build()
fun test() {
val myLiveData = db.myModelDao.getMyModels()
myLiveData!!.observe(this, Observer { data ->
...
val item = data.get(0)
item.id = 4
// This update should not cause a callback
db.myModelDao().update(item)
...
})
}
}
In the MyClass, the update instruction will cause an infinite loop, since an update to MyModel, will fire the observer. Then the code inside the observer will run again. This will do another update. This will fire the observer again and so on...
In such a scenario, is there a way to do the update of a model, but to skip the observers that might be listening for changes?
I think what you could do is just simply check whether data is already in the database. Like
fun test() {
val myLiveData = db.myModelDao.getMyModels()
myLiveData!!.observe(this, Observer { data ->
...
val item = data.get(0);
// This update should not cause a callback
if (!db.myModelDao().itemExists(item){
db.myModelDao().update(item)
}
...
})
}
this is the database class which holds the DAO classes along with its instance
#Database(entities = {Weight.class, DailyConsumption.class, DrinkType.class}, version = 1, exportSchema = false)
public abstract class MyDataBase extends RoomDatabase {
// DAO classes
public abstract WeightDao weightDao();
public abstract DailyConsumptionDao dailyConsumptionDao();
public abstract DrinkTypeDao drinkTypeDao();
private static MyDataBase dataBase;
public static MyDataBase getInstance(Context context){
if (null== dataBase){
dataBase= buildDatabaseInstance(context);
}
return dataBase;
}
private static MyDataBase buildDatabaseInstance(Context context) {
return Room.databaseBuilder(context,
MyDataBase.class,
Constants.DB_NAME)
.allowMainThreadQueries().build();
}
}
and the part where you want to insert the data in database takes two parameters. one database class object and the entities. I have designed the entities classes like a model class which you can use to set and get values inside main class.
dailyConsumption = new DailyConsumption(); // entity class
myDataBase = MyDataBase.getInstance(this);
dailyConsumption.setIcon(mIcon);
dailyConsumption.setQuantity(mQuantity);
dailyConsumption.setTime(strTime);
dailyConsumption.setIsSelected(0);
dailyConsumption.setDrinkUnit(drinkUnit);
dailyConsumption.setDateTime(insertionDate);
dailyConsumption.setDate(date);
setDailyDataBase(myDataBase, dailyConsumption);
and the method setDailyDatabase just calls the database class to insert the entity
private void setDailyDataBase(MyDataBase dataBase, DailyConsumption dailyConsumption) {
// query takes parameters to update respective columns
myDataBase.dailyConsumptionDao().updateItem(mId, mQuanity, mTime, date);
}
For your issue, i would suggest you following way for observing LiveData & updation of your Model:
class MyClass {
val db: AppDatabase = Room
.databaseBuilder(mContext, AppDatabase::class.java, "my_db")
.allowMainThreadQueries()
.build()
fun getDataObserver() = db.myModelDao.getMyModels()
fun test(item: MyModel) {
db.myModelDao().update(item)
}
}
This will help you seperate your observer logic from update logic, now call getDataObserver() method where you want to observe data and use your test() method when you want to update your Model.
Related
In my project, I use the Singleton design pattern, add a companion object and a function that returns a database object instance. This will avoid creating multiple instances
database object through which the connection to the SQL server is established.
I have the following code to connect to the database:
// Annotates class to be a Room Database with a table (entity) of the Word class
#Database(entities = arrayOf(ShoppingList::class), version = 1, exportSchema = false)
public abstract class ShoppingListRoomDatabase : RoomDatabase() {
abstract fun shoppingListDao(): ShoppingListDao
companion object {
// Singleton prevents multiple instances of database opening at the
// same time.
#Volatile
public var INSTANCE: ShoppingListRoomDatabase? = null
fun getDatabase(context: Context, scope: CoroutineScope): ShoppingListRoomDatabase {
// if the INSTANCE is not null, then return it,
// if it is, then create the database
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
ShoppingListRoomDatabase::class.java,
"shopping_list_database"
).addCallback(ShoppingListDatabaseCallback(scope)).build()
INSTANCE = instance
// return instance
instance
}
}
}
}
private class ShoppingListDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
ShoppingListRoomDatabase.INSTANCE?.let { database ->
scope.launch {
populateDatabase(database.shoppingListDao())
}
}
}
fun populateDatabase(shoppingListDao: ShoppingListDao) {
shoppingListDao.deleteAll()
var shoppingList = ShoppingList(1,"First List")
shoppingListDao.insert(shoppingList)
shoppingList = ShoppingList(2, "Second List!")
shoppingListDao.insert(shoppingList)
}
}
Interface:
#Dao
interface ShoppingListDao {
#Query("SELECT * FROM shopping_lists ORDER BY id ASC")
fun getOrderedShoppingLists(): Flow<List<ShoppingList>>
#Insert
fun insert(shoppingList: ShoppingList)
#Query("DELETE FROM shopping_lists")
fun deleteAll()
}
How can I get this database instance in another kotlin class to work with it?
I'm currently trying to set up some initial data in the Room database.
As a result, the initial data setup was successful, but App Inspection confirmed that the data is saved only when getWorkoutList().
To explain in more detail, the initial data is not saved with the insert function alone, and the initial data is saved in the DB only when a function that calls the DB data called getWorkoutList() is executed from the ViewModel.
When the database is created in the view model, I expected the initial data to be saved only with the insert function. But it wasn't.
Why is the initial data not saved with only the insert function?
The following is the DB status in App inspection according to the function call.
1. When only insertWorkoutList(data) is executed
There is no DB and table creation, and no initial data is saved.
2. When only getWokroutList() is executed.
DB and table are created, but there is no data.
3. When both are executed.
Initial data is normally saved.
Code
Dao
#Dao
interface WorkoutListDao {
#Query("SELECT * FROM WorkoutList")
suspend fun getWorkoutList() : WorkoutList
#Insert
suspend fun insertWorkoutList(workoutList: WorkoutList)
}
WorkoutListDatabase
#Database(
entities = [WorkoutList::class],
version = 1
)
#TypeConverters(WorkoutListTypeConverter::class)
abstract class WorkoutListDatabase : RoomDatabase() {
abstract fun workoutListDao() : WorkoutListDao
companion object {
private var INSTANCE : WorkoutListDatabase? = null
#Synchronized
fun getDatabase(context: Context) : WorkoutListDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WorkoutListDatabase::class.java,
"workoutlist_db"
)
.addCallback(WorkoutListCallback(context))
.build()
INSTANCE = instance
instance
}
}
}
}
WorkoutListCallback
class WorkoutListCallback(private val context: Context) : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
CoroutineScope(Dispatchers.IO).launch {
fillWithStartingWorkoutList(context)
}
}
private fun fillWithStartingWorkoutList(context: Context) {
val dao = WorkoutListDatabase.getDatabase(context).workoutListDao()
try {
val data = loadJsonData(context)
// dao.insertWorkoutList(data)
} catch (e: JSONException) {
e.printStackTrace()
}
}
private fun loadJsonData(context: Context) : WorkoutList {
val assetManager = context.assets
val inputStream = assetManager.open(WORKOUTLIST_JSON_FILE)
BufferedReader(inputStream.reader()).use { reader ->
val gson = Gson()
return gson.fromJson(reader, WorkoutList::class.java)
}
}
}
ViewModel
class WorkoutListViewModel(application: Application) : AndroidViewModel(application) {
private val workoutDao = WorkoutListDatabase.getDatabase(application).workoutListDao()
private val workoutListRepo = WorkoutListRepository(workoutDao)
fun setList(part : BodyPart) {
viewModelScope.launch(Dispatchers.IO) {
workoutListRepo.getWorkoutList()
}
}
}
The callback will only be called when the database is actually accessed NOT when an instance of the #Database annotated class is obtained.
As such the database is not created by:-
val dao = WorkoutListDatabase.getDatabase(context).workoutListDao()
but is created by
workoutListRepo.getWorkoutList()
That is the actual relatively resource hungry action of opening the database is left until it is definitely needed.
A get around could be to use :-
fun getDatabase(context: Context) : WorkoutListDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WorkoutListDatabase::class.java,
"workoutlist_db"
)
.addCallback(WorkoutListCallback(context))
.build()
INSTANCE = instance
instance.getOpenHelper().getWritableDatabase() //<<<<< FORCE OPEN
instance
}
}
This would then force an open of the database and if it does not actually exist then the overidden onCreate method will be invoked. However,this (I think) would be done on the main thread, which you do not want.
I would strongly suggest NOT using functions from the #Dao annotated class(es) especially if they have suspend as you then may have no control over when the threads will run.
Instead you should use the SupportSQLiteDatabase passed to the function, it has many methods e.g. you would likely use the insert method.
see ROOM database: insert static data before other CRUD operations for an example where the order was an issue. The example includes the conversion of the actions to utilise the intended SupportSQLiteDatabase methods.
You may notice that the example includes placing all the database changes into a single transaction, which is more efficient as instead of each individual action (insert/delete in the example) writing to disk, the entire transaction (all actions) are written to disk once.
I am doing this google codelab android-room-with-a-view-kotlin. This is the link to codelab. At the 8th step when creating room database they have used this code
// Annotates class to be a Room Database with a table (entity) of the Word class
#Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
public abstract class WordRoomDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
companion object {
// Singleton prevents multiple instances of database opening at the
// same time.
#Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(context: Context): WordRoomDatabase {
// if the INSTANCE is not null, then return it,
// if it is, then create the database
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"word_database"
).build()
INSTANCE = instance
// return instance
instance
}
}
}
}
I want to ask why there is no error in these 2 lines (val exam & val dao) where I create an instance of this abstract class (in class A) and then call its abstract function(getNoteDao) without overriding it.
class A{
val exam : WordRoomDatabase = WordRoomDatabase.getDatabase(application)
val dao = exam.getWordDao()
}
Since we know we need to override the abstract function and can not directly call it but what's happening there. why is there no error
There's no error on the exam line because getDataBase is a "companion" object, or if you're from the Java world, it means it's a "static" function within the abstract class. This means
a function within a companion object (or again, a static function) belongs the the CLASS, not the the INSTANCE of the class
you CAN NOT call a static/companion object function on the instance of the class, so notice when you call "WordRoomDatabase.getDatabase..." there are no parenthesis at the end of "WordRoomDatabase". You didn't need to create an instance of it in order to call the getDatabase function
There is no error in line 2 is a little trickier to spot.
Inside of getDatabase() you are Room.dataBaseBuilder(...) and passing in the abstract class. Inside of that builder, android actually creates the instance of your abstract WordRoomDatabase. class and overrides your abstract wordDao function
If you're using AndroidStudio, build your code. After it's done there will be a little green arrow pointing down on the column next to WordRoomDatabase. If you click on it, you'll be able to see the class that Room generated that overrides your abstract function
You don't get any error because implementation of those abstract classes is generated automatically by kapt at compile time. If you look closly at your build.gradle file, then you will see that it contains a dependency in the form
kapt 'androidx.room:room-compiler:X.X.X'
Here kapt stands for kotlin annotation processing tool, which processes all your Room classes marked with certain annotations such as #Database or #Dao and generates their implementations. For example, I defined following #Dao interface
#Dao
interface WordDao {
#Insert
fun insert(word: Word)
}
And kapt generated following implementation of this class
public final class WordDao_Impl implements WordDao {
private final RoomDatabase __db;
private final EntityInsertionAdapter<Word> __insertionAdapterOfWord;
public WordDao_Impl(RoomDatabase __db) {
this.__db = __db;
this.__insertionAdapterOfWord = new EntityInsertionAdapter<Word>(__db) {
#Override
public String createQuery() {
return "INSERT OR ABORT INTO `Word` (`someId`) VALUES (?)";
}
#Override
public void bind(SupportSQLiteStatement stmt, Word value) {
stmt.bindLong(1, value.getSomeId());
}
};
}
#Override
public void insert(final Word word) {
__db.assertNotSuspendingTransaction();
__db.beginTransaction();
try {
__insertionAdapterOfWord.insert(word);
__db.setTransactionSuccessful();
} finally {
__db.endTransaction();
}
}
}
Same goes for WordRoomDatabase, its implementation is also generated automatically. if you want to look at these classes you can always find them at \app\build\generated\source\kapt\debug\yourpackage, they are marked with _Impl suffix.
I have an application where i need to prepopulate my database when it is created , i'm using dagger hilt to inject and provide dependencies ( room daos ) , when i try to insert data , it asks for movie database but don't know how to get its reference inside appmodule, thank you for any help in advance.
This is my database :
// this is my database
#Database(entities = [DataModel::class,MovieResultItem::class], version = 1, exportSchema = false)
abstract class MoviesDatabase : RoomDatabase() {
abstract fun popularDao() : PopularMoviesDao
}
Providing dao as dependency
#Singleton
#Provides
fun providePopularMoviesDao(moviesDatabase: MoviesDatabase) : PopularMoviesDao {
return moviesDatabase.popularDao()
}
Providing database instance
#Singleton
#Provides
fun provideDatabase(#ApplicationContext context: Context): MoviesDatabase {
return Room.databaseBuilder(context.applicationContext,
MoviesDatabase::class.java, "movie.db")
.addCallback(object : RoomDatabase.Callback(){
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
// here it asks for database instance , not sure how to get it
providePopularMoviesDao().insertPopularMovies(getMovieResultItem())
}
})
.fallbackToDestructiveMigration()
.build()
}
#Database(entities = [DataModel::class,MovieResultItem::class], version = 1, exportSchema = false)
abstract class MoviesDatabase : RoomDatabase() {
abstract fun popularDao(): PopularMoviesDao
#Volatile
private var INSTANCE: MoviesDatabase? = null
#Singleton
#Provides
fun provideDatabase(#ApplicationContext context: Context): MoviesDatabase {
return INSTANCE ?: synchronized(this) {
val databaseInstance = Room.databaseBuilder(
context.applicationContext,
MoviesDatabase::class.java, "movie.db"
)
.fallbackToDestructiveMigration()
.addCallback(InsertDatabaseCallback())
.build()
INSTANCE = databaseInstance
return instance
private class InsertDatabaseCallback() :RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
INSTANCE?.let {
it.providePopularMoviesDao().insertPopularMovies(getMovieResultItem())
}
}
}
}
}
}
Would you please use the above code in your class MoviesDatabase.
Here I have added an INSTANCE variable for database and it is marked as Volatile.
Volatile means, it will not be stored in the local cache. There are lot of articles you can explore for more information on Volatile.
I have modified the provideDatabase function, so that the database instance is stored in variable INSTANCE
Then you can use this INSTANCE wherever required.
Also, I have separated the callback into class InsertDatabaseCallback for simpilicity and readability. Here you can use the database INSTANCE to perform your operation providePopularMoviesDao().insertPopularMovies(getMovieResultItem())
NOTE: The code may show syntax error or curly braces error, depending on your setup and imports. Do not worry there, please make slight changes OR add curly brackets (if required) and it should work fine.
According to documentation room instance from Room.databaseBuilder() should save data is persist. But still get lost. My Project have to database
First Database
#Database(entities = [FoodModel::class], version = 4, exportSchema = false)
abstract class FoodDatabase : RoomDatabase() {
abstract val foodDatabaseDao: FoodDatabaseDao
companion object {
#Volatile
private var INSTANCE: FoodDatabase? = null
fun getInstance(context: Context): FoodDatabase {
synchronized(this) {
var instance = INSTANCE
if (instance == null) {
instance = Room.databaseBuilder(
context.applicationContext,
FoodDatabase::class.java,
Constants.OVERVIEW_FOOD_DATABASE
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
}
return instance
}
}
}
}
Second Databse
#Database(entities = [MyFoodModel::class], version = 3, exportSchema = false)
abstract class MyFoodDatabase : RoomDatabase() {
abstract val myFoodDatabaseDao: MyFoodDatabaseDao
companion object {
#Volatile
private var INSTANCE: MyFoodDatabase? = null
fun getInstance(context: Context): MyFoodDatabase {
synchronized(this) {
var instance = INSTANCE
if (instance == null) {
instance = Room.databaseBuilder(
context.applicationContext,
MyFoodDatabase::class.java,
Constants.OVERVIEW_FOOD_DATABASE
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
}
return instance
}
}
}
}
Dao of first Database
#Dao
interface MyFoodDatabaseDao {
#Insert
fun insert(food: MyFoodModel)
#Query("SELECT * FROM MyFoodItems ORDER BY name DESC")
fun getAllFood(): LiveData<List<MyFoodModel>>
#Delete
fun deleteFood(foodModel: MyFoodModel)
}
Dao of Second database
#Dao
interface MyFoodDatabaseDao {
#Insert
fun insert(food: MyFoodModel)
#Query("SELECT * FROM MyFoodItems ORDER BY name DESC")
fun getAllFood(): LiveData<List<MyFoodModel>>
#Delete
fun deleteFood(foodModel: MyFoodModel)
}
An android application can have more than one database.
Here as I can see, You are providing same name [Constants.OVERVIEW_FOOD_DATABASE] to your both the databases [MyFoodDatabase, FoodDatabase]. So all values will be written in one database named as Constants.OVERVIEW_FOOD_DATABASE.
Please provide both the database different name and try again.
Edited
As you said, you are using two different instance of same databases and for every database instance, you are changing the database version but you are not migrating your database into that version. Instead you are using fallbackToDestructiveMigration() that does not crash database but clear the data when any existing version is found.
Please try below steps:
remove fallbackToDestructiveMigration() from both database instances.
in second instance add .addMigrations(MIGRATION_1_2) while creating
instance
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
// do nothing because you are not altering any table
}
}
in First instance add .addMigrations(MIGRATION_2_1) while creating instance
val MIGRATION_2_1 = object : Migration(2, 1) {
override fun migrate(database: SupportSQLiteDatabase) {
// do nothing because you are not altering any table
}
}
It will migrate you same database. In my case it is working. I hope it will work in your case too. :)
But it is better to use single database instance and include the list of entities associated with the database within the annotation.
Because room database instances are expensive.
https://developer.android.com/training/data-storage/room
Note: If your app runs in a single process, you should follow the singleton design pattern when instantiating an AppDatabase object. Each RoomDatabase instance is fairly expensive, and you rarely need access to multiple instances within a single process.
If your app runs in multiple processes, include enableMultiInstanceInvalidation() in your database builder invocation. That way, when you have an instance of AppDatabase in each process, you can invalidate the shared database file in one process, and this invalidation automatically propagates to the instances of AppDatabase within other processes.