11

I have a collection of users and I want to query all users from the database and display them in a RecyclerView except one, mine. This is my db schema:

users [collection]
  - uid [document]
     - uid: "fR5bih7SysccRu2Gu9990TeSSyg2"
     - username: "John"
     - age: 22
  - //other users

How to query the database like so:

String uid = FirebaseAuth.getInstance().getCurrentUser().getUid();
Query q = db.collection("users").whereNotEqualTo("uid", uid);

So I need this query object to be passed to a FirestoreRecyclerOptions object in order to display all the other users in RecyclerView.

Is this even possible? If not, how can I solve this? Thanks!

Edit:

options = new FirestoreRecyclerOptions.Builder<UserModel>()
        .setQuery(query, new SnapshotParser<UserModel>() {
            @NonNull
            @Override
            public UserModel parseSnapshot(@NonNull DocumentSnapshot snapshot) {
                UserModel userModel = documentSnapshot.toObject(UserModel.class);
                if (!userModel.getUid().equals(uid)) {
                    return userModel;
                } else {
                    return new UserModel();
                }
            }
        }).build();
Joan P.
  • 2,368
  • 6
  • 30
  • 63

8 Answers8

9

After days and days of struggling with this issue, I finally found the answer. I could not solve this without the help of @Raj. Thank you so much @Raj for the patience and guidance.

First off all, according to the answer provided by @Frank van Puffelen in his answer from this post, I stopped searching for a solution that can help me pass two queries to a single adapter.

In this question, all that I wanted to achieve was to query the database to get all the users except one, me. So because we cannot combine two queries into a single instance, I found that we can combine the result of both queries. So I have created two queries:

FirebaseFirestore db = FirebaseFirestore.getInstance();
Query firstQuery = db.collection("users").whereLessThan("uid", uid);
Query secondQuery = db.collection("users").whereGreaterThan("uid", uid);

I'm having a UserModel (POJO) class for my user object. I found not one, but two ways to solve the problem. The first one would be to query the database to get all user objects that correspond to the first criteria and add them to a list. After that, query the database again and get the other user objects that correspond to the second criteria and add them to the same list. Now I have a list that contains all the users that I need but one, the one with that particular id from the queries. This is the code for future visitors:

firstQuery.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
    @Override
    public void onComplete(@NonNull Task<QuerySnapshot> task) {
        List<UserModel> list = new ArrayList<>();
        if (task.isSuccessful()) {
            for (DocumentSnapshot document : task.getResult()) {
                UserModel userModel = document.toObject(UserModel.class);
                list.add(userModel);
            }

            secondQuery.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
                @Override
                public void onComplete(@NonNull Task<QuerySnapshot> task) {
                    if (task.isSuccessful()) {
                        for (DocumentSnapshot document : task.getResult()) {
                            UserModel userModel = document.toObject(UserModel.class);
                            list.add(userModel);
                        }

                        //Use the list of users
                    }
                }
            });
        }
    }
});

The second approach would be much shorter because I use Tasks.whenAllSuccess() like this:

Task firstTask = firstQuery.get();
Task secondTask = secondQuery.get();

