How to exclude an element from a Firestore query? - java

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();

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
}
});

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));
}
}
}

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);

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/

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
Google: Build your own data sources
Codepath: Paging Library Guide
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()
}
}
}

Simpler and earlier client-side filtering (when you add items to your list):
Get the current user's ID by using Firestore's standard method.
Get the name of the doc for all the users in your user collection.
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());
}
}
});
}

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

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;
}

Related

Why my Firebase firestore data is not showing in android

I am working on an application where anyone can list their products. I am storing data in Firebase Firestore in nested collection Now I want to retrieve that data and show that on my home screen. Now the data is showing but the problem is that it is showing only when I am login in with that same number through that I list that data into Firebase but when I try to log in with another number the data doesn't show. I want that to show to everyone who logged in to the app. Basically My app is just like OLX where anyone can list anything which shows to everyone.
MY CODE TO RETRIEVE THE DATA
//CODE TO GET CURRENT ID OR USER
String uid = FirebaseAuth.getInstance().getCurrentUser().getUid();
//CODE TO GET THE DATA FROM FIREBASE
DocumentReference uidRef = firebaseFirestore.collection("listing_details").document(uid);
CollectionReference roomDetailsRef = uidRef.collection("room_details");
String doc_id = roomDetailsRef.document().getId();
roomDetailsRef.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
for (QueryDocumentSnapshot document : task.getResult()) {
if (document != null) {
RoomsDetails obj = document.toObject(RoomsDetails.class);
roomsDetails.add(obj);
}
}
roomsAdapter.notifyDataSetChanged();
} else {
Log.d(TAG, task.getException().getMessage()); //Never ignore potential errors!
}
}
});
You have .document(uid) in your path where UID is User ID of user currently logged in. When you use another phone number, that's a different user.
If you want to fetch room_details documents from all listing_details documents then you can use Collection Group queries like this:
db.collectionGroup("room_details").get()
.addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot queryDocumentSnapshots) {
// ... iterate over all docs and render
}
});

Android Firebase Firestore query returns empty results from function, but the same code works when included directly

I'm having a trouble with firebase queries in Studio.
I'm trying to abstract a simple query to get a list of object stored in a collection (in my case "users")
I want to create a function stored in a Class that can be called by every fragment into the project.
But I don' t find any method to do that, is to repeate the same instruction the only way to do that?
Here is an example
db.collection("users") //get all the users
.get()
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
if(task.getResult() != null) {
for (QueryDocumentSnapshot document : task.getResult()) {
usersList.add(document.toObject(User.class));
} else {
Log.w(LOGIN, "Error getting documents.", task.getException());
}
});
I write these lines of code every time I need them, but I want to create a method that return a List as in this example:
public static List<User> getUsers(FirebaseFirestore db) {
List<User> usersList = new ArrayList<>();
db.collection("users") //get all the users
.get()
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
if(task.getResult() != null) {
for (QueryDocumentSnapshot document : task.getResult()) {
usersList.add(document.toObject(User.class));
} else {
Log.w(LOGIN, "Error getting documents.", task.getException());
}
});
} else {
//error
}
return usersList;
}
Data is loaded from Firestore (and most modern cloud APIs) asynchronously, because it may take some time. Instead of blocking the app during that time, the main code continues to execute. Then when the data is available, your addOnCompleteListener callback is executed with that data.
The easiest way to see this is by adding some well-placed logging to your code:
public static List<User> getUsers(FirebaseFirestore db) {
Log.i(LOGIN, "Starting getUsers");
db.collection("users") //get all the users
.get()
.addOnCompleteListener(task -> {
Log.i(LOGIN, "Got data");
})
Log.i(LOGIN, "Returning from getUsers");
}
When you run this code, you get the following output:
Starting getUsers
Returning from getUsers
Got data
This is probably not the order you expected, but it completely explains why the code that calls getUsers never sees the data: by the time your return usersList runs, the data hasn't loaded yet and usersList.add(document.toObject(User.class)) has never been called.
The solution is always the same: any code that needs the data from the database, must either be inside the completion callback, be called from there, or be synchronized by some other means.
A simple example is to create a custom callback function:
public interface GetUsersCallback {
void onCallback(List<User> users);
}
You then pass that to getUsers, which can then call it once it's gotten and processed the results from the database:
public static void getUsers(FirebaseFirestore db, GetUsersCallback callback) {
// 👆
List<User> usersList = new ArrayList<>();
db.collection("users") //get all the users
.get()
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
if(task.getResult() != null) {
for (QueryDocumentSnapshot document : task.getResult()) {
usersList.add(document.toObject(User.class));
} else {
Log.w(LOGIN, "Error getting documents.", task.getException());
}
});
callback(usersList); // 👈
}
}
And you can then use it like this:
getUsers(new GetUsersCallback() {
#Override
public void onCallback(List<User> users) {
Log.i(LOGIN, "Found "+users.size()+" users");
}
});
Asynchronous loading of data is incredibly common when dealing with cloud APIs, but it's also quite confusing when you first encounter it. I recommend reading some of these answers to learn more about it:
How to check a certain data already exists in firestore or not
How to return a DocumentSnapShot as a result of a method?
Why does my function that calls an API return an empty or null value?
Firebase Firestore get data from collection

