Gowtham K K
Gowtham K K

Reputation: 3429

Android Firestore querying particular value in Array of Objects

This is my structure of the firestore database:

Expected result: to get all the jobs, where in the experience array, the lang value is "Swift".

So as per this I should get first 2 documents. 3rd document does not have experience "Swift".

Query jobs = db.collection("Jobs").whereArrayContains("experience.lang","Swift");
jobs.get().addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
            @Override
            public void onSuccess(QuerySnapshot queryDocumentSnapshots) {
               //Always the queryDocumentSnapshots size is 0
            }
        });

Tried most of the answers but none worked out. Is there any way to query data in this structure? The docs only available for normal array. Not available for array of custom object.

Upvotes: 0

Views: 2171

Answers (2)

Alex Mamo
Alex Mamo

Reputation: 138824

Actually it is possible to perform such a query when having a database structure like yours. I have replicated your schema and here are document1, document2, and document3.

Note that you cannot query using partial (incomplete) data. You are using only the lang property to query, which is not correct. You should use an object that contains both properties, lang and years.

Seeing your screenshot, at first glance, the experience array is a list of HashMap objects. But here comes the nicest part, that list can be simply mapped into a list of custom objects. Let's try to map each object from the array to an object of type Experience. The model contains only two properties:

public class Experience {
    public String lang, years;

    public Experience() {}

    public Experience(String lang, String years) {
        this.lang = lang;
        this.years = years;
    }
}

I don't know how you named the class that represents a document, but I named it simply Job. To keep it simple, I have only used two properties:

public class Job {
    public String name;
    public List<Experience> experience;
    //Other prooerties

    public Job() {}
}

Now, to perform a search for all documents that contain in the array an object with the lang set to Swift, please follow the next steps. First, create a new object of the Experience class:

Experience firstExperience = new Experience("Swift", "1");

Now you can query like so:

CollectionReference jobsRef = rootRef.collection("Jobs");
jobsRef.whereArrayContains("experience", firstExperience).get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
    @Override
    public void onComplete(@NonNull Task<QuerySnapshot> task) {
        if (task.isSuccessful()) {
            for (QueryDocumentSnapshot document : task.getResult()) {
                Job job = document.toObject(Job.class);
                Log.d(TAG, job.name);
            }
        } else {
            Log.d(TAG, task.getException().getMessage());
        }
    }
});

The result in the logcat will be the name of document1 and document2:

firstJob
secondJob

And this is because only those two documents contain in the array an object where the lang is set to Swift.

You can also achieve the same result when using a Map:

Map<String, Object> firstExperience = new HashMap<>();
firstExperience.put("lang", "Swift");
firstExperience.put("years", "1");

So there is no need to duplicate data in this use-case. I have also written an article on the same topic

Edit:

In your approach it provides the result only if expreience is "1" and lang is "Swift" right?

That's correct, it only searches for one element. However, if you need to query for more than that:

Experience firstExperience = new Experience("Swift", "1");
Experience secondExperience = new Experience("Swift", "4");
//Up to ten

We use another approach, which is actually very simple. I'm talking about Query's whereArrayContainsAny() method:

Creates and returns a new Query with the additional filter that documents must contain the specified field, the value must be an array, and that the array must contain at least one value from the provided list.

And in code should look like this:

jobsRef.whereArrayContainsAny("experience", Arrays.asList(firstExperience, secondExperience)).get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
    @Override
    public void onComplete(@NonNull Task<QuerySnapshot> task) {
        if (task.isSuccessful()) {
            for (QueryDocumentSnapshot document : task.getResult()) {
                Job job = document.toObject(Job.class);
                Log.d(TAG, job.name);
            }
        } else {
            Log.d(TAG, task.getException().getMessage());
        }
    }
});

The result in the logcat will be:

firstJob
secondJob
thirdJob

And this is because all three documents contain one or the other object.

Why am I talking about duplicating data in a document it's because the documents have limits. So there are some limits when it comes to how much data you can put into a document. According to the official documentation regarding usage and limits:

Maximum size for a document: 1 MiB (1,048,576 bytes)

As you can see, you are limited to 1 MiB total of data in a single document. So storing duplicated data will only increase the change to reach the limit.

If i send null data of "exprience" and "swift" as "lang" will it be queried?

No, will not work.

Edit2:

whereArrayContainsAny() method works with max 10 objects. If you have 30, then you should save each query.get() of 10 objects into a Task object and then pass them one by one to the to the Tasks's whenAllSuccess(Task... tasks).

You can also pass them directly as a list to Tasks's whenAllSuccess(Collection> tasks) method.

Upvotes: 4

Doug Stevenson
Doug Stevenson

Reputation: 317467

With your current document structure, it's not possible to perform the query you want. Firestore does not allow queries for individual fields of objects in list fields.

What you would have to do is create an additional field in your document that is queryable. For example, you could create a list field with only the list of string languages that are part of the document. With this, you could use an array-contains query to find the documents where a language is mentioned at least once.

For the document shown in your screenshot, you would have a list field called "languages" with values ["Swift", "Kotlin"].

Upvotes: 2

Related Questions