Android: How to synchronize queries with Bolts from Parse.com?
Asked Answered
S

3

9

I am using Parse.com as a backend for my app. They also offer a local database to store information, as an alternative to SQLite.

I want to add numbers from phone to my database with parse. Before adding a number I need to check if the number already exists in the database, so I use findInBackground() to get a list of numbers that match the number I want to add. If the list is empty the number I want to add doesn't exists in the database.

The method to do this is:

public void putPerson(final String name, final String phoneNumber, final boolean isFav) {

        // Verify if there is any person with the same phone number
        ParseQuery<ParseObject> query = ParseQuery.getQuery(ParseClass.PERSON_CLASS);
        query.whereEqualTo(ParseKey.PERSON_PHONE_NUMBER_KEY, phoneNumber);
        query.fromLocalDatastore();
        query.findInBackground(new FindCallback<ParseObject>() {
                                   public void done(List<ParseObject> personList,
                                                    ParseException e) {
                                       if (e == null) {
                                           if (personList.isEmpty()) {
                                               // If there is not any person with the same phone number add person
                                               ParseObject person = new ParseObject(ParseClass.PERSON_CLASS);
                                               person.put(ParseKey.PERSON_NAME_KEY, name);
                                               person.put(ParseKey.PERSON_PHONE_NUMBER_KEY, phoneNumber);
                                               person.put(ParseKey.PERSON_FAVORITE_KEY, isFav);
                                               person.pinInBackground();

                                               Log.d(TAG,"Person:"+phoneNumber+" was added.");
                                           } else {
                                               Log.d(TAG, "Warning: " + "Person with the number " + phoneNumber + " already exists.");
                                           }
                                       } else {
                                           Log.d(TAG, "Error: " + e.getMessage());
                                       }
                                   }
                               }
        );
    }

Then I call this method 3 times to add 3 numbers:

ParseLocalDataStore.getInstance().putPerson("Jack", "0741234567", false);
ParseLocalDataStore.getInstance().putPerson("John", "0747654321", false);
ParseLocalDataStore.getInstance().putPerson("Jack", "0741234567", false);
ParseLocalDataStore.getInstance().getPerson(); // Get all persons from database

Notice that the third number is the same as the first, and it shouldn't be added to database. But the logcat shows:

12-26 15:37:55.424 16408-16408/D/MGParseLocalDataStore: Person:0741234567 was added.
12-26 15:37:55.424 16408-16408/D/MGParseLocalDataStore: Person:0747654321 was added.
12-26 15:37:55.484 16408-16408/D/MGParseLocalDataStore: Person:0741234567 was added.

The third number was added even if it wasn't supposed to do this, because fintInBackground() is running in 3 background threads almost simultaneously, so it will find that there is no number in the database like the one I want to add.

In this question a guy told me that I should use Bolts library from Parse. I read about it from here and some Parse blog posts, but I don't fully understand how to use this with the method I already have, and how to syncronize the queries to be executed one after another.

If someone worked with this library please guide me on how to do this or provide some basic examples so I can understand the workflow.

Thanks!

Skewness answered 28/12, 2015 at 11:28 Comment(3)
if you add callBack method into your pinInBackground() you will solve the dublicated row problem.Coraciiform
@SedatPolat How? The problem is not with pinInBackground(). The problem is that all 3 queries are precessed almost simultaneously when findInBackgroud() is called. I want to process this queries one after another.Skewness
If you add CallBack to pinInBackground() as on my answer, Your save operations will wait each other.Coraciiform
L
1

It sounds like you have a race condition. There are lots of different ways you could go about solving this problem. Here is a non bolts alternative.

The primary problem is that the search queries are happening at roughly the same time so they don't find duplicates in the database. In this case the way we solve it is to only search and add one person at a time, obviously not on the main thread as we don't want to tie up the ui doing searches. So lets make a List that does two things 1) contains items that need to be added, 2)verifies that they can be added. We can use async task to keep our search off of the mainthread.

Here is a rough idea of that needs to be done:

import com.parse.ParseException;
import com.parse.ParseObject;
import com.parse.ParseQuery;

import java.util.ArrayList;
import java.util.List;

public class AddPersonAsyncQueue {

    ArrayList<ParseObject> mPeople = new ArrayList();
    Boolean mLock;
    AddTask mRunningTask;

    public synchronized void addPerson(final String name, final String phoneNumber, final boolean isFav) {

        // we aren't adding a person just yet simply creating the object
        // and keeping track that we should do the search then add for this person if they aren't found
        ParseObject person = new ParseObject(ParseClass.PERSON_CLASS);
        person.put(ParseKey.PERSON_NAME_KEY, name);
        person.put(ParseKey.PERSON_PHONE_NUMBER_KEY, phoneNumber);
        person.put(ParseKey.PERSON_FAVORITE_KEY, isFav);

        synchronized (mLock) {
            mPeople.add(person);
        }
        processQueue();
    }

    public boolean processQueue() {
        boolean running = false;

        synchronized (mLock) {
            if (mRunningTask == null) {
                if (mPeople.size() > 0) {

                    mRunningTask = new AddTask(null);
                    mRunningTask.execute();
                    running = true;
                } else {
                    // queue is empty no need waste starting an async task
                    running = false;
                }
            } else {
                // async task is already running since it isn't null
                running = false;
            }
        }
        return running;
    }

    protected void onProcessQueueCompleted() {
        mRunningTask = null;
    }

    private class AddTask extends AsyncTask<Void, ParseObject, Boolean> {
        AddPersonAsyncQueue mAddPersonAsyncQueue;

        public AddTask(AddPersonAsyncQueue queue) {
            mAddPersonAsyncQueue = queue;
        }

