I am using Room for the first time. I am having a look at the LiveData concept. I know that we can fetch records from DB into LiveData and haveObservers attached.
#Query("SELECT * FROM users")
<LiveData<List<TCUser>> getAll();
But I am performing sync in the background, where I need to fetch data from server and compare it with the data in RoomDatabase table called "users" and then either insert,update or delete from users table. How can I traverse the LiveData list before taking any action ? As it gives error if I put it in for loop.
OR should I not use LiveData for this scenario ?
I guess I need to call
<LiveData<List<TCUser>> getAll().getValue()
But is it the right thing to do ? Here is some more code to give idea as to what I am trying to do:
List<User>serverUsers: Is the data received from a response from an API
private void updateUsers(List<User> serverUsers) {
List<UserWithShifts> users = appDatabase.userDao().getAllUsers();
HashMap<String, User> ids = new HashMap();
HashMap<String, User> newIds = new HashMap();
if (users != null) {
for (UserWithShifts localUser : users) {
ids.put(localUser.user.getId(), localUser.user);
}
}
for (User serverUser : serverUsers) {
newIds.put(serverUser.getId(), serverUser);
if (!ids.containsKey(serverUser.getId())) {
saveShiftForUser(serverUser);
} else {
User existingUser = ids.get(serverUser.getId());
//If server data is newer than local
if (DateTimeUtils.isLaterThan(serverUser.getUpdatedAt(), existingUser.getUpdatedAt())) {
deleteEventsAndShifts(serverUser.getId());
saveShiftForUser(serverUser);
}
}
}
Where:
#Query("SELECT * FROM users")
List<UserWithShifts> getAllUsers();
Is the first line in updateUsers() the right way to fetch data from DB to process before inserting new ones or should it be instead
<LiveData<List<User>> getAll().getValue()
Thanks,
If I understand your architecture correctly, updateUsers is inside of an AsyncTask or similar.
This is my proposed approach, which involves tweaking your Dao for maximum effectiveness. You wrote a lot of code to make decisions you could ask your database to make.
This is also not tight or efficient code, but I hope it illustrates more effective use of these libraries.
Background thread (IntentService, AsyncTask, etc.):
/*
* assuming this method is executing on a background thread
*/
private void updateUsers(/* from API call */List<User> serverUsers) {
for(User serverUser : serverUsers){
switch(appDatabase.userDao().userExistsSynchronous(serverUser.getId())){
case 0: //doesn't exist
saveShiftForUser(serverUser);
case 1: //does exist
UserWithShifts localUser = appDatabase.userDao().getOldUserSynchronous(serverUser.getId(), serverUser.getUpdatedAt());
if(localUser != null){ //there is a record that's too old
deleteEventsAndShifts(serverUser.getId());
saveShiftForUser(serverUser);
}
default: //something happened, log an error
}
}
}
If running on the UI thread (Activity, Fragment, Service):
/*
* If you receive the IllegalStateException, try this code
*
* NOTE: This code is not well architected. I would recommend refactoring if you need to do this to make things more elegant.
*
* Also, RxJava is better suited to this use case than LiveData, but this may be easier for you to get started with
*/
private void updateUsers(/* from API call */List<User> serverUsers) {
for(User serverUser : serverUsers){
final LiveData<Integer> userExistsLiveData = appDatabase.userDao().userExists(serverUser.getId());
userExistsLiveData.observe(/*activity or fragment*/ context, exists -> {
userExistsLiveData.removeObservers(context); //call this so that this same code block isn't executed again. Remember, observers are fired when the result of the query changes.
switch(exists){
case 0: //doesn't exist
saveShiftForUser(serverUser);
case 1: //does exist
final LiveData<UserWithShifts> localUserLiveData = appDatabase.userDao().getOldUser(serverUser.getId(), serverUser.getUpdatedAt());
localUserLiveData.observe(/*activity or fragment*/ context, localUser -> { //this observer won't be called unless the local data is out of date
localUserLiveData.removeObservers(context); //call this so that this same code block isn't executed again. Remember, observers are fired when the result of the query changes.
deleteEventsAndShifts(serverUser.getId());
saveShiftForUser(serverUser);
});
default: //something happened, log an error
}
});
}
}
You'll want to modify the Dao for whatever approach you decide to use
#Dao
public interface UserDao{
/*
* LiveData should be chosen for most use cases as running on the main thread will result in the error described on the other method
*/
#Query("SELECT * FROM users")
LiveData<List<UserWithShifts>> getAllUsers();
/*
* If you attempt to call this method on the main thread, you will receive the following error:
*
* Caused by: java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long periods of time.
* at android.arch.persistence.room.RoomDatabase.assertNotMainThread(AppDatabase.java:XXX)
* at android.arch.persistence.room.RoomDatabase.query(AppDatabase.java:XXX)
*
*/
#Query("SELECT * FROM users")
List<UserWithShifts> getAllUsersSynchronous();
#Query("SELECT EXISTS (SELECT * FROM users WHERE id = :id)")
LiveData<Integer> userExists(String id);
#Query("SELECT EXISTS (SELECT * FROM users WHERE id = :id)")
Integer userExistsSynchronous(String id);
#Query("SELECT * FROM users WHERE id = :id AND updatedAt < :updatedAt LIMIT 1")
LiveData<UserWithShifts> getOldUser(String id, Long updatedAt);
#Query("SELECT * FROM users WHERE id = :id AND updatedAt < :updatedAt LIMIT 1")
UserWithShifts getOldUserSynchronous(String id, Long updatedAt);
}
Does this solve your problem?
NOTE: I did not see your saveShiftForUser or deleteEventsAndShifts methods. Insert, Save and Update are performed synchronously by Room. If you are running either method on the main thread (I'm guessing this is where your error is coming from), you should create a daoWrapper that is returned from appDatabase like so:
public class UserDaoWrapper {
private final UserDao userDao;
public UserDaoWrapper(UserDao userDao) {
this.userDao = userDao;
}
public LiveData<Long[]> insertAsync(UserWithShifts... users){
final MutableLiveData<Long[]> keys = new MutableLiveData<>();
HandlerThread ht = new HandlerThread("");
ht.start();
Handler h = new Handler(ht.getLooper());
h.post(() -> keys.postValue(userDao.insert(users)));
return keys;
}
public void updateAsync(UserWithShifts...users){
HandlerThread ht = new HandlerThread("");
ht.start();
Handler h = new Handler(ht.getLooper());
h.post(() -> {
userDao.update(users);
});
}
public void deleteAsync(User... users){
HandlerThread ht = new HandlerThread("");
ht.start();
Handler h = new Handler(ht.getLooper());
h.post(() -> {
for(User e : users)
userDao.delete(e.getId());
});
}
}
Related
I am fairly new to Android Room and SQLite in general, so sorry if this is a simple question.
I am getting data from a API that I'd like to insert into a database so it's accessible when the device is offline.
Depending on the endpoint of the API, some fields of my Data objects may be null (Think a summary with just the basic fields versus a fully detailed object with all fields)
To keep the database clean, I'd like to update the entries, but only the columns that are not null (eg. that I have new values for) and keep the rest of the columns untouched.
Here are some example classes to clarify:
Person
#Entity(tableName = "person", indices = {
#Index(value = "id", unique = true)
})
public class Person {
#PrimaryKey
public int id;
public String name;
public String description;
}
Example:
// create db
RoomDB db = RoomDB.create(ctx);
// create some sample objects
final Person p2 = new Person(2, "Peter", null);
// insert them into the db
db.personDao().insert(p2);
// create a updated peter that likes spiders
// but has no name (as a example)
final Person newPeter = new Person(2, null, "Peter likes spiders");
// and update him
db.personDao().updateNonNull(newPeter);
// now we read him back
final Person peter = db.personDao().getById(2);
In this example, the desired values of 'peter' would be:
id = 2
name = "Peter"
description = "Peter likes spiders"
However, using Room's #Update or #Insert i can only get this:
id = 2
name = null
description = "Peter likes spiders"
The only way i found to achive this would be to manuall get the object and supplement the values like so:
#Transaction
public void updateNonNull(Person newPerson) {
final Person oldPerson = getById(newPerson.id);
if (oldPerson == null) {
insert(newPerson);
return;
}
if (newPerson.name == null)
newPerson.name = oldPerson.name;
if (newPerson.description == null)
newPerson.description = oldPerson.description;
update(newPerson);
}
However, that would result in quite a bit of code with bigger objects...
So my question, is there a better way to do this?
Edit:
After some Testing with the SQL by #Priyansh Kedia, i found that those functions indeed work as intended and do so at a higher performance than java.
However, as a SQL statement would have required me to write huge queries, i decided to use a Reflection based solution, as can be seen below.
I only did so because the function isn't called regularly, so the lower performance won't matter too much.
/**
* merge two objects fields using reflection.
* replaces null value fields in newObj with the value of that field in oldObj
* <p>
* assuming the following values:
* oldObj: {name: null, desc: "bar"}
* newObj: {name: "foo", desc: null}
* <p>
* results in the "sum" of both objects: {name: "foo", desc: "bar"}
*
* #param type the type of the two objects to merge
* #param oldObj the old object
* #param newObj the new object. after the function, this is the merged object
* #param <T> the type
* #implNote This function uses reflection, and thus is quite slow.
* The fastest way of doing this would be to use SQLs' ifnull or coalesce (about 35% faster), but that would involve manually writing a expression for EVERY field.
* That is a lot of extra code which i'm not willing to write...
* Besides, as long as this function isn't called too often, it doesn't really matter anyway
*/
public static <T> void merge(#NonNull Class<T> type, #NonNull T oldObj, #NonNull T newObj) {
// loop through each field that is accessible in the target type
for (Field f : type.getFields()) {
// get field modifiers
final int mod = f.getModifiers();
// check this field is not status and not final
if (!Modifier.isStatic(mod)
&& !Modifier.isFinal(mod)) {
// try to merge
// get values of both the old and new object
// if the new object has a null value, set the value of the new object to that of the old object
// otherwise, keep the new value
try {
final Object oldVal = f.get(oldObj);
final Object newVal = f.get(newObj);
if (newVal == null)
f.set(newObj, oldVal);
} catch (IllegalAccessException e) {
Log.e("Tenshi", "IllegalAccess in merge: " + e.toString());
e.printStackTrace();
}
}
}
}
There is no in-built method in room to do this
What you can do is, put check in the query for your update method.
#Query("UPDATE person SET name = (CASE WHEN :name IS NOT NULL THEN :name ELSE name END), description = (CASE WHEN :description IS NOT NULL THEN :description ELSE description END) WHERE id = :id")
Person update(id: Int, name: String, description: String)
We have written the update query for SQL which checks if the inserted values are null or not, and if they are null, then the previous values are retained.
Currently, we have the following database table
#Entity(
tableName = "note"
)
public class Note {
#ColumnInfo(name = "body")
private String body;
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
}
The length of the body string, can be from 0 to a very large number.
In certain circumstance, we need to
Load the all notes into memory.
A LiveData which is able to inform observers, if there's any changes made in the SQLite note table.
We just need the first 256 characters of body. We do not need entire body. Loading entire body string for all notes might cause OutOfMemoryException.
We have the following Room Database Dao
#Dao
public abstract class NoteDao {
#Query("SELECT * FROM note")
public abstract LiveData<List<Note>> getAllNotes();
}
getAllNotes able to fulfill requirements (1) and (2), but not (3).
The following getAllNotesWithShortBody is a failed solution.
#Dao
public abstract class NoteDao {
#Query("SELECT * FROM note")
public abstract LiveData<List<Note>> getAllNotes();
#Query("SELECT * FROM note")
public abstract List<Note> getAllNotesSync();
public LiveData<List<Note>> getAllNotesWithShortBody() {
MutableLiveData<List<Note>> notesLiveData = new MutableLiveData<>();
//
// Problem 1: Still can cause OutOfMemoryException by loading
// List of notes with complete body string.
//
List<Note> notes = getAllNotesSync();
for (Note note : notes) {
String body = note.getBody();
// Extract first 256 characters from body string.
body = body.substring(0, Math.min(body.length(), 256));
note.setBody(body);
}
notesLiveData.postValue(notes);
//
// Problem 2: The returned LiveData unable to inform observers,
// if there's any changes made in the SQLite `note` table.
//
return notesLiveData;
}
}
I was wondering, is there any way to tell Room database Dao: Before returning List of Notes as LiveData, please perform transformation on every Note's body column, by trimming the string to maximum 256 characters?
Examining the source code generated by Room Dao
If we look at the source code generated by Room Dao
#Override
public LiveData<List<Note>> getAllNotes() {
final String _sql = "SELECT * FROM note";
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
...
...
final String _tmpBody;
_tmpBody = _cursor.getString(_cursorIndexOfBody);
_tmpPlainNote.setBody(_tmpBody);
It will be great, if there is a way to supply transformation function during runtime, so that we can have
final String _tmpBody;
_tmpBody = transform_function(_cursor.getString(_cursorIndexOfBody));
_tmpPlainNote.setBody(_tmpBody);
p/s Please do not counter recommend Paging library at this moment, as some of our features require entire List of Notes (with trimmed body String) in memory.
You can use SUBSTR, one of SQLite's built-in functions.
You need a primary key in your #Entity. Assuming that you call it id, you can write a SQL like below.
#Query("SELECT id, SUBSTR(body, 0, 257) AS body FROM note")
public abstract LiveData<List<Note>> getAllNotes();
This will return the body trimmed to 256 chars.
With that being said, you should consider segmenting your rows. If you have too many rows, they will eventually use up your memory at some point. Using Paging is one way to do it. You can also use LIMIT and OFFSET to manually go through segments of rows.
I am trying to test some RxJava code using TestSubsriber. The data is coming from Room ORM as Flowable<Task>.
This is how my DAO class looks right now.
#Dao
public interface TaskDao {
#Insert()
long insertTask(TaskEntity task);
#Delete
int deleteTask(TaskEntity task);
#Query("SELECT * FROM task_table WHERE status = :taskStatus ORDER BY created_at DESC")
Flowable<List<TaskEntity>> getAllTasks(String taskStatus);
#Query("SELECT * FROM task_table WHERE id = :taskId")
Flowable<TaskEntity> getTask(String taskId);
#Update()
int updateTask(TaskEntity updatedTask);
#Query("SELECT COUNT(*) FROM task_table")
int rowCount();
}
I am trying to test getTask() method. Here the exact test method that is failing.
#Test
public void getTask_getSavedTask_returnsTask() {
Long resp = mTaskDao.insertTask(taskEntity);
assertThat(resp, is(1L));
assertThat(mTaskDao.rowCount(), is(1));
Flowable<TaskEntity> response = mTaskDao.getTask(taskEntity.getTaskId());
TestSubscriber<TaskEntity> testSubscriber = new TestSubscriber<>();
response.subscribe(testSubscriber);
testSubscriber.assertNoErrors();
testSubscriber.assertValueCount(1);
}
This code snippet fails at testSubscriber.assertValueCount(1) method.
The model is getting saved in db since the returned value from insert call is > 1. Also the row count is getting increased.
I tried running the same kind of code from outside the test environment (from an activity) and it's working fine there.
Looks like your DAO runs asynchronously and the TestSubscriber doesn't receive an answer immediately. Change the test to this and it should work:
testSubscriber.awaitCount(1, BaseTestConsumer.TestWaitStrategy.SLEEP_1MS, 5000);
testSubscriber.assertNoTimeout();
testSubscriber.assertNoErrors();
testSubscriber.assertValueCount(1);
I use room with Live Data to access external database.
The value stored in external database is updated by another application every 5 seconds.
It seems that I can't constantly receive the updated value from room.
Should I rebuild database to get updated value every 5 seconds?
Here are some reference code:
#Dao public interface GasDynamicDao {
#Query("Select * from gas_dyamic")
LiveData<List<GasDynamicEntity>> getAllInformation();
#Query("Select * from gas_dyamic")
List<GasDynamicEntity> getAllInformationSync();
#Insert(onConflict = OnConflictStrategy.REPLACE)
void insertAll(List<GasDynamicEntity> products);
#Query("Select * from gas_dyamic where Device_ID = :deviceID")
LiveData<GasDynamicEntity> loadGas(String deviceID);
#Query("Select * from gas_dyamic where Device_ID = :deviceID")
GasDynamicEntity loadGasSync(String deviceID);
}
public static GasDatabase buildDatabase(final Context appContext, final AppExecutors executors) {
return Room.databaseBuilder(appContext, GasDatabase.class,
DATABASE_FULL_PATH).allowMainThreadQueries().build();
}
I solve the problem by polling database.
But database needs not to be recreated or reloaded from room.
LiveData will change again by re-querying database through Dao methods.
My app uses a database with SongVersions having many Tracks. This to-manx relationship is generated by greendao and works fine so far.
But now I try to update a track - let's say to mute it - and the update is only wokring until the next app start. This is, because the udpate is only reflected in the cached list of tracks, but not really persisted to the database.
Here is my code so far (not working)
// iterate through tracks
for (Track track : mSongVersion.getTrackList()) {
if (trackId.equals(track.getId())) {
// mute specific track
track.setMuted(muted);
mSongVersion.update();
}
}
SongVersion and Track are the entitiy classes genertated by greendao. SongVersion has an update()-method, but Track has no update()-method. So I was thinking that one has to update a specific track by updating "the whole thing" using SongVersion#update().
But this call only updates the SongVersion, ignoring changes of its tracks...
I also tried some variations of (not) resetting the tracklist to make sure that (no) cached values are interfering, but also to no avail.
EDIT:
Here's some code that might help.
SongVersion (generated by greendao!):
/** Used for active entity operations. */
private transient SongVersionDao myDao;
/** called by internal mechanisms, do not call yourself. */
public void __setDaoSession(DaoSession daoSession) {
this.daoSession = daoSession;
myDao = daoSession != null ? daoSession.getSongVersionDao() : null;
}
public void update() {
if (myDao == null) {
throw new DaoException("Entity is detached from DAO context");
}
myDao.update(this);
}
Parts of the Database generation java:
public static void main(String[] args) throws Exception {
Schema schema = new Schema(1, "my.app.database");
Entity songVersion = schema.addEntity("SongVersion");
Entity track = schema.addEntity("Track");
// SongVersion fields w/o relations
songVersion.setHasKeepSections(true);
songVersion.addIdProperty().autoincrement();
songVersion.addStringProperty("name").notNull();
// Track fields w/o relations
track.setHasKeepSections(true);
track.implementsInterface("Comparable");
track.addIdProperty().autoincrement();
track.addBooleanProperty("muted").notNull();
// relations
Property songVersionId =
track.addLongProperty("songVersionId").notNull().getProperty();
songVersion.addToMany(track, songVersionId);
}
I think that you should use the TrackDAO to update the track after modifying the list instance. Something like this:
daoMaster = new DaoMaster(db);
daoSession = daoMaster.newSession();
trackDao = daoSession.getTrackDao();
// iterate through tracks
for (Track track : mSongVersion.getTrackList()) {
if (trackId.equals(track.getId())) {
// mute specific track
track.setMuted(muted);
trackDao.update(track);
}
}