Transaction with room to update multiple rows doesn't update - kotlin

I'm facing an issue with Room on Android.
I have a DAO with multiple methods
#Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insertEntity(entity: Entity)
#Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun updateEntity(entity: Entity)
#Transaction
suspend fun insertOrUpdateEntities(entities: List<Entity>) {
entities.forEach { entity ->
try {
insertEntity(entity)
} catch (sqlException: SQLiteConstraintException) {
updateEntity(entity)
}
}
(I simplify the catch part, I change the object entity to do some merge of fields)
But, while the entity object in "updateEntity" is correct, the database is not update with the new values.
Do you have an idea why?
Thanks!

I would suggest not raising an ABORT but instead using
#Insert
suspend fun insertEntity(entity: Entity): Long
#Update
suspend fun updateEntity(entity: Entity): Int
#Transaction
suspend fun insertOrUpdateEntities(entities: List<Entity>) {
entities.forEach { entity ->
if (insertEntity(entity) < 1) {
updateEntity(entity)
}
}
}
by default #Insert has onConflictStrategy.IGNORE, so PRIMARY KEY, UNIQUE, NULL and CHECK conflicts will be ignored.
the #Insert method can return a long (the rowid of the inserted row, or -1 if the row was not inserted), so obtaining that can be used to determine whether or not the row was inserted.

Related

Room + Kotlin Flow not emitting result

i'm trying to fetch some data from api, and them store on room database, so the main data source is roomDatabase.
my repository code looks like:
suspend fun fetchData(): Flow<Response<List<Foo>>> {
val shouldRequestData = dao.getFoo().isEmpty()
return if (shouldRequestData) {
getFoo()
} else getLocalFoo()
}
override suspend fun getFoo(): Flow<Response<List<Foo>>> {
return ....... request done normally... inserting normally on database (and showing
on database inspector)
}
override suspend fun getLocalFoo(): Flow<Response<List<Foo>>> = flow {
dao.getFoo().transform<List<FooLocal>, Response<List<Foo>>> {
Response.Success(
it.map {
it.toDomainModel()
}
)
}
}
on Dao:
#Query("SELECT * FROM localdb")
fun getFoo(): Flow<List<Foo>>
and then collecting it normally on viewmodel...
The problem is: the data is not appearing.. how could i solve this? The non-flow version works :/
I already searched for this problem, but nothing seems to work.
Solved by putting this on getLocalFoo() ->
val result: Flow<Response<List<Foo>>> =
Transformations.map(dao.getFoo()) {
Response.Success(it?.map {
it.asDomainModel()
} ?: emptyList()
}.asFlow()
return result
I have found a solution to investing so much time.
Solution: Same Dao Object should be used when we insert details into the room database and get information from DB.
If you are using dagger hilt then
#Singleton annotation will work.
I hope this will solve your problem.

How to wait for a flow to complete emitting the values

I have a function "getUser" in my Repository which emits an object representing a user based on the provided id.
flow function
fun getUser(id: String) = callbackFlow {
val collectionReference: CollectionReference =
FirebaseFirestore.getInstance().collection(COLLECTION_USERS)
val query: Query = collectionReference.whereEqualTo(ID, id)
query.get().addOnSuccessListener {
val lst = it.toObjects(User::class.java)
if (lst.isEmpty())
offer(null)
else
offer(it.toObjects(User::class.java)[0])
}
awaitClose()
}
I need these values in another class. I loop over a list of ids and I add the collected user to a new list. How can I wait for the list to be completed when I collect the values, before calling return?
collector function
private fun computeAttendeesList(reminder: Reminder): ArrayList<User> {
val attendeesList = arrayListOf<User>()
for (friend in reminder.usersToShare) {
repoScope.launch {
Repository.getUser(friend).collect {
it?.let { user ->
if (!attendeesList.contains(user))
attendeesList.add(user)
}
}
}
}
return attendeesList
}
I do not want to use live data since this is not a UI-related class.
There are multiple problems to address in this code:
getUser() is meant to return a single User, but it currently returns a Flow<User>
which will never end, and never return more than one user.
the way the list of users is constructed from multiple concurrent query is not thread safe (because multiple launches are executed on the multi-threaded IO dispatcher, and they all update the same unsafe list directly)
the actual use case is to get a list of users from Firebase, but many queries for a single ID are used instead of a single query
Solution to #1
Let's tackle #1 first. Here is a version of getUser() that suspends for a single User instead of returning a Flow:
suspend fun getUser(id: String): User {
val collectionReference = FirebaseFirestore.getInstance().collection(COLLECTION_USERS)
val query = collectionReference.whereEqualTo(ID, id)
return query.get().await().let { it.toObjects(User::class.java) }.firstOrNull()
}
// use the kotlinx-coroutines-play-services library instead
private suspend fun <T> Task<T>.await(): T {
return suspendCancellableCoroutine { cont ->
addOnCompleteListener {
val e = exception
if (e == null) {
#Suppress("UNCHECKED_CAST")
if (isCanceled) cont.cancel() else cont.resume(result as T)
} else {
cont.resumeWithException(e)
}
}
}
}
It turns out that this await() function was already written (in a better way) and it's available in the kotlinx-coroutines-play-services library, so you don't need to actually write it yourself.
Solution to #2
If we could not rewrite the whole thing according to #3, we could deal with problem #2 this way:
private suspend fun computeAttendeesList(reminder: Reminder): List<User> {
return reminder.usersToShare
.map { friendId ->
repoScope.async { Repository.getUser(friendId) }
}
.map { it.await() }
.toList()
}
Solution to #3
Instead, we could directly query Firebase for the whole list:
suspend fun getUsers(ids: List<String>): List<User> {
val collectionReference = FirebaseFirestore.getInstance().collection(COLLECTION_USERS)
val query = collectionReference.whereIn(ID, ids)
return query.get().await().let { it.toObjects(User::class.java) }
}
And then consume it in a very basic way:
private suspend fun computeAttendeesList(reminder: Reminder): List<User> {
return Repository.getUsers(reminder.usersToShare)
}
Alternatively, you could make this function blocking (remove suspend) and wrap your call in runBlocking (if you really need to block the current thread).
Note that this solution didn't enforce any dispatcher, so if you want a particular scope or dispatcher, you can wrap one of the suspend function calls with withContext.

Room query with id doesn't return the right list RxJava MVVM architecture

I have a problem concerning my query returns, I have a student class that contains a string id from another table
data class StudentEntity(
#PrimaryKey
val idStudent: String,
val classId: String,
val name: String,
val notes: Note?,
)
I also created a room database that I'm populating from my api call
#Database(entities = [Student::class, Note::class], version = 14, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun programDAO(): ProgramDAO
companion object{
#Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context) : AppDatabase {
synchronized(this) {
var instance = INSTANCE
if (instance == null) {
instance = Room.databaseBuilder(
context.applicationContext, AppDatabase::class.java, "student-database"
).fallbackToDestructiveMigration().build()
INSTANCE = instance
}
return instance
}
}
}
}
and for that, I have a programDao that helps me to run my queries
#Dao
interface ProgramDAO {
#Transaction
#Query("select * from studentEntity")
fun getStudents(): Single<List<StudentEntity>>
#Transaction
#Query("select * from studentEntity where classId = :classid")
fun getStudentsWithId(classid: String): Single<List<StudentEntity>>
}
In order to execute those queries, I have my Repository:
class ProgramRepository(val api: ApiService, val programDAO: ProgramDAO) {
fun getStudentsFromDbWithId(idClass: String) : Observable<StudentEntity>{
return programDAO.getStudentsWithId(idClass).toObservable()
}
fun getStudentsFromDb() : Observable<StudentEntity>{
return programDAO.getStudents().toObservable()
}
}
The MV allows me to connect the data and the view:
class ProgramListViewModel(private val programRepository: ProgramRepository) {
fun getListFromDBWithId(classID: String): Observable<List<StudentEntity>> {
return programRepository.getStudentsFromDbWithId(deliverySiteId)
}
fun getListFromDB(): Observable<List<StudentEntity>> {
return programRepository.getStudentsFromDb()
}
}
So in order to use the data and get the adapter and the KPIs on my fragment, I don't receive the right list from the database, I did log the results to see what I get, to start, I do log the whole list without id, and I have the list, but when I use an ID like:
subscribe(programListViewModel.getListFromDBWithId("111").subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
Log.e("list of students", it.toString())
})
I got an empty list, so I thought the problem was from the id, I tried to use the id that has been logged from the whole list and didn't work, I also used S to launch my sql query with the same id and I got the results.
Any help please?
Thanks
Is your AircraftEntity supposed to be StudentEntity? If yes (and I think it is, according to the rest of your code examples), then please update your question.
By default, Room uses the class name as the database table name, and I think it's case-sensitive, so your queries should have been "select * from StudentEntity". The better approach would be giving it a name explicitly:
#Entity(tableName = "students")
data class StudentEntity (
// ...
)
Then, your ProgramDAO would look like follow:
#Dao
interface ProgramDAO {
#Query("select * from students")
fun getStudents(): Single<List<StudentEntity>>
#Query("select * from students where classId = :classid")
fun getStudentsWithId(classid: String): Single<List<StudentEntity>>
}
You said: ... a room database that I'm populating from my api call
Where and how do you populate your database? I don't see any #Insert functions in your DAO, unless you just left them out from your code snippet. If your room DB is not populated, of course you will get no data.