        @Override
        protected Boolean doInBackground(Void... voids) {
            boolean errors = false;
            ParseObject nextObject = null;

            while (!isCancelled()) {

                synchronized (mLock) {
                    if (mPeople.size() == 0) {
                        break;
                    } else {
                        // always take the oldest item fifo
                        nextObject = mPeople.remove(0);
                    }
                }

                if (alreadyHasPhoneNumber(nextObject.getInt(ParseKey.PERSON_PHONE_NUMBER_KEY))) {
                    // do nothing as we don't want to add a duplicate
                    errors = true;
                } else if (addPerson(nextObject)) {
                    // nice we were able to add successfully

                } else {
                    // we weren't able to add the person object we had an error
                    errors = true;
                }

            }
            return errors;
        }

        private boolean alreadyHasPhoneNumber(int phoneNumber) {
            try {
                ParseQuery<ParseObject> query = ParseQuery.getQuery(ParseClass.PERSON_CLASS);
                query.whereEqualTo(ParseKey.PERSON_PHONE_NUMBER_KEY, phoneNumber);
                query.fromLocalDatastore();
                List<ParseObject> objects = query.find();
                return objects.size() > 0;
            } catch (Exception error) {
                // may need different logic here to do in the even of an error
                return true;
            }
        }

        private boolean addPerson(ParseObject person) {
            try {
                // now we finally add the person
                person.pin();
                return true;
            } catch (ParseException e) {
                e.printStackTrace();
                return false;
            }
        }

        @Override
        protected void onPostExecute(Boolean aBoolean) {
            super.onPostExecute(aBoolean);
            onProcessQueueCompleted();
        }
    }


}
Lyte answered 5/1, 2016 at 13:30 Comment(0)
C
0

You should remove duplicate records before save them into the database. Using HashSet and creating a person object with overriding its equals() and hashCode() methods will solve the duplicate records problem. Phone number is unique value, so if phone numbers are equals with others we should save only one of them. This means you should override equals() and hashCode() methods of Person object by using only phone field.

public class Person {
    private String name;
    private String phone;
    private boolean isFav;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public boolean isFav() {
        return isFav;
    }

    public void setFav(boolean isFav) {
        this.isFav = isFav;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((phone == null) ? 0 : phone.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Person other = (Person) obj;
        if (phone == null) {
            if (other.phone != null)
                return false;
        } else if (!phone.equals(other.phone))
            return false;
        return true;
    }

    @Override
    public String toString() {
        return "Person [name=" + name + ", phone=" + phone + ", isFav=" + isFav + "]";
    }
}

Testing:

public static void main(String[] args) {

    HashSet<Person> persons = new HashSet<Person>();

    Person person;

    person = new Person();
    person.setName("Joe");
    person.setPhone("+199999");
    person.setFav(false);
    persons.add(person);

    person = new Person();
    person.setName("Jessie");
    person.setPhone("+133333");
    person.setFav(false);
    persons.add(person);

    person = new Person();
    person.setName("Johnny");
    person.setPhone("+199999");
    person.setFav(false);
    persons.add(person);

    System.out.println(persons);
}

Prints:

[Person [name=Joe, phone=+199999, isFav=false], Person [name=Jessie, phone=+133333, isFav=false]]

HashSet has Person objects which have unique phone numbers. Johnny has same phone number with Joe, so HashSet rejects to add Johnny into the list.

Also adding CallBack method on putPerson() will help you about failed pinning operation.

person.pinInBackground( new SaveCallback() {

    @Override
    public void done( ParseException e ) {
        if( e == null ) {
            pinnedPersonList.add(person)
        } else {
            unPinnedPersonList.add(person)
        }
    }
} );
Coraciiform answered 30/12, 2015 at 13:20 Comment(5)
I tested it but the result is the same. The third is still added. It was expected to be like that because findInBackground() is the method that search if the number exists. It has nothing to do with pinInBackGround(). I need to make findInBackground() methods to wait each other.Skewness
instead of "findInBackground()", you may use "query.find()"Coraciiform
Yes query.find() works but it takes between 1.5 and 3 seconds to add 100 numbers(in case my database is empty at first run, otherwise it could take longer). The ideal solution would be to use findInBackground() that doesn't do all the job in UI thread, and synchronize the queries in that separate thread. Also I would like to learn how to use Bolts library, in case I will need it for another problem.Skewness
In case I can't find a better solution do you think that using query.find() in a separate thread is ok?Skewness
You can start a background thread which puts a list of persons. This will help you.Coraciiform
L
0

This is from Bolts documentation.

Tasks in Series

Tasks are convenient when you want to do a series of tasks in a row, each one waiting for the previous to finish. For example, imagine you want to delete all of the comments on your blog.

ParseQuery<ParseObject> query = ParseQuery.getQuery("Comments");
query.whereEqualTo("post", 123);

findAsync(query).continueWithTask(new Continuation<List<ParseObject>, Task<Void>>() {
  public Task<Void> then(Task<List<ParseObject>> results) throws Exception {
    // Create a trivial completed task as a base case.
    Task<Void> task = Task.forResult(null);
    for (final ParseObject result : results) {
      // For each item, extend the task with a function to delete the item.
      task = task.continueWithTask(new Continuation<Void, Task<Void>>() {
        public Task<Void> then(Task<Void> ignored) throws Exception {
          // Return a task that will be marked as completed when the delete is finished.
          return deleteAsync(result);
        }
      });
    }
    return task;
  }
}).continueWith(new Continuation<Void, Void>() {
  public Void then(Task<Void> ignored) throws Exception {
    // Every comment was deleted.
    return null;
  }
});

This is how its done. If you want to learn what is going on exactly you should read the documentation even though it might look pretty straightforward :)

https://github.com/BoltsFramework/Bolts-Android

Lavinialavinie answered 4/1, 2016 at 21:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.