5

Data structure:

houses (collection)
  name (string)
  users (map)
    90c234jc23 (map)
      percentage: 100% (string/number)

Rules:

allow read: request.auth.uid in resource.data.users;

The problem is when I try to query houses which user owns:

FirebaseFirestore.getInstance().collection(House.COLLECTION)
//                .whereArrayContains(House.USERS_FIELD, currentUser.getUid()) // does not work
                .whereEqualTo("users." + currentUser.getUid(), currentUser.getUid()) // does not work either
                .get()

No result are returned.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
Jaky
  • 51
  • 1
  • 1
  • 2
  • Is this really a firestore database? I cannot find whereArrayContains nor whereEqualTo in the docs. If it is not firestore, then perhaps the google-cloud-firestore tag should be removed. And potentially instead add the actual tech-tag (realtime?). – Spiralis Aug 28 '19 at 11:02
  • 2
    this is definitely Firestore. https://firebase.google.com/docs/firestore/query-data/queries – Jeff Padgett Sep 08 '19 at 05:00

3 Answers3

12

You cannot perform this type of query in firestore as there is no 'map-contains-key' operator. However, there are very simple workarounds for implementing this by making slight adjustments to your datastructure.

Specific Solution

Requirement: For this solution to work, each map value has to be uniquely identifyable in a firestore query, meaning it cannot be a map or an array.

If your case meets the listed requirements, you can go with @Dennis Alund's solution which suggests the following data structure:

{
  name: "The Residence",
  users: {
    uid1: 80,
    uid2: 20
  }
}

General Solution

If your map values are maps or arrays, you need to add a property to each value which will be constant across all created values of this type. Here is an example:

{
  name: "The Residence",
  users: {
    uid1: {
      exists: true,
      percentage: 80,
      ...
    },
    uid2:  {
      exists: true,
      percentage: 20,
      ...
    },
  }
}

Now you can simply use the query:

_firestore.collection('houses').whereEqualTo('users.<uid>.exists', true)

Edit:

As @Johnny Oshika correctly pointed out, you can also use orderBy() to filter by field-name.

Lucas Aschenbach
  • 862
  • 1
  • 11
  • 17
  • How could I do it in Android ?? https://stackoverflow.com/questions/69494502/multiple-where-clause-in-firestore-on-map-in-android – Zar E Ahmer Oct 08 '21 at 10:45
  • @AndroidGeek The data structure is the same. The query might have a slightly different syntax with the Android library but still, the filter condition in the query remains as 'where `exists` equals `true`' – Lucas Aschenbach Oct 08 '21 at 10:53
  • 1
    This first statement: `You cannot perform this type of query in firestore as there is no 'map-contains-key' operator` is incorrect. See my solution below on how to do this. – Johnny Oshika Apr 01 '22 at 20:17
11

You can use orderBy to find documents where map contains a certain key. Using this example document:

{
  "users": {
    "bob": {},
    "sam": {},
  }
}

.orderBy('users.bob') will only find documents that contain users.bob.

Johnny Oshika
  • 54,741
  • 40
  • 181
  • 275
9

This query is not working because your users field is a map and not an array.

.whereArrayContains(House.USERS_FIELD, currentUser.getUid())

This query

.whereEqualTo("users." + currentUser.getUid(), currentUser.getUid())

is not working because your map value for users.<uid> is a string that says percentage: xx% and that statement is testing if percentage: xx% === <uid>, which is false.

And that strategy will be problematic since you can not do queries to find items that "are not null" or "strings not empty", etc.

I'm assuming that the percentage is the user's ownership in the house (?). If so, you might have better luck in trying to structure your house document data like this if you want to maintain the same structure of document as in your question

{
  name: "The Residence",
  users: {
    uid1: 80,
    uid2: 20
  }
}

That will allow you to do a query such as

.whereGreaterThan("users." + currentUser.getUid(), 0)

to find users that has some shares of ownership in that house.

But a fair bit of warning, as soon as you need composite indexes you will start having problems to maintain that structure. You might instead want to consider storing an array of users that owns that house for ease of querying.

Dennis Alund
  • 2,916
  • 1
  • 13
  • 34
  • 3
    Great explanation Dennis. I'd indeed add an **additional** field `owners` that is just an array of the owner UIDs, so that you can use `whereArrayContains` on that. – Frank van Puffelen Jan 03 '19 at 14:43
  • Hi. Nice Answer. Is the "as soon as you need composite indexes you will start having problems" part still true? Perhaps there is something like "map-contains" or "map-contains-key" index (type) (analogy to array-contains)? Either available now or in the works? Thank You. – KarolDepka Apr 10 '20 at 03:28
  • @Frank : So this helper array would be an emulation of a phantasized "map-contains-key" – KarolDepka Apr 10 '20 at 03:38
  • Yup, that's pretty much it. Firestore creates indexes for full fields, not for parts of them. So the `owners` field extracts the parts of the field that you want to query on. – Frank van Puffelen Apr 10 '20 at 15:11
  • RE "as soon as you need composite indexes you will start having problems", why is that the case? – nbransby May 14 '20 at 13:11
  • Because user ids are dynamically created and if you are having composite indexes on dynamic values you will end up with an infinitely growing number of indexes. If you are using array contains, you don't have a separate composite index for each UID. – Dennis Alund May 15 '20 at 02:34