Query by array item and other fields at once in Firestore - firebase

I had to rephrase this question since it was a bit misleading (my fault).
Here is my dilemma, let's say I have a party collection:
parties {
status: "open",
invitees: [56486,68978,897650], # user ids of invited users
scheduled_at: 1948089050 # timestamp
}
I'd like to query only "open" parties, that I'm invited to (my user id in the invitees array), and sorted by scheduled_at
I could solve the first part of querying the array by turning it into a hash (thanks to #renaud and #james poag):
parties {
status: "open",
invitees: {
56486: true,
68978: true,
897650: true
}
scheduled_at: 1948089050
}
Now performing this:
db.collection('parties').where('status', '==', 'open').where('invitees.56486', '==', true').orderBy('scheduled_at')
Results in a firebase error asking me to make a composite index for status + invitees.56486 + scheduled_at. as you can see it's impractical for me to add an index for each user id.
Any ideas?

It looks like you're trying to make a query against a schema that doesn't really support that query. You're going to have to adjust your schema (possibly duplicating data between collections) to support your intended query. This sort of practice is normal for NoSQL type databases.
You're going to need a new collection that relates a single party with a single invitee, one for each combination, that effectively serves as a "join" between them:
party-invitees
- party_id
- party_status ("open")
- party_scheduled_at
- attendee_id
Now you can find out which open parties an attendee is invited to:
db.collection('party-invitees')
.where('party_status', '==', 'open')
.where('attendee_id', '==', 'whatever')
.orderBy('party_scheduled_at')
Bear in mind that you'll have to change this collection along with any other collections with the same data as they change. Fortunately, batch writes and transactions make this easier to do atomically.

Related

Firebase - get posts without which I voted

I have this data model.
/tests/testId
{
"photos":{
79075240-f6c3-11ea-9d76-c328c656dbfc:{
"url":"",
"votes":0
},
7a394290-f6c3-11ea-bd51-5d216a9dfad9:{
"url":"urlperPhoto"
"votes":0
}
},
"moderated":false,
"owner":o8SIEjIByyaNciEgCFH5Kfh4ngh2,
"active":false,
"votes":0
}
/tests/testId/votes
{
photoId: 'xxx',
birthday: null,
sex: false,
votedDate: null
}
I would like to get a list of posts without which I voted. Because I have voted in other collections so I can add additional field for the post model.
Example:
votedUsers: [user1, user2, user3] or votedUsers: {user1: true, user2: true}
But... I don't have in firebase filter like "not exists". How can I display posts for the user, without this which he voted?
This sort of query is not possible with Firestore, as there are no indexes for data that doesn't exist. You can only query for data that does exist, and is indexed. This means that you will need to execute one query to get some possible items that the user has not voted on, then compare that to the results of another query that checks to see if that user has voted on, and remove those from the result set. Yes, this is difficult, and potentially expensive. But this is just not the sort of problem that Firestore is good at.
You might want to consider using another data along with Firestore in order to maintain this sort of relationship between users and things they have not yet seen or done. (It just won't scale like Firestore.)
See also:
Firebase Firestore Structure for getting un-seen trending posts - Social
How to query Cloud Firestore for non-existing keys of documents

Is it possible to fetch all documents whose sub-collection contains a specific document ID?

I am trying to fetch all documents whose sub-collection contain a specific document ID. Is there any way to do this?
For example, if the boxed document under 'enquiries' sub-collection exists, then I need the boxed document ID from 'books' collection. I couldn't figure out how to go backwards to get the parent document ID.
I make the assumption that all the sub-collections have the same name, i.e. enquiries. Then, you could do as follows:
Add a field docId in your enquiries document that contains the document ID.
Execute a Collection Group query in order to get all the documents with the desired docId value (Firestore.instance.collectionGroup("enquiries").where("docId", isEqualTo: "ykXB...").getDocuments()).
Then, you loop over the results of the query and for each DocumentReference you call twice the parent() methods (first time you will get the CollectionReference and second time you will get the DocumentReference of the parent document).
You just have to use the id property and you are done.
Try the following:
Firestore.instance.collection("books").where("author", isEqualTo: "Arumugam").getDocuments().then((value) {
value.documents.forEach((result) {
var id = result.documentID;
Firestore.instance.collection("books").document(id).collection("enquiries").getDocuments().then((querySnapshot) {
querySnapshot.documents.forEach((result) {
print(result.data);
});
First you need to retrieve the id under the books collection, to be able to do that you have to do a query for example where("author", isEqualTo: "Arumugam"). After retrieving the id you can then do a query to retrieve the documents inside the collection enquiries
For example, if the boxed document under 'enquiries' sub-collection exists, then I need the boxed document ID from 'books' collection.
There is no way you can do that in a single go.
I couldn't figure out how to go backwards to get the parent document ID.
There is no going back in Firestore as you probably were thinking. In Firebase Realtime Database we have a method named getParent(), which does exactly what you want but in Firestore we don't.
Queries in Firestore are shallow, meaning that it only get items from the collection that the query is run against. Firestore doesn't support queries across different collections in one go. A single query may only use the properties of documents in a single collection. So the solution to solving your problem is to perform two get() calls. The first one would be to check that document for existence in the enquiries subcollection, and if it exists, simply create another get() call to get the document from the books collection.
Renaud Tarnec's answer is great for fetching the IDs of the relevant books.
If you need to fetch more than the ID, there is a trick you could use in some scenarios. I imagine your goal is to show some sort of an index of all books associated with a particular enquiry ID. If the data you'd like to show in that index is not too long (can be serialized in less than 1500 bytes) and if it is not changing frequently, you could try to use the document ID as the placeholder for that data.
For example, let's say you wanted to display a list of book titles and authors corresponding to some enquiryId. You could create the book ID in the collection with something like so:
// Assuming admin SDK
const bookId = nanoid();
const author = 'Brandon Sanderson';
const title = 'Mistborn: The Final Empire';
// If title + author are not unique, you could add the bookId to the array
const uniquePayloadKey = Buffer.from(JSON.stringify([author, title])).toString('base64url');
booksColRef.doc(uniquePayloadKey).set({ bookId })
booksColRef.doc(uniquePayloadKey).collection('enquiries').doc(enquiryId).set({ enquiryId })
Then, after running the collection group query per Renaud Tarnec's answer, you could extract that serialized information with a regexp on the path, and deserialize. E.g.:
// Assuming Web 9 SDK
const books = query(collectionGroup(db, 'enquiries'), where('enquiryId', '==', enquiryId));
return getDocs(books).then(snapshot => {
const data = []
snapshot.forEach(doc => {
const payload = doc.ref.path.match(/books\/(.*)\/enquiries/)[1];
const [author, title] = JSON.parse(atob(details));
data.push({ author, title })
});
return data;
});
The "store payload in ID" trick can be used only to present some basic information for your child-driven search results. If your book document has a lot of information you'd like to display once the user clicks on one of the books returned by the enquiry, you may want to store this in separate documents whose IDs are the real bookIds. The bookId field added under the unique payload key allows such lookups when necessary.
You can reuse the same data structure for returning book results from different starting points, not just enquiries, without duplicating this structure. If you stored many authors per book, for example, you could add an authors sub-collection to search by. As long as the information you want to display in the resulting index page is the same and can be serialized within the 1500-byte limit, you should be good.
The (quite substantial) downside of this approach is that it is not possible to rename document IDs in Firestore. If some of the details in the payload change (e.g. an admin fixes a book titles), you will need to create all the sub-collections under it and delete the old data. This can be quite costly - at least 1 read, 1 write, and 1 delete for every document in every sub-collection. So keep in mind it may not be pragmatic for fast changing data.
The 1500-byte limit for key names is documented in Usage and Limits.
If you are concerned about potential hotspots this can generate per Best Practices for Cloud Firestore, I imagine that adding the bookId as a prefix to the uniquePayloadKey (with a delimiter that allows you to throw it away) would do the trick - but I am not certain.

Firebase Firestore: How to acess a collection from another collection

I am using Vue.js and Firebase Firestore. Now I have two collectionsusers and orders. In the orders collection, I have already stored the id of each document of user collection. I now have to fetch the details of the corresponding users from this. How am I supposed to go about it?
This is what I've done so far
let orderRef = db.collection("orders")
orderRef.onSnapshot(snapshot => {
snapshot.docChanges().forEach(change => {
if (change.type == "added") {
let doc = change.doc;
this.orders.push({
id: doc.id,
orderData: doc.data().orderData,
user_id: doc.data().user_id,
userInfo: db.collection("users").doc(user_id),
});
}
});
});
I need to store user data in userInfo.Thanks in advance
Firestore doesn't support foreign key like SQL database, so you can't retrieve nested data like SQL.
In Firestore you need to fetch referenced data separately, either you can fetch all users data separately in parallel with orders data and store it in map, or if you don't need users data initially then fetch each users data when needed like when you check details of each order.
I think that you're structuring your data as if you were working in a relational database. Firestore is a no-SQL database that doesn't have any notion of reference, the only thing Firestore understands are key-values, documents and collections, everything else has to me modeled on top of that. See Firestore data model.
In Firestore relationships are usually modeled by storing the id of the document you'd like to "reference". In that sense you might not need to store the 'users' document inside the 'order' but the field 'user_id' would suffice. The caveat is that this data layout comes at the price of having to fetch the 'user_id' from orders before you can fetch the actual user data. You could read more about data denormalization and modeling relationships in articles link1, link2 and this other thread.
Also, it's worth noting that Firestore documents are limited in size to 1MB so with your actual configuration if the amount of info of 'user' documents increases it may get to a point where it would be necessary to reshape your documents structure.
All in all, if you don't want to change your data layout you would need to follow Ked suggestions and first retrieve the 'users' data to inline it into the 'userInfo' field.

Firebase database check if element exists in a ListField in Flutter

I have a real-time database on firebase which consists of ListFields. Among these fields, one field, participants is a list of strings and two usernames. I want to make a query to firebase database such that it will return the documents in which a particular username is present in the participants list.
The structure of my document is as follows :
I want to make a query such that Firebase returns all the documents in which the participants list consists aniruddh. I am using Flutter with the flutterfire plugins.
Your current data structure makes it easy to find the participants for a conversation. It does however not make it easy to find the conversations for a user.
One alternative data structure that makes this easier is to store the participants in this format:
imgUrls: {},
participants: {
"aniruddh": true,
"trubluvin": true
}
Now you can technically query for the the conversations of a user with something like:
db.child("conversations").orderByChild("participants/aniruddh").equalTo(true)
But this won't scale very well, as you'll need to define an index for each user.
The proper solution is to add a second data structure, known as an inverted index, that allows the look up of conversations for a user. In your case that could look like this:
userConversations: {
"aniruddh": {
"-LxzV5LzP9TH7L6BvV7": true
},
"trubluvin": {
"-LxzV5LzP9TH7L6BvV7": true
}
}
Now you can look up the conversations that a user is part of with a simple read operation. You could expand this data structure to contain more information on each conversation, such as the information you want to display in your list view.
Also see my answer heres:
Firebase query if child of child contains a value (for more explanation on why the queries won't work in your current structure, and why they won't scale in the first structure in my answer).
Best way to manage Chat channels in Firebase (for an alternative way of naming the chat rooms).

How to query by item existance in array on Firestore? [duplicate]

This question already has answers here:
Firestore: Query by item in array of document
(5 answers)
Closed 4 years ago.
I have read this https://firebase.google.com/docs/firestore/solutions/arrays but I still couldn't figure out how it solves the issue of querying not-hardcoded user-generated items in array.
Let's say I have a Party collection:
Party {
name: "Birthday party",
invitees: [56448, 869987, 230232, 202030] # user id's of invited people
}
How can I query only parties where I'm in the invitees array without making an index for each possible ID?
https://firebase.google.com/docs/firestore/solutions/arrays
Limitations
The solution shown above is a great way to simulate array-like structures in Cloud Firestore, but you should be aware of the following limitations:
Indexing limits - A single document can have only 20,000 properties in order to use Cloud Firestore built-in indexes. If your array-like data structure grows to tens of thousands of members, you may run into this limit.
So the short answer is no.
If I were using Firebase Database, I would create a cloud function to watch the party/invitees branch on creation and modification, then propagate the invitation to a branch on the userID profile. Because you can be invited and uninvited, the changeset contains old and new and you can remove invitations from people who are in the previous but not in the next.
I haven't explored Cloud Functions for Firestore (yet) but I suspect that you can do something similar.
If you follow the method described in the documentation you point to (i.e. https://firebase.google.com/docs/firestore/solutions/arrays) and instead of storing the users in an array you save an object like
Party {
name: "Birthday party",
invitees: {
"56448": true,
"869987": true,
"230232": true,
.....
}
}
you will be able to query like
var yourUserID = .....; //e.g. '869987'
db.collection('invitees')
.where('invitees.' + yourUserID, '==', true)
.get()
.then(() => {
// ...
});
And as the doc says "This technique relies on the fact that Cloud Firestore creates built-in indexes for all document fields, even fields in a nested map.", so no worries for maintaining the indexes.

Resources