0

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.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Gowtham K K
  • 3,123
  • 1
  • 11
  • 29

2 Answers2

4

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.

Alex Mamo
  • 130,605
  • 17
  • 163
  • 193
  • Btw, there is a typo in your database, the first object in the array has a property `lan` and **not** `lang`. – Alex Mamo Apr 04 '20 at 12:24
  • Sorry that was a typo mistake. Actually this a fake mockup of similar structure to ask a question ( Privacy of my data) – Gowtham K K Apr 04 '20 at 12:47
  • Oh, I understand. No worries, this solution will work with any kind of data. Have you tried it? – Alex Mamo Apr 04 '20 at 12:49
  • I have tried this approach before asking question. But I dont want to query the years. In your approach it provides the result only if years is "1" and lang is "Swift" right? But I want all the jobs if array contains "swift". Your approach avoids data duplication.But I have confusion on this.If i send null data of "years" and "swift" as "lang" will it be queried? If I have tried Dough's answers and its working. I think in javascript ,there is an option to query this type of data. Thanks for your suggestion. – Gowtham K K Apr 04 '20 at 13:00
  • 1
    I see your concern but there is actually a very simple solution for that. So please see my updated answer with an approach that will help you avoid duplicating data. See also Firestore document limits. – Alex Mamo Apr 04 '20 at 13:27
  • Your answer is correct and works for this mock data. I accept yours. But the problem is ,in my original structure Experience class have more than 6 fields. So it's not possible for me create that much list of Experience objects. Thanks for the update.I'll use this approach in another case. – Gowtham K K Apr 04 '20 at 14:06
  • It works with two properties, it will work as well with six or ten. In my answer, I have used only two, to keep things simple. I don't know the use-case of your app but usually, those objects come from an operation. For example, if I need to display a list of `Experience` objects, once a user selects one or more objects, I save them to a list and then I perform the query against "Jobs" collection, right? – Alex Mamo Apr 04 '20 at 14:29
  • I will try to explain you. If the years may be of 0-30 years . So If I need to query the experience with "Swift" should I create a least of objects which has years 0-30? Like wise I has couple of fields. How can I add multiple fields. I need to query all Jobs with "Swift" not "Swift" with "1 year,2 years" like wise .... – Gowtham K K Apr 04 '20 at 14:42
  • In that case, please see again my updated answer. You cannot query using just "Swift". Since that array holds objects, you should pass objects to create a match between them. – Alex Mamo Apr 04 '20 at 14:59
  • Updated answer seems to be good for me. I will try this approach.Thanks – Gowtham K K Apr 04 '20 at 15:08
  • You didn't provide the entire context from the beginning but I hope it's ok now. – Alex Mamo Apr 04 '20 at 15:19
  • Sure .It will be appreciated. – Gowtham K K Apr 05 '20 at 16:56
  • Oh sorry my bad. The doubt if I had is if we store the reference in an array and read the data by using that reference, will the read counts be calculated extra for that or the same . – Gowtham K K Apr 05 '20 at 17:04
2

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"].

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
  • Thank you for quick answer. But I need to similar kind of structure where experience is inside document. If I use nested collection , can I query the particular data easily? – Gowtham K K Apr 03 '20 at 19:01
  • It's never "hard" to query a subcollecction. They work just like top-level collections. Now, to be clear, in my answer I'm not suggesting that you change what you have now. I'm suggesting that you just add new data to it for the purpose of querying. This is extremely common. – Doug Stevenson Apr 03 '20 at 19:16
  • I will add new data as normal array as your suggestion. It seems quite simple.Thanks – Gowtham K K Apr 03 '20 at 19:20