I tried to access the subcollection 'match' using docs map.
final firestore = FirebaseFirestore.instance.collection('time').get();
firestore.docs.map((e) {
DateTime dt = e.data()['time'];
e.collection('match'); // How to access its subcollection
})
How can I access the subcollection 'match' on each document and at the same time accessing the 'time' field.
Your firestore variable is a Future<QuerySnapshot> object, and you're missing an await or then to wait for the future to resolve:
final firestore = FirebaseFirestore.instance.collection('time').get();
final snapshot = await firestore;
You can then access a subcollection of each time document with:
snapshot.docs.map((doc) {
...
var matchCollection = doc.reference.collection('match');
})
You might also want to look at using a collection group query to read/query all match collections in one go.
Related
I am looking to fetch the data of collection named users which has documents with an id of logged-in users' uid. Further, these documents contain some data and a subCollection called posts.
which looks like -
So now, I need to fetch all four(4) of these documents along with the posts collection data together so that I can display it.
My approach -
( here I fetched the document ids - middle section of image IDs)
// Fetching Firestore Users Document IDs
const [userDocs, setUserDocs] = React.useState([]);
React.useEffect(() => {
try {
const data = firestore.collection('users')
.onSnapshot(snap => {
let docIDs = [];
snap.forEach(doc => {
docIDs.push({id: doc.id});
});
setUserDocs(docIDs);
})
}
catch(err) {
console.log(err);
}
}, [])
Now, I have tried to fetch the entire data using the following way (which isn't working)
// Fetching Firestore Posts Data
const [postData, setPostData] = useState([]);
React.useEffect(() => {
try {
userDocs.map(data => {
const data = firestore.collection('users/'+currentUser.uid+'/posts')
.onSnapshot(snap => {
let documents = [];
snap.forEach(doc => {
documents.push({...doc.data(), id: doc.id});
});
setPostData(documents);
})
})
}
catch(err) {
console.log(err);
}
}, [])
Finally, I should end up with postData array which I can map on my card component to render all posted images and captions to the UI.
I am not sure if this is the right way to achieve what I am doing here, please help me correct this error and if there's a more subtle and easy way to do it please let me know. Thank You.
I have tried to fetch the entire data
Looking at the code you wrote for fetching "the entire data" (i.e. the second snippet) it seems that you don't need to link a post document to the parent user document when fetching the post documents. In other words, I understand that you want to fetch all the posts collection independently of the user documents.
Therefore you could use a Collection Group query.
If you need, for each post document returned by the Collection Group query, to get the parent user doc (for example to display the author name) you can do as explained in this SO answer, i.e. using the parent properties.
I am building an app using Firebase Firestore as a BaaS.
But I am facing a problem when I try to create a feed/implement full-text-search on my app.
I want to be able to search through all the users posts, the problem is, the users posts are structured like this in the Firestore Database:
Posts(collection) -> UserID(Document) -> user posts(subcollection that holds all userID posts) -> actual posts(separate documents within that collection)
I want to loop through every user's user posts subcollection and fetch all data for the feed, and also to implement it with a full text search app like Algolia or ES.
I can loop through a specific user ID(code below), but being a beginner, I couldn't find a way to loop through all of them and fetch all of them.
firebase.firestore()
.collection('allPosts')
.doc('SPECIFIC_USER_ID') //-> Here I have to loop through all docs in that collection
.collection('userPosts')
.orderBy("creation", "asc")
.get()
.then((snapshot) => {
let posts = snapshot.docs.map(doc => {
const data = doc.data();
const id = doc.id;
return { id, ...data }
})
setUserPosts(posts)
})
}
Would love some help!
Collection Group Query
You can query in all collections named X using a collection group query.
var posts= db.collectionGroup('userPosts').orderBy('creation').limit(10);
posts.get().then((querySnapshot) => {
let posts = querySnapshot.map(doc => {
const data = doc.data();
const id = doc.id;
return { id, ...data }
})
setUserPosts(posts)
});
Source: https://firebase.google.com/docs/firestore/query-data/queries#collection-group-query
Algolia implementation
You will need to use Cloud Functions to migrate fields to a dedicated collection specifically for Algolia. Many users have found nested SubCollections to be problematic with Algolia's setup.
You do this by duplicating the user Post data as a 'source' to this new public collection, and using the Firebase Algolia Extension, you can sync it directly
exports.bakePosts= functions.firestore
.document('allPosts/{userID}/userPosts/{postID}')
.onWrite((change, context) => {
// Get an object with the current document value.
// If the document does not exist, it has been deleted.
const document = change.after.exists ? change.after.data() : null;
// Get an object with the previous document value (for update or delete)
const oldDocument = change.before.data();
if(document != null)
db.collection("posts/"+ context.params.postID).set(document);
if(document == null)
db.collection("posts/"+ context.params.postID).delete();
});
Algolia Extension:
https://firebase.google.com/products/extensions/firestore-algolia-search
You can avoid most of the above if you simply submit posts to a master collection and have the userID as the 'owner' property within the document. The above also have benefits, but more related to blog posts where users may have a "work in progress" version vs Live.
The Algolia Extension has the full guide on how to set it up and if you need to customize the extensions, the source code is also available.
I have a 100 user documents in database, and I have to update one field to 0 in all documents. How do we do it?
As #Doug mentioned there's no direct way, you'll have to query the data, get the DocumentReference from QueryDocumentSnapshot and invoke update on it.
var collection = FirebaseFirestore.instance.collection('collection');
var querySnapshots = await collection.get();
for (var doc in querySnapshots.docs) {
await doc.reference.update({
'single_field': 'newValue',
});
}
Firestore doesn't offer a SQL-like "update where" command that updates everything at once, so you will have to:
Query for the documents to update
Iterate each DocumentSnapshot and get its DocumentReference object using the reference property
For each document, update the field with its new value
Try doing the following
Query the documents to update
Go through each DocumentSnapshot and get its DocumentReference object using the reference property and for each document, update the field with its new value
void updateNotificationCount() async {
FirebaseUser _currentUser = await FirebaseAuth.instance.currentUser();
String authid = _currentUser.uid;
var snapshots =
activityFeedRef.document(authid).collection('feedItems').snapshots();
try {
await snapshots.forEach((snapshot) async {
List<DocumentSnapshot> documents = snapshot.documents;
for (var document in documents) {
await document.reference.updateData(<String, dynamic>{
'seen': true,
});
}
});
} catch (e) {
print(e.toString());
}
}
The code above simply updates all unread notification from false to true immediately the user clicks on the notification tray
Every day I am importing products from external retailers into a Google Cloud Firestore database.
During that process, products can be either new (a new document will be added to the database) or existing (an existing document will be updated in the database).
Should be noted that I am importing about 10 million products each day so I am not querying the database for each product to check if it exists already.
I am currently using set with merge, which is exactly what I need as it creates a document if it doesn't exist or updates specific fields of an existing document.
Now the question is, how could I achieve a createdAt timestamp given that provided fields will be updated, therefore the original createdAt timestamp will be lost on update? Is there any way to not update a specific field if that field already exists in the document?
I suggest that you use a Cloud Function that would create a new dedicated field when a Firestore doc is created. This field shall not be included in the object you pass to the set() method when you update the docs.
Here is a proposal for the Cloud Function code:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.productsCreatedDate = functions.firestore
.document('products/{productId}')
.onCreate((snap, context) => {
return snap.ref.set(
{
calculatedCreatedAt: admin.firestore.FieldValue.serverTimestamp()
},
{ merge: true }
)
.catch(error => {
console.log(error);
return false;
});
});
Based on Bob Snyder comment above, note that you could also do:
const docCreatedTimestamp = snap.createTime;
return snap.ref.set(
{
calculatedCreatedAt: docCreatedTimestamp
},
{ merge: true }
)
.catch(...)
in version firebase 9 the correct way is:
import { serverTimestamp } from "firebase/firestore";
....
return snap.ref.set(
{
calculatedCreatedAt: serverTimestamp();
.....
Is it possible to implement reactivity by a sub-class with a transformed collection?
This is the example code of jamgold on the Meteor forum; the sub-class subcollection is joined to the main-class collection_name. If something changes on the collection_name collection, Meteor is in fact reactive. However, when something changes on the sub-collection subcollection, this is not reactively pushed to this publish/subscription.
Collection = new Meteor.Collection('collection_name');
if(Meteor.isServer)
{
Meteor.publish('collection', function(query,options) {
var self = this;
var handler = null;
query = query == undefined ? {} : query;
options = options == undefined ? {} : options;
//
handler = Collection.find(query,options).observeChanges({
added: function (id, doc) {
var object = null;
doc.object = Meteor.subcollection.findOne({_id: doc.objectId},);
self.added('collection_name', id, doc);
},
changed: function (id, fields) {
self.changed('collection_name', id, fields);
},
removed: function (id) {
self.removed('collection_name', id);
}
});
self.ready();
self.onStop(function () {
if(handler) handler.stop();
});
});
}
if(Meteor.isClient)
{
Meteor.subscribe('collection');
}
To make it reactive for the SubCollection, you would need to also observe it's changes as well. Keep in mind that this becomes very complex fast and my example only works if there is a 1 to 1 relationship between your Collection and SubCollection. You could implement something that works for a 1 to many relationship, but you will have some logic issues to address (e.g. when a doc in SubCollection changes...does that invalidate all associated Collection docs that were already published with that SubCollection doc. If so then do you emit a removed then an added to re-send them with their updated SubCollection doc, etc.).
Here is the full example.
const Collection = new Meteor.Collection('collection_name');
const SubCollection = new Meteor.Collection('sub_collection_name');
if (Meteor.isServer) {
Meteor.publish('collection', function(query,options) {
var self = this;
var handler = null;
query = query == undefined ? {} : query;
options = options == undefined ? {} : options;
// enable reactivity for Collection
handler = Collection.find(query, options).observeChanges({
added: function (id, doc) {
// find the associated object (using it's id) and add it to the doc
doc.object = SubCollection.findOne({_id: doc.objectId});
// now pass the original doc + the associated object down to client
self.added('collection_name', id, doc);
},
changed: function (id, fields) {
// doc.object is assumed to already exist on the doc...so just notify the subscriber
// of the changes in Collection
self.changed('collection_name', id, fields);
},
removed: function (id) {
// doc.object is assumed to already exist on the doc...so just notify the subscriber
// of the changes in Collection
self.removed('collection_name', id);
}
});
var handleSubCollectionDocChange = function(callbackThis, id) {
// find the doc from Collection that has a reference to the new SubCollection doc
var parentCollectionDoc = Collection.findOne({objectId: id});
// only do something if one exists
if (parentCollectionDoc) {
// remove the previously published doc since the SubCollection doc changed (if it was previously published)
self.removed('collection_name', parentCollectionDoc._id);
// store the new SubCollection doc in Collection.object
parentCollectionDoc.object = doc;
// send down the Collection doc (with new SubCollection doc attached)
self.added('collection_name', parentCollectionDoc._id, parentCollectionDoc);
}
};
// enable reactivity for SubCollection
subhandler = SubCollection.find().observeChanges({
added: function (id, doc) {
// find the doc from Collection that has a reference to the new SubCollection doc
var parentCollectionDoc = Collection.findOne({objectId: id});
// only do something if one exists
if (parentCollectionDoc) {
// remove the previously published doc since the SubCollection doc changed (if it was previously published)
self.removed('collection_name', parentCollectionDoc._id);
// store the new SubCollection doc in Collection.object
parentCollectionDoc.object = doc;
// send down the Collection doc (with new SubCollection doc attached)
self.added('collection_name', parentCollectionDoc._id, parentCollectionDoc);
}
},
changed: function (id, fields) {
// get the full SubCollection doc (since we only get the fields that actually changed)
var doc = SubCollection.findOne({_id: id});
// find the doc from Collection that has a reference to the new SubCollection doc
var parentCollectionDoc = Collection.findOne({objectId: id});
// only do something if one exists
if (parentCollectionDoc) {
// remove the previously published doc since the SubCollection doc changed (if it was previously published)
self.removed('collection_name', parentCollectionDoc._id);
// store the new SubCollection doc in Collection.object
parentCollectionDoc.object = doc;
// send down the Collection doc (with new SubCollection doc attached)
self.added('collection_name', parentCollectionDoc._id, parentCollectionDoc);
}
},
removed: function (id) {
// find the doc from Collection that has a reference to the new SubCollection doc
var parentCollectionDoc = Collection.findOne({objectId: id});
// only do something if one exists
if (parentCollectionDoc) {
// remove the previously published doc since the SubCollection doc no longer exists (if it was previously published)
self.removed('collection_name', parentCollectionDoc._id);
}
}
});
self.ready();
self.onStop(function () {
if (handler) handler.stop();
if (subhandler) subhandler.stop();
});
});
}
With that said, if you are only trying to achieve reactive joins then you really should look into the Meteor Publish Composite package. It handles reactive joins very easily and will keep your publication up to date with the parent collection changes or any of the child collections change.
Here is what a publication would look like (based on your example) using publish composite.
const Collection = new Meteor.Collection('collection_name');
const SubCollection = new Meteor.Collection('sub_collection_name');
Meteor.publishComposite('collection', function(query, options) {
query = query == undefined ? {} : query;
options = options == undefined ? {} : options;
return {
find: function() {
return Collection.find(query,options);
},
children: [{
find: function(collectionDoc) {
return SubCollection.find({_id: collectionDoc.objectId});
}
}],
};
});
With this example, anytime Collection or associated SubCollection docs change they will be sent to the client.
The only gotcha with this approach is that it publishes the docs into their respective collections. So you would have to perform the join (SubDocument lookup) on the client. Assuming we have subscribed to the above publication and we wanted to get a SubCollection doc for a certain Collection doc on the client, then it would look like this.
// we are on the client now
var myDoc = Collection.findOne({ //..search selector ..// });
myDoc.object = SubCollection.findOne({_id: myDoc.objectId});
The composite publication ensures that the latest SubCollection doc is always on the client. The only problem with the above approach is that if your SubCollection doc changes and is published to the client, your data will be stale because you have stored an static (and unreactive) version of the SubCollection doc in myDoc.object.
The way around this is to only perform your join when you need it and don't store the results. Or, another option is to use the Collection Helpers package and create a helper function that dynamically does the join for you.
// we are on the client now
Collection.helpers({
object: function() {
return SubCollection.findOne({_id: myDoc.objectId});
},
});
With this helper in place, anytime you need access to the joined SubCollection doc you would access it like this.
var myDoc = Collection.findOne({ //..search selector ..// });
console.dir(myDoc.object);
Under the covers, the collection helper does the SubCollection lookup for you.
So long story short, take your pick (roll your own reactive join publication or use Publish Composite + Collection Helpers). My recommendation is to use the packages because it's a tried and true solution that works as advertised out of the box (fyi...I use this combination in several of my Meteor apps).