Task combinedTask = Tasks.whenAllSuccess(firstTask, secondTask).addOnSuccessListener(new OnSuccessListener<List<Object>>() {
        @Override
        public void onSuccess(List<Object> list) {
            //This is the list that I wanted
        }
});
Joan P.
  • 2,368
  • 6
  • 30
  • 63
  • Now, can you plz tell how will you pass that data in Firestore Recycler Adapter ? – Raj Aug 22 '18 at 19:18
  • In both cases I have a list of all user objects except mine. I'll pass that list to the adapter constructor and populate the views accordingly. – Joan P. Aug 22 '18 at 19:27
  • When you successfully pass this data to "Firestore Recycler Adapter" do post the solution. – Raj Aug 22 '18 at 19:34
  • @Raj Yes. Thanks again! – Joan P. Aug 22 '18 at 20:18
  • 1
    You can improve this solution since its using two queries to firestore, in the long run it can increase your cost. If you get time to improve it do look at the custom implementation for recycler view and filtering at client side. – Umar Hussain Aug 27 '18 at 07:48
  • Querying Firebase twice to return one set of information does not seem cost efficient for real world applications. – AdamHurwitz Aug 27 '18 at 23:29
  • 1
    @AdamHurwitz Thanks for your comment and answer but why do you say "Querying Firebase twice to return one set of information does not seem cost efficient for real world applications"? What can happen? Thank you! – Joan P. Aug 28 '18 at 08:58
  • @AdamHurwitz It was also really necessary to down-vote? :( – Joan P. Aug 28 '18 at 08:59
  • @IoanaP. Firebase charges based on how many requests you send them. For prototyping or smaller applications making an extra query / request is not going to have a costly impact, but at larger levels will rack up a high bill and is less performant. – AdamHurwitz Aug 28 '18 at 16:59
  • @AdamHurwitz I'm sorry Adam but I cannot understand you. What is difference if I query the database once to get all users or if I query the database using two queries to get same amount of users except one? Let's say I have 1000 users, in the first case I make 1000 reads in one query in the second case I make 999 reads in 2 queries. One read operation is the difference? I have the cost of one read operation, even if I query the database and I get no result, right? – Joan P. Aug 29 '18 at 10:53
  • @AdamHurwitz Beside that, two queries on the same amount of data will be much faster rather than a single query on the exact amount of data, right? – Joan P. Aug 29 '18 at 10:55
  • @IoanaP. One read operation is the difference. This difference is fine for getting something to work, but is not suitable for production if this information is updated on a regular basis as the extra read operation would accumulate over time. Firestore query performance is based on the size of the query result, not the size of the information queried from. If 2 queries are being made I'd expect the additional operation to add slightly additional time. From an architecture stand point it is always best to simplify the # of queries being made. – AdamHurwitz Aug 30 '18 at 18:35
  • using your code, i am get getting warning error in isScoliing , Transform variable into final one element array, please resolve me this query, thank you – Qutbuddin Bohra Feb 06 '20 at 07:09
6

According to the official firestore documentation:-

Cloud Firestore does not support the following type of query:

Queries with a != clause. In this case, you should split the query into a greater-than query and a less-than query. For example, although the query clause where("age", "!=", "30") is not supported, you can get the same result set by combining two queries, one with the clause where("age", "<", "30") and one with the clause where("age", ">", 30).

If you are using FirestoreRecyclerAdapter then FirestoreRecyclerOptions will directly accepts the query using setQuery() method and hence not allows you to perform client side filtering.

If you try to apply filters in onBindViewHolder() while setting the data that might results in empty items in the recycler view. In order to resolve that refer Method 2.

So, the possible solution to your problem would be to create an integer field in your users collection under every document. Eg:-

users [collection]
  - uid [document]
     - uid: "fR5bih7SysccRu2Gu9990TeSSyg2"
     - username: "John"
     - age: 22
     - check: 100

In this I have created a 'check' variable whose value is 100. So, put value of 'check' in all other documents as less than 100. Now, you can easily make a query that finds documents with check<100 as:-

Query q = db.collection("users").whereLessThan("check", 100);

This will retrieve all your documents except the one you don't want. And while setting the data you can set other parameters skipping the check variable.

Method 2 (Client Side Filtering)

We can apply a check in onBindViewHolder() method that if the retrieved uid matches with current user uid then set the height of Recycler view as 0dp. As:-

ViewUserAdapter.java

public class ViewUserAdapter extends FirestoreRecyclerAdapter<User, ViewUserAdapter.ViewUserHolder>
{
    String uid;
    FirebaseAuth auth;

    public ViewUserAdapter(@NonNull FirestoreRecyclerOptions<User> options)
    {
        super(options);
        auth = FirebaseAuth.getInstance();
        uid = auth.getCurrentUser().getUid();
    }

    @Override
    protected void onBindViewHolder(@NonNull ViewUserHolder holder, int position, @NonNull User model)
    {
        DocumentSnapshot snapshot =  getSnapshots().getSnapshot(position);
        String id = snapshot.getId();

        if(uid.equals(id))
        {
            RecyclerView.LayoutParams param = (RecyclerView.LayoutParams)holder.itemView.getLayoutParams();
            param.height = 0;
            param.width = LinearLayout.LayoutParams.MATCH_PARENT;
            holder.itemView.setVisibility(View.VISIBLE);

        }
        else
        {
            holder.tvName.setText(model.name);
            holder.tvEmail.setText(model.email);
            holder.tvAge.setText(String.valueOf(model.age));
        }
    }
}
Raj
  • 2,997
  • 2
  • 12
  • 30
  • 1
    Hope this helps @IoanaP. – Raj Aug 16 '18 at 20:37
  • 1
    Thank you so much for your answer, I really appreciate it but I cannot understand how your solution can solve my problem. Let's say I'm authenticated in the app and everything works fine but what happens if another user is using the app? It will display all the users except me and this not what I want. I want to display all the users except him, right? – Joan P. Aug 17 '18 at 11:18
  • 1
    Raj, do you have another idea? Thanks! – Joan P. Aug 17 '18 at 14:28
  • 1
    As you didn't mention anything about this in your above question. So, I thought your requirement is to retrieve data only at one place or for one user. So, this will not work in your case. So, let me think for some another logic to solve your problem @loanaP. – Raj Aug 17 '18 at 14:39
  • 1
    Ok, take your time and thanks again for trying to help me. Hope you'll find a way because I'm struggling for days. – Joan P. Aug 17 '18 at 15:12
  • Hi, have you found a solution? I also added a bounty for this question. Thanks! – Joan P. Aug 19 '18 at 09:53
  • What you are actually suggesting is to set the height of the view to `0` but this doesn't mean that I'm excluded from the query. All I want is to query the Firestore database to get all the users except me. Thanks Raj for your time. If you have also other ideas, please consider adding it. – Joan P. Aug 20 '18 at 13:32
  • As such firestore doesn't have any method to retrieve the data using not equal to. So, you can retrieve data in that way. The thing is you can retrieve all data and use logic on client side to filter according to your requirements. As you have to leave only one data so it doesn't make much effect in efficiency if you are retrieving the complete data. – Raj Aug 20 '18 at 13:43
  • I'm not concerned about efficiency for the moment, I just want to get all the users except me, that's it. – Joan P. Aug 20 '18 at 14:46
  • Yes and the result of the query still contains my user object that I don't want it. What you are doing is just hiding a view. – Joan P. Aug 20 '18 at 15:52
  • Is the current user details are show in in recycler view ?? Did you add this DocumentSnapshot snapshot = getSnapshots().getSnapshot(position); String id = snapshot.getId(); ? – Raj Aug 20 '18 at 15:57
  • No, are not displayed but this is because you are hiding a view not because you are querying the database and you exclude an element that I don't want it to be there. Do you understand me now? – Joan P. Aug 20 '18 at 16:00
  • The only method I found after searching for days on Internet is this only. It doesn't matter if I hide that item or something else. The thing is the data now shown in recycler view is correct as according to your requirements – Raj Aug 20 '18 at 16:20
  • My requirement is to query the database to get the data except one object, mine user object. "doesn't matter if I hide that item or something else" it does, I want to a clean query, not to hide a view. Btw, thanks for your efforts. – Joan P. Aug 20 '18 at 18:28
  • Hi Raj, sorry for disturbing you. Do you think that you can find a way to solve this issue? I also wrote to Umar Hussain but he is not answering. Thanks! – Joan P. Aug 22 '18 at 15:23
  • As I already said, I need to query the database, to get all the users except me but not through hiding a view. Thanks! – Joan P. Aug 22 '18 at 15:40
  • Basically what task you want to do by getting that data. Can you plz explain in detail. As the above task is not possible in firestore. So, we can look for some another logic to do the exact task you want to perform. – Raj Aug 22 '18 at 15:43
  • Moreover is it really necessary for you to use Query? What is the main purpose that you are using query. If you use addSnapshotListener your task will be achieved very easily. And also you can customized the data as you want @LoanaR. – Raj Aug 22 '18 at 16:06
  • I've searched and read tens of answers but none of them helped me so far. I cannot believe this is not possible in Firestore :( I only want to get all users, except one. "it really necessary for you to use Query?" Yes it is. I don't need to use `addSnapshotListener`. I don't need data in realtime, I only need to get it once. Hope there will be a way. Thanks! – Joan P. Aug 22 '18 at 16:24
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/178554/discussion-between-raj-and-ioana-p). – Raj Aug 22 '18 at 16:29
  • Because you assured me in your last comment that this is possible, I finally found the answer and put it in code. Thanks again! – Joan P. Aug 22 '18 at 18:57
  • It doesn't , it works fine. Just tested both solutions. – Joan P. Aug 22 '18 at 19:03
  • Thanks again, you deserve it. – Joan P. Aug 27 '18 at 10:22
  • Welcome @loanaP. – Raj Aug 27 '18 at 12:47
