I'm using room in my new Android app, and I'm trying to update an a model object's property at runtime, but it doesn't seem to get saved.
Entity
#Entity(tableName = "sessions")
data class Session(#ColumnInfo(name="id") #PrimaryKey(autoGenerate = true) var id: Int = 0,
#ColumnInfo(name="date") var date: Date,
#ColumnInfo(name="repetitions_completed") var repetitionsCompleted: Int,
#ColumnInfo(name="squeeze_time_per_rep") var squeezeTimePerRep: Int,
#ColumnInfo(name="finishing_repetitions_completed") var finishingRepetitionsCompleted: Int,
#ColumnInfo(name="finishing_squeeze_time_per_rep") var finishingSqueezeTimePerRep: Int)
In my app I'm using it like this
session = Session(0,date,0,slowSeconds,0,quickSeconds)
sessionDao.insertAll(sessionDB)
...
session.repetitionsCompleted = totalSlowReps
session.finishingRepetitionsCompleted = totalQuickReps
sessionDao.updateSessions(session)
The problem is that session's property values like repetitionsCompleted and finishingRepetitionsCompleted doesn't seem to be saved, and it always remain 0 when I restart the app. updateSessions also returns 0
Here is the code for the Dao
#Dao
interface SessionDao {
// https://developer.android.com/training/data-storage/room/accessing-data#query-params
#Query("SELECT * FROM sessions")
fun getAll(): List<Session>
#Query("SELECT * FROM sessions WHERE date BETWEEN :from AND :to")
fun getSessionsBetween(from: Date, to: Date): List<Session>
#Query("SELECT * FROM sessions ORDER BY date LIMIT 1")
fun getOldestSession(): List<Session>
#Update
fun updateSessions(vararg sessions: Session) : Int
#Insert
fun insertAll(vararg sessions: Session)
}
What's weirder is that update seems to work in my unit test, but not in the actual app code.
I don't know if this affects things, but I'm running this in a service.
I figured it out, the problem was that when I created the session object, object is returned with an id set to 0 rather than its autoGenerate primary key, so what I did was alter the insert to return it's inserted id
#Insert
fun insertAll(vararg sessions: Session) : List<Long>
When I create my session, I manually reassign its ID
sessionDB = Session(0,date,0,slowSeconds,0,quickSeconds)
val insertID = sessionDao.insertAll(sessionDB).first().toInt()
sessionDB.id = insertID
maybe it sounds a little stupid but it fixed my issue. I guess all of you are familiar with the OnConflictStrategy. it's making sense to set strategy for insert. but google need this strategy for #Update as well as the default strategy is ABORT.
#return How to handle conflicts. Defaults to {#link OnConflictStrategy#ABORT}.
so the solution would be.
#Update(onConflict = OnConflictStrategy.REPLACE)
Maybe you're not sending the class id.
the value #PrimaryKey of the #Entity Class
Related
I'm kind of new to Android development, but I'm taking over some project somebody more experimented than me did.
I'm making an application that retrieves data from the internet at startup and creates (or updates) a room database to store the information.
So far, it works. However, I noticed that when I push an update of the application, for some users, the database doesn't update anymore. They can reinstall the app, it doesn't change anything. The only thing that works is to clear the cache and restart the application. Then everything goes back to normal, when data are retrieved from the internet, they are properly inserted in the database. But the problem comes back with the next update.
I added the 'fallbackToDestructiveMigration()' option but it doesn't help, because it's not a migration of the database per se, as the structure doesn't change here.
Ideally, I'd like to preserve the data already present in the database.
I'm using Room 2.2.5 and API 28.
I'm not sure why updating the app results in the database not updating anymore. Maybe the new version of the app creates another database and populates this one, but the app is still using the old one (not updated anymore).
The Storage Module:
val storageModule = module {
single {
Room.databaseBuilder(androidContext(), LessonDatabase::class.java, "MyDB")
.fallbackToDestructiveMigration().build()
}
single {
get<LessonDatabase>().lessonDao()
}
}
The LessonDatase:
#Database(entities = [Lesson::class], version = BuildConfig.DATABASE_VERSION, exportSchema = false)
abstract class LessonDatabase : RoomDatabase() {
abstract fun lessonDao(): LessonDao
}
The LessonDao:
#Dao
interface LessonDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertLesson(lesson: Lesson)
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertLessons(lessons: List<Lesson>)
#Update
fun updateLesson(lesson: Lesson)
#Delete
fun deleteLesson(lesson: Lesson)
#Query("DELETE FROM Lesson")
fun clearLessons()
#Query("SELECT * FROM Lesson WHERE id == :id")
fun getLessonById(id: String): Lesson
#Query("SELECT * FROM Lesson ORDER BY creation_date DESC")
fun getLessons(): List<Lesson>
#Query("SELECT * FROM Lesson WHERE favorite = 1")
fun getFavoriteLessons(): List<Lesson>
#Query("SELECT * FROM Lesson WHERE difficulty LIKE :level")
fun getLessonsByDifficulty(level: Int): List<Lesson>
}
And the code for the application startup:
class SplashscreenViewModel(
private val repository: LessonRepository,
private val lessonDao: LessonDao,
val context: Context
) : BaseViewModel() {
val nextScreenLiveData = MutableLiveData<Boolean>()
override fun start() {
ioScope.launch {
val lessons = repository.getLessons().filter {
it.site.contains("website")
}.filter {
DataUtils.isANumber(it.id)
}
lessonDao.insertLessons(lessons)
nextScreenLiveData.postValue(true)
}
}
override fun stop() {
}
}
A question I have is, if I update the application, I guess Room.databaseBuilder will be called again. But what happens if the name of the database is the same as the previous one? Will it retrieve the old one, or create a new one? Overwrite the old one?
Another question I have, in the Insert query, it says onConflictStrategy.IGNORE. But as I pass a list as parameters, what happens if some of the entries are already in the database and some not? Will it ignore all of them? Just the already existing ones?
Thank you.
Edit: I installed Android-Debug-Database (https://github.com/amitshekhariitbhu/Android-Debug-Database) and it seems the database is fine actually. The only problem is that when I update the app, the new entries I insert are returned at the end of the SELECT * FROM table query. So I tried to sort them by Id, and it seems to work.
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 have a data class like this
#Entity
data class Question(
#field:SerializedName("question")
var question: String? = null,
#field:SerializedName("answers")
var answers: ArrayList<String?>? = null,
#field:SerializedName("id")
#PrimaryKey
var id: Int? = null
)
Then in DAO I have saving and getting methods like this
#Dao
interface QnADao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun saveQuestion(questions:Question)
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun saveAllQuestions(questions: List<Question?>?)
#Query("SELECT * from Question")
fun getAllQnA():List<Question>
}
I am saving a list of Questions and then later on retrieving them. So whenever I retrieve them I get the list sorted according to the id which is the primary key.
So if I am saving questions with id:254, id:23, id:45 and id:92 then I am getting it like this id:23, id:45, id:92 and id:254
But I don't need a sorted list like that, I need to get the data as it was saved in the database. Any help would be appreciated. Thanks.
Try to use autoGenerate = true for primary key so it will create PK number in sequence
See below lines
#PrimaryKey(autoGenerate = true)
So that now your insert and retrive order will be same
You can add a Date field to your Question entity
#field:SerializedName("date")
var date: Date? = null,
and order your entities by date
#Query("SELECT * FROM Question ORDER BY date DESC")
I've an entity called Events which is defined as follows:
#Entity(tableName = "Events")
data class Event(#PrimaryKey val id: Long, val name: String, val venues: Set<String>, val rating: Int)
I've a pair of #TypeConverter methods for handling Set<String>:
#TypeConverter
fun fromStringToStringSet(str: String): Set<String> = str.split("<|>")
#TypeConverter
fun fromStringSetToString(set: Set<String>): String = set.joinToString("<|>")
In my Dao, I've a method annotated with #Query as follows:
#Query("UPDATE Events SET name = :name, venues = :venues WHERE id = :id")
fun updateAndRetainRating(id: Long, name: String, venues: Set<String>)
When I try to update an event with 2 venues, I get a runtime error telling me that the SQL couldn't be compiled. The generated SQL is:
UPDATE Events SET name = ?, venues = ?,? WHERE id = ?
This is obviously wrong. Looking into the generated code, Room is getting the size of Set<String> and adding the same number of ?s.
Why isn't my TypeConverter being used? I don't face this issue in other queries(#Insert and #Query(for SELECT)). Other TypeConverters are also working fine.
EDIT: The same issue also occurs if I use List<String> + TypeConverters instead of Set<String>.
Looking into the Room documentation, it appears that whenever we use a Collection in #Query, it bypasses TypeConverters and is straight away flattened.
For example, this:
#Query("SELECT * FROM Customers WHERE city IN (:cities)")
fun getCustomersInCities(cities: List<String>): Flowable<List<Customer>>
results in SELECT * FROM Customers WHERE city IN ('london', 'paris') if cities contains "london" and "paris".
Unfortunately, the following is also converted in a similar way:
#Query("UPDATE Customers SET phoneNumbers = :phoneNumbers WHERE id = :id")
fun updateCustomerPhoneNumbers(id: Long, phoneNumbers: List<String>)
The result is UPDATE Customers SET phoneNumbers = 1234567890, 9876543210 WHERE id = 23 if id is 23 and the phoneNumbers contains "1234567890" and "9876543210".
While this does make sense, it is really inconvenient and should be documented more clearly.
This solution worked for me:
TypeConverter not working when updating List<Boolean> in Room Database
Basically, replace List<String> by ArrayList<String>.
After shifting from SqliteOpenHelper to room in my app, I've trying to write tests for the DAO class.
My DAO looks something like this:
#Query("SELECT * FROM cards")
fun getAllCards(): List<CardData>
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertCard(vararg cardData: CardData): List<Long>
#Query("SELECT * FROM cards ORDER BY isRead ASC, id DESC")
fun getItemList(): DataSource.Factory<Int, CardData>
#Query("SELECT * FROM cards where instr(title, :query) > 0 ORDER BY isRead ASC, id DESC")
fun getItemList(query: String): DataSource.Factory<Int, CardData>
#Query("UPDATE cards set isRead = 1 where title = :title")
fun markRead(title: String): Int
While writing test for getAllCards, insertCard and markRead is trivial, I am still not sure how do I test the apis which return DataSource.Factory, i.e getItemList apis.
After searching on internet, I couldn't find anything related to this.
Can someone please help.
this is how I did:
val factory = dao.getItemList()
val list = (factory.create() as LimitOffsetDataSource).loadRange(0, 10)
Quoting CommonsWare
If you use paging with Room and have a #Dao method return a DataSource.Factory, the generated code uses an internal class named LimitOffsetDataSource to perform the SQLite operations and fulfill the PositionalDataSource contract.
source: https://commonsware.com/AndroidArch/previews/paging-beyond-room#head206