0

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.

Al-Mothafar
  • 7,949
  • 7
  • 68
  • 102
  • Both are reasonable data models. What's your question? – Frank van Puffelen Jul 13 '17 at 14:00
  • @FrankvanPuffelen the better performance, let's say you get 1 Million users, which one should be better for querying about customerId. – Al-Mothafar Jul 13 '17 at 17:22
  • The second approach is called fanning out and is covered in the Firebase documentation under: https://firebase.google.com/docs/database/android/structure-data#fanout. 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. Those are not as slow as most devs expect (see http://stackoverflow.com/questions/35931526/speed-up-fetching-posts-for-my-social-network-app-by-using-query-instead-of-obse/35932786#35932786), but there's always a usability limit on that. – Frank van Puffelen Jul 13 '17 at 19:06
  • It is hard to say "this is better than that". If there was, the Firebase 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 https://stackoverflow.com/questions/37884671/are-firebase-queries-scalable, https://stackoverflow.com/questions/39712833/firebase-performance-how-many-children-per-node/39713060#39713060 – Frank van Puffelen Jul 13 '17 at 19:09
  • @FrankvanPuffelen if you got time to make this comment as an answer so I can accept it, I'm going to close this question :) , thanks – Al-Mothafar Nov 21 '17 at 15:11

1 Answers1

2

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?.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807