Why do room database creations only need to synchronize `Room.databaseBuilder.build`? - android

I'm following the Android Room with a View - Kotlin code lab and I came across the following code:
companion object {
// Singleton prevents multiple instances of database opening at the
// same time.
#Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(context: Context): WordRoomDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"word_database"
).build()
INSTANCE = instance
return instance
}
}
}
For the most part, this makes sense. However, I'm a little confused by the synchronization part. Technically, couldn't two threads call getDatabase at the same time, and both of them get past the if statement and then each create their own database handles? I would think all of the code in getDatabase needs to be protected by the mutex. Why does only the creation part need to be protected by a mutex?

Related

Pass Application to singleton directly in Android

I'm a beginner to Android development.
When making a singleton in the Application Context, here is my code.
I pass the application context to the instantiation
class Blah{
companion object {
#Volatile
private var INSTANCE: Blah? = null
//Singleton
fun getInstance(applicationContext: Context): Blah =
INSTANCE ?: synchronized(this) {
INSTANCE ?: Blah(applicationContext).also { INSTANCE = it }
}
}
}
Can I pass the application directly to the instantiation? Like so:
class Blah{
companion object {
#Volatile
private var INSTANCE: Blah? = null
//Singleton
fun getInstance(application: Application): Blah =
INSTANCE ?: synchronized(this) {
INSTANCE ?: Blah(application).also { INSTANCE = it }
}
}
}
Will this present memory leaks?
Application is also a singleton. This doesn't cause a memory leak because there is only one instance of Application and when your app is running you have one and when your app isn't running there is nothing there, so go for it.

(Android) When is the initial data stored in the Room database?

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.

Singleton design pattern to get a RoomDatabase instance

I´m working with RoomDatabase and after setting up my DAO, Database, and Entity classes I need to find a way to get the RoomDatabase instance, but on the documentation, I see this:
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.
So from the Udacity courses I see there are various ways to do this the first one:
private lateinit var INSTANCE: MainDBForObjects
fun getDatabase(context: Context): MainDBForObjects{
if (!::INSTANCE.isInitialized){
INSTANCE = Room.databaseBuilder(
context,
MainDBForObjects::class.java, "database"
).fallbackToDestructiveMigration()
.build()
}
return INSTANCE
}
and the other one is with a companion object from the database abstract class:
companion object {
#Volatile
private var INSTANCE: SleepDatabase? = null
fun getInstance(context: Context): SleepDatabase {
synchronized(this) {
var instance = INSTANCE
if (instance == null) {
instance = Room.databaseBuilder(
context.applicationContext,
SleepDatabase::class.java,
"sleep_history_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
}
return instance
}
}
}
So is there any important difference between these 2? is it one better than the other?
Added an implementation to create a Singleton object through double check locking.
This would ensure that a lock is only acquired when required (if the database object is uninitialized) as synchronization is generally expensive
The double check here refers to the null check again within synchronized block, this helps us in scenarios where a thread A had acquired the lock to initialize the object for the first time and other threads were also waiting for it (thread B, C) now as soon as thread A initializes the object and releases the lock all waiting threads immediately get the updated value.
The need for using a local variable is to ensure that partially initialized objects are not visible to threads leading to inconsistent state (related to language semantics, more info here --> https://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java)
companion object {
#Volatile
private var sInstance: SleepDatabase? = null
#JvmStatic
fun getInstance(context: Context): SleepDatabase {
val localInstance = sInstance
if (localInstance != null) {
return localInstance
}
return synchronized(this) {
var instance = sInstance
if (instance == null) {
instance = Room.databaseBuilder(
context.applicationContext,
SleepDatabase::class.java,
"sleep_history_database"
)
.fallbackToDestructiveMigration()
.build()
sInstance = instance
}
instance
}
}
}

Room databse loses data on restart application

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.

Why Room.databaseBuilder function requires context as the parameter in a Room database?

companion object {
#Volatile
private lateinit var instance: ExampleDatabase
fun getInstance(context: Context): ExampleDatabase {
synchronized(this) {
if(!::instance.isInitialized) {
instance = Room.databaseBuilder(
context.applicationContext, // Why does this require context?
LottoDatabase::class.java,
"lotto_database"
)
.fallbackToDestructiveMigration()
.build()
}
return instance
}
}
}
The above code is the general way of creating singleton of the room database.
I wonder why Room.databaseBuilder function requires a context as the parameter. I know this question might be stupid cuz I'm lack understanding of the Context in Android.
What argument should I pass in that parameter?
What can be different if I pass in the Activity context or application?

Categories

Resources