12

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,

KG6ZVP
  • 3,610
  • 4
  • 26
  • 45
user2234
  • 1,282
  • 1
  • 21
  • 44

1 Answers1

6

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());
        });
    }
}
KG6ZVP
  • 3,610
  • 4
  • 26
  • 45
  • I won't be inserting data into DB unless I process it, so does really using the above mentioned code by you help ? – user2234 Oct 19 '17 at 01:58
  • It's very helpful because db queries are synchronous operations and using LiveData to observe their output avoids a lot of boilerplate code. Your question here appears to be different than the original one asked. Would you edit your question to include detail about the architecture of your data fetching and "processing?" – KG6ZVP Oct 19 '17 at 02:00
  • Thanks for the detailed answer. Appreciate it. But what you explaining is taking action once the data is updated/added/deleted in database when I get onChanged() method event. But what I want to do is, before I insert an new set of data into the DB, I need to fetch old records and process the new data received from server and then update the db. What you explaining me I did understand but that' s not what I want. – user2234 Oct 20 '17 at 05:46
  • Is your question how to retrieve data from a server? – KG6ZVP Oct 20 '17 at 05:48
  • I think there is some confusion, ok let me ask this way. If I have to fetch records from the DB before inserting new ones, Do I have to write another method like: List getAllData() which does not return me Live data and then compare with the new data received from server, and use Server> getAll() only to update my UI ? – user2234 Oct 20 '17 at 05:59
  • Okay, would you update your question with substantial detail about the data which needs to be processed and why and how this fits into your app architecture? If I am understanding you correctly, a lot more information is needed to provide a good answer. – KG6ZVP Oct 20 '17 at 06:08
  • I have update the question with code. Please check. Thanks – user2234 Oct 20 '17 at 06:45
  • Did this answer your question sufficiently? – KG6ZVP Oct 21 '17 at 05:57
  • Thanks alot. Your answer did clear out lot of my doubts in the implementation. I appreciate the detailed explanation of yours. Just one question, this method appDatabase.userDao().userExists(String id), I see that you have called this to check each entry. Doesn't this increase the db operations ? Wouldn't it be better to fetch all and then traverse ? – user2234 Oct 22 '17 at 23:00
  • 2
    An important rule of thumb: always choose shorter, simpler code unless you have to perform that optimization. Maintainability > Efficiency – KG6ZVP Oct 22 '17 at 23:15
  • You should measure the difference in speed between the two versions of the code. If you give the phone to someone else does the slower code bother them? – KG6ZVP Oct 22 '17 at 23:23