5

2021 Update: This Is Supported

Howdy devs. It looks like this is now supported with the where operator used like this: citiesRef.where("capital", "!=", false);

Davis Jones
  • 1,504
  • 3
  • 17
  • 25
4

Firestore doesn't support not equal to operation. So you need to filter the data at the client side. Since in you case you only have one extra item you can filter it out.

For that you may need to build your own recycler implementation where when adding data to recycler adapter data layer, you restrict the data when ever it matches your != condition.

I haven't explored recycler implementation firebase provided so I cannot say it supports data manipulation to adapter data or not.

Here is a good resource to start implementing recycler view : https://www.androidhive.info/2016/01/android-working-with-recycler-view/

Umar Hussain
  • 3,461
  • 1
  • 16
  • 38
  • Thanks for your answer Umar. I alrdeay tried this out to use a condition when setting the names to the `RecyclerView` and I always get an empty item view. The text is not present but the view remains. Imagine I have 4 users in my database and when I'm trying to display them using an if statement (`if(!myUserClas.getId().equals(uid))`) I get 3 rows with the names and one without. Do you have any other idea?s – Joan P. Aug 15 '18 at 13:54
  • You are on the right track but the issue is where you are applying the condition. At that time recycler view already has the data and the your condition only removes the name not the actual recycler item. – Umar Hussain Aug 15 '18 at 13:55
  • Yes, this is exact what is happening. You are absolutely right so I thought I should just remove that elemet before passing the query to the `FirestoreRecyclerOptions`, right? – Joan P. Aug 15 '18 at 13:57
  • Have you tried the customer parser with the set query and return null when you find the user snapshot with the current user id? By passing null I think you can remove that empty view but I'm not sure. https://github.com/firebase/FirebaseUI-Android/blob/master/database/README.md#using-the-firebaserecycleradapter – Umar Hussain Aug 15 '18 at 13:58
  • I cannot return `null` because I get this error: `FATAL EXCEPTION: main Process: com.package.firebase, PID: 1388 java.lang.NullPointerException: key == null || value == null` I have tried to return `return new UserModel();` by I'm ending in having again an empty view. Do you have any idea why is behaving like this and how can I solve this? – Joan P. Aug 20 '18 at 14:44
