I have a Site and corresponding SiteDao:
#Dao
interface SiteDao {
#get:Query("SELECT * FROM site WHERE uid = 1 LIMIT 1")
val site: LiveData<Site>
#get:Query("SELECT * FROM site WHERE uid = 1 LIMIT 1")
val getSiteSync: Site
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(context: Site)
}
This works:
siteRepository.getSite().observe(activity, Observer<Site> {
// `it` is instance of Site, working as intended
})
This doesn't:
Thread {
val site = siteRepository.getSiteSync()
// site is null
}.start()
Nevermind that I'm using Repository instead of ViewModel, just an example.
Any idea why?
Room doesnt allow synchronous queries by default.
To achieve that you have to explicity call allowMainThreadQueries while initializing your database.
That is designed that way because database selection should observe for changes and ain't fetched immediately.
Related
In my Android App, I use Room as local database to store the Account information of a user. When I make a simple Room request to retrieve the Account object stored in the database, I get the following error message :
java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
Here, is the Fragment code from which I make the local database request:
// AccountInformationFragment.kt
accountDataFragmentViewModel.retrieveAccountData(accountId).observe(viewLifecycleOwner, Observer {
// do some stuff
})
In the ViewModel class I have implemented retrieveAccountData() like this:
// AccountInformationFragmentViewModel.kt
// used to get the account from the local datasource
fun retrieveAccountData(id:Long): LiveData<Account>{
val result = MutableLiveData<Account>()
viewModelScope.launch {
val account = authRepository.retrieveAccountData(id)
result.postValue(account)
}
return result
}
In the Repository class, I have implemented retrieveAccountData() like this:
// AccountRepository.kt
suspend fun retrieveAccountData(accId:Long): Account =
accountDao.retrieveAccountData(accId)
I understand that I have to use some sort of asnyc operation because the local database operation may take a long time when its performed on the main thread.
But in the ViewModel class I launched the coroutine inside the viewModelScope. Is that not enough? Based on the exception, it seems not. So, is there someone who could tell me how to do this correctly.
EDIT:
Here is the Dao class :
#Query("SELECT * FROM account_table WHERE id = :id")
fun retrieveAccountData(id: Long) : Account
Thanks in advance
As per the Room documentation, if you want Room to automatically move to a background thread to run your #Query, you can make your method a suspend method:
#Query("SELECT * FROM account_table WHERE id = :id")
suspend fun retrieveAccountData(id: Long) : Account
RoomDB supports LiveData. You could return the query result as a livedata which is by default does the operation in the background thread and observe it in your UI layer. I have modified your query below which will return LiveData instead of Account.
#Query("SELECT * FROM account_table WHERE id = :id")
fun retrieveAccountData(id: Long) : LiveData<Account>
Is it possible to check if Flow sends back a value and to act on it if it doesn't?
override suspend fun insertUserResponse(userResponse: UserResponse) {
val userResponseFromBDD: Flow<UserResponse>? = userResponseDAO.searchUserByToken(userResponse.profilePOJO.uniqueID)
userResponseFromBDD?.collect {
userResponseDAO.updateUser(userResponse)
} ?: {
userResponseDAO.insertUser(userResponse)
}
}
Several remarks:
There are two types of queries - one-shot and streams. For your use-case you need one-shot query (to ge response or null once with searchUserByToken and once - to insert or update value). For one-shot queries in Room you can use suspend function with no Flow:
#Query("SELECT * FROM userResponse where id = :id")
suspend fun searchUserByToken(id: Int):UserResponse?
And your code with checking value would be:
override suspend fun insertUserResponse(userResponse: UserResponse) {
val userResponseFromBDD = userResponseDAO.searchUserByToken(userResponse.profilePOJO.uniqueID)
userResponseFromBDD?.let { userResponseDAO.updateUser(userResponse)}
?: userResponseDAO.insertUser(userResponse)
}
With Flow you get stream, that issues values whenever data is updated in DB. That's why in your use-case you can get values in loop: you get value value from searchUserByToken -> you update value -> you get new value since Room uses Flow and invokes searchUserByToken again -> you update value -> ...
If you use Room's #Insert(onConflict = OnConflictStrategy.REPLACE) you can not to check if userResponse is in DB and use just insertUser method (since if user is in db insert would cause it's update)
I have an app which has a session end routine. I want to update the session with the end date/time and potentially other information when the End Session button is clicked. I have a dao, a repository, and a ViewModel.
I thought the best way to do this would be to get the record, update the fields in the object, and save the object back to Room.
I don't exactly know the best way to go about this. Here is the code I am working with:
In the Dao:
#Query("SELECT * FROM Session WHERE id=:id")
Single<Session> getSessionById(int id);
In the repository:
public Single<Session> getSessionById(int sessionId) {
return sessionDao.getSessionById(sessionId);
}
In the ViewModel:
public void endSession () {
Single<Session> session = sessionRepository.getSessionById(sessionId);
//????? session.doOnSuccess()
//get current date/time
Date date = Calendar.getInstance().getTime();
//set the end date
session.setEndTime(date);
//update the session
sessionRepository.update(session);
}
I would love any advice on the range of options. I had started using a plain object, but got errors related to running the query on the main thread and wanted to avoid that. I don't need an observable/flowable object. I understand Async is to be deprecated in Android 11. So I thought Single would work.
Any advice on my choice to use Single or code would be really helpful.
UPDATE:
I just need a simple example in Java of pulling a single record from and the main part is the functionality in the ViewModel (what does the doOnSuccess look like and optionally on error as well).
If you just want to update without retrieving the whole record, writing a custom query is the option that I go with:
#Query("UPDATE table_name SET column_name = :value WHERE id = :id")
fun update(id: Int, value: String): Completable
In repository:
fun update(id: Int, value: String): Completable
In ViewModel or UseCase:
update().subscribeOn(...).observeOn(...).subscribe()
If you want to avoid using RxJava:
#Query("UPDATE table_name SET column_name = value WHERE id = :id")
fun update(id: Int, value: String): Boolean
And use Executors to run transactions on a background thread.
Executors.newSingleThreadExecutor().execute {
repository.update()
}
If you want to perform both retrieving and updating you can use #Update and #Query to retrieve the recorded inside your ViewModel or UseCase (interactor) and push the update toward Room.
RxJava Single Example:
#Query("SELECT * FROM table_name")
fun getAll(): Single<List<Model>>
Repository:
fun getAll(): Single<List<Model>> = modelDao.getAll()
ViewModel or UseCase:
val statusLiveData = MutableLive<Strig>()
modelRepository.getAll()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ statusLiveData.value = "Success" }, { statusLiveData.value = "Failed" })
I would like to perform an asynchonous operation on each record in a large Room table.
I thought I could add a method returning Flow in my DAO like this:
#Query("SELECT * FROM events")
fun getEvents(): Flow<EventEntity>
But according to this blog post and this documentation page returning a Flow is making an "observable read" so the Flow never completes and it watches for database changes.
My goal is to iterate over all the entities only once. I don't want the "observability" behavior. Also, since the table is very large, I don't want to load all the records into a List at once in order to avoid consuming too much memory.
Could you recommend some solution, please?
Create a new method that does not use Flow.
#Query("SELECT id FROM events")
fun getAllIds(): List<Int> // If your primary key is Integer.
#Query("SELECT * FROM events WHERE id = :id")
fun getById(id: Int): EventEntity?
Use Kotlin coroutines to call this method on IO thread.
There could be several strategies to load one row at a time. This is the simplest - get all ids and load each item one at a time.
suspend fun getEvents() {
withContext(Dispatchers.IO) {
// Get entities from database on IO thread.
val ids = dao.getAllIds()
ids.forEach { id ->
val event = dao.getById(id)
}
}
}
Pagination based approach
This approach assumes that you have a column that stores timestamp (eg. created_at).
#Query("SELECT * from events WHERE created_at > :timestamp ORDER BY created_at LIMIT 10")
fun getAfter(timestamp: Long): List<EventEntity>
You can use this method to paginate.
suspend fun getEvents() {
withContext(Dispatchers.IO) {
var timestamp: Long = 0
while (true) {
// Get entities from database on IO thread.
val events = dao.getAfter(timestamp)
// Process this batch of events
// Get timestamp for pagination offset.
timestamp = events.maxBy { it.createAt }?.createAt ?: -1
if (timestamp == -1) {
// break the loop. This will be -1 only if the list of events are empty.
}
}
}
}
In a Repository class (see: https://developer.android.com/jetpack/docs/guide), I'm trying to:
1) Read a value from the Room DB
2) Increment the value
3) Write the value back out through the appDao to Room.
I'm pretty sure I can solve this at the Dao level, ie in a Transaction or something, but I'm not sure if that's the right approach. It seems like a pretty simple use case, and the solutions I've come up with seem way more complicated than necessary. I'm wondering if my tenuous handle on Kotlin coroutines is holding me back here.
/* Repository Class*/
fun getCurrentAlarmTime():LiveData<Calendar> {
return Transformations.map(appDao.getProfileData(currentAlarmTime)){ nullableProfileData ->
if (nullableProfileData == null) {
defaultAlarmTime
} else {
nullableProfileData.value.toLongOrNull()?.let { millis ->
getCalendar(millis)
}
}
}
}
fun setCurrentAlarmTime(calendar:Calendar) {
GlobalScope.launch {
appDao.setProfileData(
ProfileData(
currentAlarmTime,
calendar.timeInMillis.toString()
)
)
}
}
fun incrementAlarmTimeBy1Hour() {
// this is what I'm having a problem with, using the fns above.
// I've got a pretty good handle on LiveData,
// Transformations, and MediatorLiveData,
// but I am still stuck.
}
Expected result would be that the time in the database is updated by 1 hour.
I think I figured out something that's not a terrible solution, by reading the reference for Room here: https://developer.android.com/training/data-storage/room/accessing-data
The crux of it is that Room can return LiveData, or it can return what I would call synchronous queries. It's the difference between the following two signatures:
#Query("SELECT * FROM profileData WHERE entry = :entry limit 1")
fun getProfileData(entry: String): LiveData<ProfileData?>
#Query("SELECT * FROM profileData WHERE entry = :entry limit 1")
suspend fun getProfileDataSync(entry: String): ProfileData
The first one you would observe, the second one you could call directly from a coroutine.
Someone should let me know if this is not best practice, but it seems like that reference material above supports it.
To note, I didn't have to put the Room DB in any kind of weird synchronous mode to support this.