OrderBy not working properly in firestore

I'm facing with a not so cool problem and been thinking a way through it.
As in the above picture I'm using the timestamp field which has a timestamp datatype, to do a orderBy descending query.
After the query is done I need to enter the Order Item subcollection to get all the data.
CollectionReference orderRef = fStore.collection(Reference.ORDER_COL);
orderRef.orderBy(Reference.TIME_STAMP, Query.Direction.DESCENDING)
.whereEqualTo(Reference.CUSTOMER_UID, logInSession.getUID())
.get()
.addOnSuccessListener(this, new OnSuccessListener<QuerySnapshot>() {
#SuppressWarnings("ConstantConditions")
#Override
public void onSuccess(QuerySnapshot queryDocumentSnapshots) {
if (!queryDocumentSnapshots.isEmpty()) {
for (QueryDocumentSnapshot docSnap : queryDocumentSnapshots) {
Log.e("TAG 1", "Loop 1");
final boolean packing = docSnap.getBoolean(Reference.STATUS_PACKING);
final boolean outForDelivery = docSnap.getBoolean(Reference.STATUS_OUT_FOR_DELIVERY);
final boolean delivered = docSnap.getBoolean(Reference.STATUS_DELIVERED);
final String orderDate = docSnap.getString(Reference.DATE);
final int totalAmount = docSnap.getLong(Reference.TOTAL_AMOUNT).intValue();
CollectionReference orderQuery = orderRef.document(docSnap.getId()).collection(Reference.ORDER_ITEM_COL);
orderQuery.addSnapshotListener(new EventListener<QuerySnapshot>() {
#Override
public void onEvent(#Nullable QuerySnapshot value, #Nullable FirebaseFirestoreException error) {
if (!value.isEmpty()) {
for (QueryDocumentSnapshot docSnap : value) {
Log.d("TAG 2" , "Loop 2");
String productName = docSnap.getString(Reference.PRODUCT);
String totalPrice = String.valueOf(docSnap.getLong(Reference.PRICE).intValue());
String quantity = String.valueOf(docSnap.getLong(Reference.QUANTITY).intValue());
String thumbnail = docSnap.getString(Reference.THUMBNAIL);
orderHistoryList.add(new OrderHistory(productName, thumbnail, quantity, totalPrice, orderDate, packing, outForDelivery, delivered));
adapter.notifyDataSetChanged();
}
}
}
});
}
} else {
noOrderTxt.startAnimation(AnimationUtils.loadAnimation(OrderActivity.this, R.anim.fade_in));
noOrderTxt.setVisibility(View.VISIBLE);
}
}
});
Above are the coding I did and the output I get is all messed up and is not queried on timestamp, there is also a whereEqualTo() method which is working fine.
As you can see I use whereEqualTo() and orderBy() and also have created the required Composite Indexes.
I place a log on both the for loops, here the first for loop executed first and then the second for loop came in which is confusing. I thought after the first for loop the second for loop will be executed but it didn't happen, not like the regular nested for loop it didn't work as I plan so I think that's the reason behind the messed up result I get on my app.
What I want is a proper DESCENDING orderBy query which I will be displaying to a RecyclerView.
Please help me out guys and thank you in advance. If you need anymore codes or details please tell me :)

How Should I fetch the document fields and use them in another map for another collection?