1

The simplest solution would be to use a PagedListAdapter and create a custom DataSource for the Firestore queries. In the DataSource the Query can be transformed into an Array or ArrayList in which you can easily remove your item before adding the data to the method callback.onResult(...).

I used a similar solution to process data after a Firestore query in order to filter and sort by a time attribute, and then re-sort by a quality score attribute in the client before passing the data back in to callback.onResult(...).

Documentation

Data Source Sample

class ContentFeedDataSource() : ItemKeyedDataSource<Date, Content>() {

override fun loadBefore(params: LoadParams<Date>, callback: LoadCallback<Content>) {}

override fun getKey(item: Content) = item.timestamp

override fun loadInitial(params: LoadInitialParams<Date>, callback: LoadInitialCallback<Content>) {
    FirestoreCollections.contentCollection
            .collection(FirestoreCollections.ALL_COLLECTION)
            .orderBy(Constants.TIMESTAMP, Query.Direction.DESCENDING)
            .whereGreaterThanOrEqualTo(Constants.TIMESTAMP, DateAndTime.getTimeframe(WEEK))
            .limit(params.requestedLoadSize.toLong())
            .get().addOnCompleteListener {
                val items = arrayListOf<Content?>()
                for (document in it.result.documents) {
                    val content = document.toObject(Content::class.java)
                    items.add(content)
                }
                callback.onResult(items.sortedByDescending { it?.qualityScore })
            }
}

override fun loadAfter(params: LoadParams<Date>, callback: LoadCallback<Content>) {
    FirestoreCollections.contentCollection
            .collection(FirestoreCollections.ALL_COLLECTION)
            .orderBy(Constants.TIMESTAMP, Query.Direction.DESCENDING)
            .startAt(params.key)
            .whereGreaterThanOrEqualTo(Constants.TIMESTAMP, DateAndTime.getTimeframe(WEEK))
            .limit(params.requestedLoadSize.toLong())
            .get().addOnCompleteListener {
                val items = arrayListOf<Content?>()
                for (document in it.result.documents) {
                    val content = document.toObject(Content::class.java)
                    items.add(content)
                }
                val sortedByQualityScore = ArrayList(items.sortedByDescending { it?.qualityScore })
                callback.onResult(sortedByQualityScore)
                sortedByQualityScore.clear()
            }
}
}
AdamHurwitz
  • 9,758
  • 10
  • 72
  • 134
  • 1
    Can you please explain me how can I use this code to remove a single user (which is me) from a query? I'll prefer in Java because JS or Kotlin I don't know. Thanks! – Joan P. Aug 28 '18 at 09:04
  • @IoanaP. In the sample code above the `loadInitial()` method for instance makes the Firebase Firestore query to return the data `it.result.documents`. I convert each `document` into my own `Content` object: `val content = document.toObject(Content::class.java)` and then add it to a list `items.` After all of the `Content` objects are in the list of `items` you can remove the unwanted user from the list using `.remove(itemToRemove)` before adding the list of items to the callback method `callback.onResult(items)`. You would also make sure to do the same in the `loadAfter()` method. – AdamHurwitz Aug 28 '18 at 17:05
  • The documentation in my post above explains the PagedListAdapter in full detail. – AdamHurwitz Aug 28 '18 at 17:08
  • As I understand from you code, is that you are ordering the result, you aren't excluding one of them at all, as my initial question was. Can you provide me a concrete example on how I can get the same result as mine, using your code? Thanks! – Joan P. Aug 29 '18 at 10:57
  • @IoanaP. Correct, the first step taken is ordering the result. Once the ordered result is organized into a collection such as an ArrayList you can remove one or multiple objects if you define a custom **Hash Code** for your object based on the object's attributes. If you define an `ArrayList` you can then call `listName.remove(objectName)`. Here is a sample on defining a custom Hash Code: https://www.mkyong.com/java/java-how-to-overrides-equals-and-hashcode/. – AdamHurwitz Aug 30 '18 at 18:27