Kotlin Spring JPA Reactive overcoming the default exception

I had a problem which was,
When have a unique constraint in the DB query,
and then want to insert it again,
I wanted to override the default exception.
like when having a DB table like this
CREATE TABLE IF NOT EXISTS $ENROLLED_USERS_TABLE (
id INT AUTO_INCREMENT PRIMARY KEY,
$USERS_FIELD VARCHAR(250) NOT NULL UNIQUE
);
and the repo like this
#Repository
interface UserRepository: ReactiveCrudRepository<User, Int>{
#Modifying
#Query("insert into $ENROLLED_USERS_TABLE ($USERS_FIELD) values (:user)")
fun insertUser(#Param("user") user: String?): Mono<Void?>?
}
this is my controller
#PostMapping("/{user}")
suspend fun add(#PathVariable user:String): Void? = userRepository.insertUser(user)?.awaitFirstOrNull()
I have tried to put them in try and catch but it didn't work
to catch the
#Service
class UserService(val userRepository: UserRepository) {
suspend fun add(user:String) {
try {
// some code
userRepository.insertUser(user)?.onErrorReturn(throw ResourceAlreadyExists("User Already exists"))
}
catch (e: SQLException) {
println("Custome Message")
}
}
}
nothing worked I still get the exception that shows my database query :/
the solution was simple
#PostMapping("/{user}")
suspend fun add(#PathVariable user:String): Void? = userRepository.insertUser(user)?.onErrorMap { throw ResourceAlreadyExists("User Already Exists") }?.awaitFirstOrNull()
mapping the error was the right one

I am working on RoomDatabase using kotlin, I want to delete the selected row in room database

I am working on RoomDatabase, I want to delete the selected row in room database.
#Dao
interface DataDao {
#Query("SELECT * from marksheet_table")
fun getAlphabetizedWords(): LiveData<List<DataEntity>>
#Insert
fun insert(dataentity: DataEntity)
#Delete
fun deleteItem(dataentity: DataEntity)
#Query("DELETE FROM marksheet_table")
fun deleteAll()
}
Try:
#Query("DELETE FROM marksheet_table WHERE ID = :id") fun deleteById(id:Int)