How should I fetch the document fields from one collection and combine them to add a new document to another collection? I have attached picture of the database how does it looks, I want to fetch the fields from the collection show and want to update it to the new collection along with some other data:
private void savePost(String mPostTitle, String mPostContent, String mlistSpinnerC) {
final DocumentReference docRef = FirebaseFirestore.getInstance().collection("users").document(mauth.getCurrentUser().getUid());
docRef.get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
if (task.isSuccessful()) {
DocumentSnapshot document = task.getResult();
if (document != null) {
String username = (String)
document.get("username");
String email= (String) document.get(email);
} else {
Log.d(TAG, "No such document");
}
} else {
Log.d(TAG, "get failed with ", task.getException());
}
}
});
postMap.put(Constants.POSTTTITLE, mPostTitle);
postMap.put(Constants.POSTCATEGORY, mlistSpinnerC);
postMap.put(Constants.POSTCONTENT, mPostContent);
postMap.put(Constants.TIMESTAMP, (System.currentTimeMillis()/1000));
postMap.put(Constants.USER_ID,mauth.getCurrentUser().getUid());
postMap.put("username", username);
PostsRef.document().set(postMap).addOnCompleteListener(new OnCompleteListener<Void>() {
#Override
public void onComplete(#NonNull Task<Void> task) {
if(task.isSuccessful()){
Intent toHomeActivity = new Intent(AddPostActivity.this, MainActivity.class);
startActivity(toHomeActivity);
}
}
});
I am just not able to map the fields from one collection to another collection, please guide me the correct method to that.
By the time you are trying to add the username to your postMap using the following line of code:
postMap.put("username", username);
The data has not finished loading yet from the database and this is because the listener you have added to your get() call is being invoked some unknown amount of time later after your query finishes. You don't know how long it's going to take, it may take from a few hundred milliseconds to a few seconds before that data is available. The onComplete() method has an asynchronous behavior, that's why you cannot get that username in such a way.
A quick solve for this problem would be to move all that block of code related to adding data to the postMap, inside the onComplete() method. In this you are waiting for the callback and username your will be available. Otherwise I recommend you see the last part of my anwser from this post in which I have explained how it can be done using a custom callback. You can also take a look at this video for a better understanding.

Query FireStore against a List of Documents

I have a List<String> of names referring to Documents that I want to retrieve from FireStore. I want to access the contents after they are finished loading so I have implemented an OnCompleteListener in the Fragment that uses the data. However, I am not sure how to run a loop within a Task to query FireStore for each Document. I am querying FireStore in a Repository class that returns a Task object back through my ViewModel and finally to my Fragment. I want the Repository to return a Task so that I can attach an OnCompleteListener to it in order to know when I have finished loading my data.
My Repository Query method:
public Task<List<GroupBase>> getGroups(List<String> myGroupTags){
final List<GroupBase> myGroups = new ArrayList<>();
for(String groupTag : myGroupTags){
groupCollection.document(groupTag).get()
.addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
if(task.isSuccessful()){
myGroups.add(task.getResult().toObject(GroupBase.class));
}
}
});
}
return null; //Ignore this for now.
}
I know this won't return what I need but I am not sure how to structure a Task that incorporates a loop inside of it. Would I need to extract the contents of the List<String> in my Fragment and run individual queries for each item?
Any suggestions would be greatly appreciated.
According to your comment:
I have a List of Document names and I need to transform this to essentially a List of Tasks to retrieve the entire document.
To solve this, please use the following lines of code:
FirebaseFirestore rootRef = FirebaseFirestore.getInstance();
CollectionReference collRef = rootRef.collection("yourCollection");
List<String> myGroupTags = new ArrayList<>();
List<DocumentReference> listDocRef = new ArrayList<>();
for(String s : myGroupTags) {
DocumentReference docRef = collRef.document(s);
listDocRef.add(docRef);
}
List<Task<DocumentSnapshot>> tasks = new ArrayList<>();
for (DocumentReference documentReference : listDocRef) {
Task<DocumentSnapshot> documentSnapshotTask = documentReference.get();
tasks.add(documentSnapshotTask);
}
Tasks.whenAllSuccess(tasks).addOnSuccessListener(new OnSuccessListener<List<Object>>() {
#Override
public void onSuccess(List<Object> list) {
//Do what you need to do with your list
for (Object object : list) {
GroupBase gb = ((DocumentSnapshot) object).toObject(GroupBase.class);
Log.d("TAG", tp.getPropertyname);
}
}
});

Categories

Resources