0

Simpler and earlier client-side filtering (when you add items to your list):

  1. Get the current user's ID by using Firestore's standard method.
  2. Get the name of the doc for all the users in your user collection.
  3. Before adding the user to your RecyclerView list, check that the user it is about to add to your list is not the current user.

When done is this way, you can use the "not equals" method on the client side and not get into any Firestore issues. Another benefit is that you don't have to mess with your adapter or hide the view from a list-item you didn't want in the recycler.

public void getUsers(final ArrayList<Users> usersArrayList, final Adapter adapter) {

    CollectionReference usersCollectionRef = db.collection("users");

    Query query = usersCollectionRef
            .whereEqualTo("is_onboarded", true);

    query.get()
            .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
                @Override
                public void onComplete(@NonNull Task<QuerySnapshot> task) {
                    if (task.isSuccessful()) {
                        for (QueryDocumentSnapshot document : task.getResult()) {

                            final String otherUserID = document.getId();

                             FirebaseUser user = mAuth.getCurrentUser();
                             String currentUserID = user.getUid();

                            if (!otherUserID.equals(currentUserId)) {

                              usersArrayList.add(new User(otherUserID));
                              adapter.notifyDataSetChanged(); //Ensures users are visible immediately
                                            }
                                        } else {
                                            Log.d(TAG, "get failed with ", task.getException());
                                        }
                                    }
                                });
                            }

                        }
                    } else {
                        Log.d(TAG, "Error getting documents: ", task.getException());
                    }
                }
            });
}
The Fluffy T Rex
  • 430
  • 7
  • 22
0

You don't have to do all this

Just do normal query and hide the layout by setting getLayoutParams().height and width to 0 respectively. See example below.

  if(firebaseUserId.equalIgnoreCase("your_holder_firebase_user_id"){
    holder.mainLayout.setVisibility(View.INVISIBLE);
    holder.mainLayout.getLayoutParams().height = 0;
    holder.mainLayout.getLayoutParams().width = 0;

  }else {
   //show your list as normal
  }
  //This will hide any document snapshot u don't need, it will be there but hidden
 
Ally Makongo
  • 269
  • 5
  • 14
0

here's my solution with flutter for usernames

Future<bool> checkIfUsernameExistsExcludingCurrentUid(
      // TODO NOT DONE
      String username,
      String uid) async {
    print("searching db for: $username EXCLUDING SELF");
    bool exists = true;

    QuerySnapshot result = await _firestore
        .collection(USERS_COLLECTION)
        .where(
          "username",
          isEqualTo: username,
        )
        .getDocuments();

    List _documents = result.documents;
    _documents.forEach((element) {
      if (element['uid'] == uid) {
        exists = false;
      } else {
        return true;
      }
    });

    return exists;
  }
Alex Chashin
  • 3,129
  • 1
  • 13
  • 35