How to query a document and sub-document together Firestore? - firebase

My data structure looks something like this:
- posts (collection)
- post (document)
title
description
labels
vote counts
author
author choice
- votes (collection)
- vote (document)
choice
I need to write a query so that I find a post under the collections document where author != the user's UID, author choice == null, and category == any of some values (that I have no trouble with) but also where a document with the user's UID doesn't exist in the votes sub-collection.

Firestore queries can only filter on values in the documents that they return. There is no way to have a condition on documents from a subcollection of those documents.
My preferred solution is to include information about the existence of the UIDs that exist in the votes subcollection in the parent document too, update that with every vote that is written, and then query on that new field.
Firestore queries also can only query on values that are present in a document. Values that don't exist are not in any index, so can't be part of a query condition.
That is tricky here as you want to test for the absence of a value. I also suspect that your list of UIDs is dynamic, so you'd have to update every parent document whenever a user is added.
So alternatively, you will have to retrieve the documents regardless of the existence in votes and then perform the additional filtering in your application code.

Related

Flutter cloud firestore : query document->map->array

My db design is above picture. I wanna create a query which returns user where tags are matched. But i didnt any solution to query.
This is my flutter code:
But it doesnt work. How can i query array of map of document?
The courses is an array and not a map so you cannot use the dot notation to query. If the courses is made a collection (or a sub-collection) on it's own then you would be able to query users easily:
users -> {userId}
(col) (doc)
courses -> {courseId}
(col) (doc)
You would have to include a field userId in each course document which would be used to identify which user owns that course.
await firestore.collection("courses").where("tags", arrayContainsAny: tagKeys)
This will return all courses where the tags array contains at least 1 item in the tagKeys list. If you need exact match i.e. all the tags in tagKeys must be present in Firestore document then you would have to restructure the database as mentioned in this answer.
Fetching all matching documents might not be ideal since you just need user IDs that matches the tags. In that case you can store a field which contains tags from all the courses in a single array field in the user document.

how do I do a collectionGroup query but get the documents that subcollection is inside of?

how do I do a collectionGroup query but get the documents that subcollection is inside of. My data is structured as: Users (collection) -> UID(doc) -> privateData (collection) -> UID (doc)
and I want to query based on a field in the UID docin the privateData collection, but I want the actual data from the UID Doc in the Users collection
You can't query across multiple collections like this. If you want data from a different subcollection, then your collection group query will need to target that other subcollection instead. This means you would probably have to duplicate the fields from privateData collection into the Users collection. Duplication of field data is common in nosql databases like Firestore.
If you can't duplicate that data for whatever reason (such as privacy), then you would need to query each related document separately, effectively joining each users doc to the matching doc in privateData.
Depending on your data model, you would have to have a reference to the parent document for the sub collection in the child object, or fetch the document and using the documents snapshot, you can call its parent reference
something like this: querySnapshot.docs()[x].snapshot.getRef().getParent()

Firebase Get document from subcollection using its ID only

Is it possible to fetch a document from a subcollection without parent document ids?
We have a structure like:
RootCollection1
-- SubCollection1
-- SubCollection2
- Document1
-- SubCollection3
-- Document2
-- SubCollection2
-- Document3 [different fields with Document1]
App User will only have the Document1 ID.
I tried implementing a collection group query. Notice that SubCollection2 name appears twice and they have different fields/values. I only want to get the document from one SubCollection2.
Is it possible to fetch the data from document1 without the parent doc id from SubCollection1 and RootCollection1.
If you can't locate a document using its full path (knowing the names of all documents and collections in the path), and you can't use a collection group query, then what you're trying to do isn't possible. You can't focus a collection group query to a specific path - they always consider all documents in all collections with the same name.
You have two viable workarounds:
Change the name of the subcollection so that it's unique and does not overlap with subcollections that should not be queried in a collection group query.
Add a field to the document so you can use it in a filter on a collection group query. You will have to be able to identify which documents you are interested in without considering all documents in all subcollections with the same name. For example, if you have a field called "scope", you can narrow the scope of your collection group query like this:
firestore.collectionGroup('coll').where('scope', '==', 'x')
Or you can store the ID of the document as a field in it, and filter for it:
firestore.collectionGroup('coll').where('id', '==', 'id')
This is all you need:
firestore.collection("Task").doc(taskId).collection("SubTask").doc(subTaskId).get();

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.

How to query for matches between users in cloud firestore?

I have a following collections
-likes (collection)
-{uid} (document)
{otheruserUID: true, anotherUID: true ...}
-likedBy (collection)
-{uid} (document)
{otheruserUID: true, anotherUID: true ...}
A user can like other users. What I want to query for is given a user, query for all matches of that user. Should I query whole likes and likedby data and run match in result and produce match results? Is there any other easy way to do this? Or may be better way to model the data?
Personally, I would simply have a single collection, called likes. Each like generates a new document with an auto-id and contains 3 fields: user (an object containing the id and name of the user), likedBy (an object containing the id and name of the user who liked them) and timestamp (when they were liked).
You'll be able to carry out the following queries:
// Find all users who liked likedUser, sorted by user
db.collection('likes').where('likedBy.name', '!=', null).where('user.id', '==', likedUser).orderBy('likedBy.name');
// Find all users who were liked by likedByUser, sorted by user
db.collection('likes').where('user.name', '!=', null).where('likedBy.id', '==', likedByUser).orderBy('user.name');
The first time that you run these queries, you will get an error, telling you to create an index. This error will include the URL to create the index for you.
The first where is required to allow the orderBy to work, see the documentation section Range filter and orderBy on the same field

Resources