I am building an Android application with MVVM Architecture. Table A has an Auto-Generated Primary Key column which is a Foreign Key into Table B. When the user clicks a button on the main fragment a row is inserted into Table A. As part of this button's onClickListener, I'd like to retrieve the Auto-Generated Primary Key value (rowId) after creation and insert it into a column in Table B along with more data.
I am using MVVM Architecture, Coroutines, etc... but cannot figure out how to do this. Below is some code and I'm happy to post more if anyone can help.
**// DAO Code**
#Dao
interface WLDAO {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTableA(tableAEntry: TableAEntry) : Long
**// Repo Code (function only)**
suspend fun insertTableA(tableAEntry: TableAEntry): Long {
return WLDAO.insertTableA(TableAEntry)
}
**// View Model Code (function only)**
fun addToTableA(tableAEntry: TableAEntry) = viewModelScope.launch {
repo.insertTableA(TableAEntry)
}
I'd like to retrieve the Auto-Generated Primary Key value (rowId) after creation and insert it into a column in Table B along with more data.
The Long returned by your Room DAO's #Insert function is the row ID associated with that INSERT operation.
For using that value in further data operations, handle that in your repository or viewmodel. For example, your repository could have something like:
suspend fun doStuffWithBothTables(tableAEntry: TableAEntry, stuffForTableB: Whatever): Long {
val rowID = WLDAO.insertTableA(TableAEntry)
// TODO use rowID and stuffForTableB for some other DAO operations
}
Since doStuffWithBothTables() is itself a suspend fun, you can call other suspend functions normally, as if there were no threads or anything involved. So doStuffWithBothTables() can get the row ID from the first insert and then do additional work with that result.
Related
private fun savetosqlite(CoinListesi: List<CoinInfo>){
launch{
val dao = CoinInfoDatabase(getApplication()).CoinDao()
dao.deleteAll()
val uuidList= dao.insertAll(*CoinListesi.toTypedArray())
}
dao is reset but primary key keeps increasing every time the function is called, also primary key doesn't start from 0 how do I solve it?
Dao
#Dao
interface CoinInfoDao {
#Insert
suspend fun insertAll(vararg CoinInfo: CoinInfo):List<Long>
#Query("DELETE FROM CoinInfo")
suspend fun deleteAll() }
model
#Entity
data class CoinInfo (...){
#PrimaryKey(autoGenerate = true)
var uuid:Int=0
}
Because autoGenerate/autoIncreament from the sqlite docs says it is to
prevent the reuse of ROWIDs over the lifetime of the database
As deleting all the rows with "DELETE FROM CoinInfo" does not affect the lifetime of this table in the database then the numbers continue to increase.
You need to end the "life" of the table with the SQL "DROP TABLE CoinInfo" and then re-create it for it to start a new lifetime and reset the auto generated Int.
Or you can directly reset the value of where SQLite stores then last number used with a query like "DELETE FROM sqlite_sequence WHERE name='CoinInfo'" (or set the value to be 0/1 of this row)
You would need to execute something like
CoinInfoDatabase.getDatabase(application)
.getOpenHelper()
.getWritableDatabase()
.execSQL("DELETE FROM sqlite_sequence WHERE name='CoinInfo'");
Or more efficient is do you really need autoincrement? as per the docs
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)
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 return Flow<List<Long>> after Room insert?
Something like this:
#Insert(onConflict = REPLACE)
suspend fun insert( notesModel: List<NotesModel>): Flow<List<Long>>
When I do it I get an exception:
error: Not sure how to handle insert method's return type.
AFAIK, you can't. The return type of #Insert annotated functions could be Unit, Long, List<Long>, or Array<Long>.
Only #Query annotated functions are able to return observable objects like LivaData, Flow, Single, Flowable, etc. Because the #Query functions are designed to be able to reflect changes on db.
Here is a quote from official documentation:
If the #Insert method receives only 1 parameter, it can return a long, which is the new rowId for the inserted item. If the parameter is an array or a collection, it should return long[] or List instead.
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.
}
}
}
}