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 :)
Related
I have a list of objects, saved by unique ids. Every time, I read several objects(decided by few conditions). That is done with for loop, using snapshot.getChildren(). My problem is how to continue reading the data from last retrieved object. Eg. If I have 10 objects in the database, I read 3 of them first time. Next time the app should continue reading from the fourth object, without going through already retrieved objects.
I researched about Query, startAt(), but it is allowing only search by value, not by id(my ids are firebaseReference.push()).
Code which I am using to point at specific value, but is not working:
public void getFeedPosts(final FeedPostsCallback feedPostCallback) {
final ArrayList<Post> posts = new ArrayList<>();
readFollowing = true;
getFollowing(myID, new FollowingCallback() {
#Override
public void onCallback(final ArrayList<String> following) {
referenceOnPosts().startAt("-MMequ1jrR1m73cKfgbt", "-MMequ1jrR1m73cKfgbt").addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(#NonNull DataSnapshot snapshot) {
try {
if (readFeedPosts) {
readFeedPosts = false;
Log.d(tag, "Reading feed posts");
for (DataSnapshot dataSnapshot : snapshot.getChildren()) {
Log.d(tag, dataSnapshot.toString());
posts.add(dataSnapshot.getValue(Post.class));
}
Collections.reverse(posts);
feedPostCallback.onCallback(posts);
}
} catch (Exception e) {
Log.d(tag, e.toString());
}
}
#Override
public void onCancelled(#NonNull DatabaseError error) {
Log.d(tag, error.getMessage());
}
});
}
});
}
Here is the image:
Cursors in Firebase Realtime Database are based on being able to quickly find the anchor node in the list of potential results.
To do that you need to know up to two values:
You need to know the value of the property you sorted on, for the node you want to start at.
You may need to know the key of the node you want to start at, in case there may be more nodes with the same value for #1.
If you have both values, you pass them into startAt() (or endAt() if you want the final slice of the results):
ref.orderBy("propertyToOrderOn").startAt("valueOfProperty", "keyOfNode")
If you want to order/paginate on the key only, you can do:
ref.orderByKey().startAt("-MMequ1jrR1m73cKfgbt")
I have a RecyclerView that utilizes the FireaseUI and orders all objects from the "Polls" node by the "timestamp" field (sequentially).
New Fragment - .onViewCreated()
Query queryStore = FirebaseFirestore.getInstance()
.collection(POLLS_LABEL)
.orderBy("timestamp", Query.Direction.ASCENDING);
//Cloud Firestore does not have any ordering; must implement a timestampe to order sequentially
FirestoreRecyclerOptions<Poll> storeOptions = new FirestoreRecyclerOptions.Builder<Poll>()
.setQuery(queryStore, Poll.class)
.build();
mFirestoreAdaper = new FirestoreRecyclerAdapter<Poll, PollHolder>(storeOptions) {
#Override
protected void onBindViewHolder(#NonNull final PollHolder holder, final int position, #NonNull Poll model) {
holder.mPollQuestion.setText(model.getQuestion());
String voteCount = String.valueOf(model.getVote_count());
//TODO: Investigate formatting of vote count for thousands
holder.mVoteCount.setText(voteCount);
Picasso.get()
.load(model.getImage_URL())
.fit()
.into(holder.mPollImage);
holder.mView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Intent toClickedPoll = new Intent(getActivity(), PollHostActivity.class);
String recyclerPosition = getSnapshots().getSnapshot(position).getId();
Log.v("Firestore ID", recyclerPosition);
toClickedPoll.putExtra("POLL_ID", recyclerPosition);
startActivity(toClickedPoll);
}
});
}
I have another layout in my app that subscribes to this same node, but instead queries by "followers" and then by "timestamp.:
Following Fragment - .onViewCreated()
Query queryStore = FirebaseFirestore.getInstance()
.collection(POLLS_LABEL)
.whereArrayContains("followers", mUserId)
.orderBy("timestamp", Query.Direction.ASCENDING);
FirestoreRecyclerOptions<Poll> storeOptions = new FirestoreRecyclerOptions.Builder<Poll>()
.setQuery(queryStore, Poll.class)
.build();
mFirestoreAdaper = new FirestoreRecyclerAdapter<Poll, PollHolder>(storeOptions) {
#Override
protected void onBindViewHolder(#NonNull final PollHolder holder, final int position, #NonNull Poll model) {
holder.mPollQuestion.setText(model.getQuestion());
String voteCount = String.valueOf(model.getVote_count());
//TODO: Investigate formatting of vote count for thousands
holder.mVoteCount.setText(voteCount);
Picasso.get()
.load(model.getImage_URL())
.fit()
.into(holder.mPollImage);
holder.mView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Intent toClickedPoll = new Intent(getActivity(), PollHostActivity.class);
String recyclerPosition = getSnapshots().getSnapshot(position).getId();
Log.v("Firestore ID", recyclerPosition);
toClickedPoll.putExtra("POLL_ID", recyclerPosition);
startActivity(toClickedPoll);
}
});
}
In the first scenario, UI items populate, in real-time, into my RecyclerView as they are added to Firebase. However, when I query by ".whereArrayContains," I do not get this same behavior, and I was curious as to why. The items only reappear when I restart the application:
Edit:
I commented out the below line:
// .whereArrayContains("followers", mUserId)
and the behavior performed as expected, therefore I can isolate the issue to the .whereArrayContains() query. It is the only difference between each Fragment.
This is happening because when you are using whereArrayContains() and orderBy() methods in the same query, an index is required. To use one, go to your Firebase Console and create it manually or if you are using Android Studio, you'll find in your logcat a message that sounds like this:
W/Firestore: (0.6.6-dev) [Firestore]: Listen for Query(products where array array_contains YourItem order by timestamp) failed: Status{code=FAILED_PRECONDITION, description=The query requires an index. You can create it here: ...
You can simply click on that link or copy and paste the url into a web broswer and you index will be created automatically.
Why is this index needed?
As you probably noticed, queries in Cloud Firestore are very fast and this is because Firestore automatically creates an indexes for any fields you have in your document. When you need to order your items, a particular index is required that should be created as explained above. However, if you intend to create the index manually, please also select from the dropdown the corresponding Array contains option, as in the below image:
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;
}
When the user checks his friends list in my app, I want the app to go through each user in the list and retrieve his up to date information from the Cloud Firestore.
This is my current code:
final CollectionReference usersRef= FirebaseFirestore.getInstance().collection("users");
usersRef.document(loggedEmail).collection("friends_list").get().addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot documentSnapshots) {
if (!documentSnapshots.isEmpty()){
for (DocumentSnapshot friendDocument: documentSnapshots) {
usersRef.document(friendDocument.getString("email")).get().addOnSuccessListener
(new OnSuccessListener<DocumentSnapshot>() {
#Override
public void onSuccess(DocumentSnapshot documentSnapshot) {
User friend=documentSnapshot.toObject(User.class);
friendsList_UserList.add(friend);
}
});
}
///...
}
else
noFriendsFound();
}
And this is an illustration of my wanted process:
As you can see, I can get the information of each user in this way, but I can't find a way to listen to this process, and proceed when I have the information about all the friends in the user's list.
Is the a way which I'll be able to get all of the friends information at once?
Firestore does not directly support joins like you're asking for.
You could construct a chained listener using the getDocumentChanges in the QuerySnapshot to keep track of which friends you should listen to.
Imagine if you kept a map of friend listener registrations like this
Map<String, ListenerRegistration> friendListeners = new HashMap<>();
Then you could register something like this:
usersRef.document(loggedEmail).collection("friends_list")
.addSnapshotListener(new EventListener<QuerySnapshot>() {
#Override
public void onEvent(QuerySnapshot snapshot, FirebaseFirestoreException error) {
for (DocumentChange change : snapshot.getDocumentChanges()) {
DocumentSnapshot friend = change.getDocument();
String friendId = friend.getId();
ListenerRegistration registration;
switch (change.getType()) {
case ADDED:
case MODIFIED:
if (!friendListeners.containsKey(friendId)) {
registration = usersRef.document(friendId).addSnapshotListener(null);
friendListeners.put(friendId, registration);
}
break;
case REMOVED:
registration = friendListeners.get(friendId);
if (registration != null) {
registration.remove();
friendListeners.remove(friendId);
}
break;
}
}
}
});
Note, however that this may not actually be a good idea. You may be better off pushing enough information down into the friends_list documents that you only need to load the actual friend user document once you're actually drilling down into that friend's details.
I'm facing a really strange decision here about the performance of the following scenarios with dealing with Firebase database, what I'm doing is I generate a random customerId for alternative use and store it for that inside profile (I still use Firebase uid but it is just for "friendly numeric number" as a client wants).
What I'm trying to do one of the following:
When I get the request:
UserVO createdUser = Json.fromJson(getRequestBodyAsJson(), UserVO.class);
CompletableFuture<String> checkCustomerIdCompletableFuture = firebaseDatabaseService.buildUniqueCustomerId();
return checkCustomerIdCompletableFuture.thenApply(customerId -> {
createdUser.setCustomerId(customerId);
return firebaseDatabaseService.addToUserProfile(createdUser.getId(), getObjectAsMapOfObjects(createdUser));
}).thenCompose(completableFuture -> CompletableFuture.completedFuture(ok(Json.toJson(createdUser))));
The customerId is always indexed inside profiles:
"profiles":{
"$uid":{
".read":"$uid === auth.uid",
".write":"$uid === auth.uid",
},
".indexOn": ["customerId", "email"]
}
And on both cases, the profile of user should be like this:
"profiles" : {
"jiac4QpEfggRTuKuTfVOisRGFJn1" : {
"contactPhone" : "",
"createdAt" : 1499606268255,
"customerId" : 4998721187, // OR "A-4998721187" as string
"email" : "almothafar#example.com",
"firstName" : "Al-Mothafar",
"fullName" : "Al-Mothafar Al-Hasan",
"id" : "jiac4QpEfggRTuKuTfVOisRGFJn1",
"lastName" : "Al-Hasan2",
"updatedAt" : 1499857345960,
"verified" : false
}
}
I have 2 options here for buildUniqueCustomerId():
The first one is directly querying inside profiles about customerId, and return the unique id, using queryByChild and the customerId is indexed:
public CompletableFuture<String> buildUniqueCustomerId() {
String customerId = String.valueOf(System.currentTimeMillis()).substring(1, 9).concat(RandomStringUtils.randomNumeric(2));
CompletableFuture<String> dataSnapshotCompletableFuture = new CompletableFuture<>();
firebaseDatabaseProvider.getUserDataReference().child("/profiles").orderByChild("customerId").equalTo(customerId).limitToFirst(1)
.addChildEventListener(new ChildEventListener() {
#Override
public void onChildAdded(DataSnapshot snapshot, String previousChildName) {
if (snapshot.exists()) {
buildUniqueCustomerId();
} else {
dataSnapshotCompletableFuture.complete(customerId);
}
}
#Override
public void onChildChanged(DataSnapshot snapshot, String previousChildName) {
if (snapshot.exists()) {
buildUniqueCustomerId();
} else {
dataSnapshotCompletableFuture.complete(customerId);
}
}
#Override
public void onChildRemoved(DataSnapshot snapshot) {
dataSnapshotCompletableFuture.completeExceptionally(new BusinessException("Child Remove"));
}
#Override
public void onChildMoved(DataSnapshot snapshot, String previousChildName) {
dataSnapshotCompletableFuture.completeExceptionally(new BusinessException("Child MOved"));
}
#Override
public void onCancelled(DatabaseError error) {
dataSnapshotCompletableFuture.completeExceptionally(new BusinessException(error.getMessage()));
}
});
return dataSnapshotCompletableFuture;
}
Another way is to, create new node like reservedCustomerIds, check if customerId is reserved already, and push that id to that array in case it is not reserved and return the ID for use, in this case, customerId is a key:
public CompletableFuture<String> buildUniqueCustomerId() {
String customerId = "A-".concat(String.valueOf(System.currentTimeMillis()).substring(1, 9).concat(RandomStringUtils.randomNumeric(2)));
String customerRef = String.format("/reservedCustomerIds/%s", customerId);
return firebaseDatabaseProvider.fetchObjectAtRef("/usersData".concat(customerRef))
.thenCompose(dataSnapshot -> {
if (dataSnapshot.getValue() != null) {
return buildUniqueCustomerId();
} else {
return CompletableFuture.completedFuture(customerId);
}
})
.thenCompose((newCustomerId) -> this.updateObjectData(true, customerRef).thenApply(aVoid -> newCustomerId))
.exceptionally(throwable -> {
Logger.error(throwable.getMessage());
return null;
});
}
The first way code needs some cleaning, but it is just quick kick in, but you can see that the second way is shorter in the code but it is one more step to store that ID, also, it will have additional storage reservedCustomerIds just for check IDs:
"reservedCustomerIds" : {
"A-4998721187" : true,
"A-4998722342" : true,
"A-4998722222" : true,
"A-4998724444" : true,
"A-4998725555" : true,
}
Which one the best for performance, faster to check customerId uniqueness? use customerId as a key in with extra storage, or use customerId itself inside profiles with .indexOn?
P.S: in comments or full answer if you can give me a link(s) for how firebase indexing, or querying, I'll be so thankful.
The second approach is called fanning out and is covered in the Firebase documentation. Its advantage is that you can read the IDs without needing a query, which means that there's no realistic limit on its scale.
But you need to then do an extra read for each customer. While those are not as slow as most devs expect (see Speed up fetching posts for my social network app by using query instead of observing a single event repeatedly), but there's always a usability limit on that.
It is hard to say "this is better than that". If there was, the Firebase Realtime Database documentation would be explicit (and loud) about it. But flatter structures, fanned out data, and not querying millions of nodes to find 1 all help to having an app that scales smoothly. Also see Are Firebase queries scalable, Firebase Performance: How many children per node?.