How to read all nested collections of all users on firestore? [duplicate] - javascript

I thought I read that you can query subcollections with the new Firebase Firestore, but I don't see any examples. For example I have my Firestore setup in the following way:
Dances [collection]
danceName
Songs [collection]
songName
How would I be able to query "Find all dances where songName == 'X'"

Update 2019-05-07
Today we released collection group queries, and these allow you to query across subcollections.
So, for example in the web SDK:
db.collectionGroup('Songs')
.where('songName', '==', 'X')
.get()
This would match documents in any collection where the last part of the collection path is 'Songs'.
Your original question was about finding dances where songName == 'X', and this still isn't possible directly, however, for each Song that matched you can load its parent.
Original answer
This is a feature which does not yet exist. It's called a "collection group query" and would allow you query all songs regardless of which dance contained them. This is something we intend to support but don't have a concrete timeline on when it's coming.
The alternative structure at this point is to make songs a top-level collection and make which dance the song is a part of a property of the song.

UPDATE
Now Firestore supports array-contains
Having these documents
{danceName: 'Danca name 1', songName: ['Title1','Title2']}
{danceName: 'Danca name 2', songName: ['Title3']}
do it this way
collection("Dances")
.where("songName", "array-contains", "Title1")
.get()...
#Nelson.b.austin Since firestore does not have that yet, I suggest you to have a flat structure, meaning:
Dances = {
danceName: 'Dance name 1',
songName_Title1: true,
songName_Title2: true,
songName_Title3: false
}
Having it in that way, you can get it done:
var songTitle = 'Title1';
var dances = db.collection("Dances");
var query = dances.where("songName_"+songTitle, "==", true);
I hope this helps.

UPDATE 2019
Firestore have released Collection Group Queries. See Gil's answer above or the official Collection Group Query Documentation
Previous Answer
As stated by Gil Gilbert, it seems as if collection group queries is currently in the works. In the mean time it is probably better to use root level collections and just link between these collection using the document UID's.
For those who don't already know, Jeff Delaney has some incredible guides and resources for anyone working with Firebase (and Angular) on AngularFirebase.
Firestore NoSQL Relational Data Modeling - Here he breaks down the basics of NoSQL and Firestore DB structuring
Advanced Data Modeling With Firestore by Example - These are more advanced techniques to keep in the back of your mind. A great read for those wanting to take their Firestore skills to the next level

What if you store songs as an object instead of as a collection? Each dance as, with songs as a field: type Object (not a collection)
{
danceName: "My Dance",
songs: {
"aNameOfASong": true,
"aNameOfAnotherSong": true,
}
}
then you could query for all dances with aNameOfASong:
db.collection('Dances')
.where('songs.aNameOfASong', '==', true)
.get()
.then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
console.log(doc.id, " => ", doc.data());
});
})
.catch(function(error) {
console.log("Error getting documents: ", error);
});

NEW UPDATE July 8, 2019:
db.collectionGroup('Songs')
.where('songName', isEqualTo:'X')
.get()

I have found a solution.
Please check this.
var museums = Firestore.instance.collectionGroup('Songs').where('songName', isEqualTo: "X");
museums.getDocuments().then((querySnapshot) {
setState(() {
songCounts= querySnapshot.documents.length.toString();
});
});
And then you can see Data, Rules, Indexes, Usage tabs in your cloud firestore from console.firebase.google.com.
Finally, you should set indexes in the indexes tab.
Fill in collection ID and some field value here.
Then Select the collection group option.
Enjoy it. Thanks

You can always search like this:-
this.key$ = new BehaviorSubject(null);
return this.key$.switchMap(key =>
this.angFirestore
.collection("dances").doc("danceName").collections("songs", ref =>
ref
.where("songName", "==", X)
)
.snapshotChanges()
.map(actions => {
if (actions.toString()) {
return actions.map(a => {
const data = a.payload.doc.data() as Dance;
const id = a.payload.doc.id;
return { id, ...data };
});
} else {
return false;
}
})
);

Query limitations
Cloud Firestore does not support the following types of queries:
Queries with range filters on different fields.
Single queries across multiple collections or subcollections. Each query runs against a single collection of documents. For more
information about how your data structure affects your queries, see
Choose a Data Structure.
Logical OR queries. In this case, you should create a separate query for each OR condition and merge the query results in your app.
Queries with a != clause. In this case, you should split the query into a greater-than query and a less-than query. For example, although
the query clause where("age", "!=", "30") is not supported, you can
get the same result set by combining two queries, one with the clause
where("age", "<", "30") and one with the clause where("age", ">", 30).

I'm working with Observables here and the AngularFire wrapper but here's how I managed to do that.
It's kind of crazy, I'm still learning about observables and I possibly overdid it. But it was a nice exercise.
Some explanation (not an RxJS expert):
songId$ is an observable that will emit ids
dance$ is an observable that reads that id and then gets only the first value.
it then queries the collectionGroup of all songs to find all instances of it.
Based on the instances it traverses to the parent Dances and get their ids.
Now that we have all the Dance ids we need to query them to get their data. But I wanted it to perform well so instead of querying one by one I batch them in buckets of 10 (the maximum angular will take for an in query.
We end up with N buckets and need to do N queries on firestore to get their values.
once we do the queries on firestore we still need to actually parse the data from that.
and finally we can merge all the query results to get a single array with all the Dances in it.
type Song = {id: string, name: string};
type Dance = {id: string, name: string, songs: Song[]};
const songId$: Observable<Song> = new Observable();
const dance$ = songId$.pipe(
take(1), // Only take 1 song name
switchMap( v =>
// Query across collectionGroup to get all instances.
this.db.collectionGroup('songs', ref =>
ref.where('id', '==', v.id)).get()
),
switchMap( v => {
// map the Song to the parent Dance, return the Dance ids
const obs: string[] = [];
v.docs.forEach(docRef => {
// We invoke parent twice to go from doc->collection->doc
obs.push(docRef.ref.parent.parent.id);
});
// Because we return an array here this one emit becomes N
return obs;
}),
// Firebase IN support up to 10 values so we partition the data to query the Dances
bufferCount(10),
mergeMap( v => { // query every partition in parallel
return this.db.collection('dances', ref => {
return ref.where( firebase.firestore.FieldPath.documentId(), 'in', v);
}).get();
}),
switchMap( v => {
// Almost there now just need to extract the data from the QuerySnapshots
const obs: Dance[] = [];
v.docs.forEach(docRef => {
obs.push({
...docRef.data(),
id: docRef.id
} as Dance);
});
return of(obs);
}),
// And finally we reduce the docs fetched into a single array.
reduce((acc, value) => acc.concat(value), []),
);
const parentDances = await dance$.toPromise();
I copy pasted my code and changed the variable names to yours, not sure if there are any errors, but it worked fine for me. Let me know if you find any errors or can suggest a better way to test it with maybe some mock firestore.

var songs = []
db.collection('Dances')
.where('songs.aNameOfASong', '==', true)
.get()
.then(function(querySnapshot) {
var songLength = querySnapshot.size
var i=0;
querySnapshot.forEach(function(doc) {
songs.push(doc.data())
i ++;
if(songLength===i){
console.log(songs
}
console.log(doc.id, " => ", doc.data());
});
})
.catch(function(error) {
console.log("Error getting documents: ", error);
});

It could be better to use a flat data structure.
The docs specify the pros and cons of different data structures on this page.
Specifically about the limitations of structures with sub-collections:
You can't easily delete subcollections, or perform compound queries across subcollections.
Contrasted with the purported advantages of a flat data structure:
Root-level collections offer the most flexibility and scalability, along with powerful querying within each collection.

Related

How to limit number of Firebase compound indexes for querying with multiple filters

I am creating a Firestore project that contains group chats/forums where users can filter the posts based on these fields:
Number of likes
If the post contains attachments (photos and/or files)
4 Tags (Questions, Exams, Assignments, Notes)
Users can apply any or all of these filters at the same time.
Each Post document has the following fields:
Number of Clips
A different boolean for each of the tags
A list of the files in the post
The text of the post
My method for creating these queries looks like this:
setQueryFilters() {
var queryPosts = db.collection('posts').where('course_id', '==', this.course);
if (this.filterByNotes) {
queryPosts = queryPosts.where('notesTag', '==', true);
}
if (this.filterByExams) {
queryPosts = queryPosts.where('examsTag', '==', true);
}
if (this.filterByAssignments) {
queryPosts = queryPosts.where('assignmentsTag', '==', true);
}
if (this.filterByQuestions) {
queryPosts = queryPosts.where('questionsTag', '==', true);
}
if (this.filterByFiles) {
queryPosts = queryPosts.where('files', '!=', []);
}
if (this.filterByClips) {
queryPosts = queryPosts.orderBy('clips', 'desc');
} else {
queryPosts = queryPosts.orderBy('created_at', 'desc');
}
return queryPosts;
},
Since there are 6 different filters that can be applied, I have hundreds of potential different queries, and all of them (except a few) require me to create compound indexes in firebase. I'm not even sure if firebase will allow me to create this many indexes. Is there a better way to do this?
The Firebase CLI lets you deploy security rules and indexes based on a JSON file in your project workspace. You can write code to generate the JSON that describes the indexes you want to create, then deploy them with one command from the CLI. You should not have any problems creating all the required indexes.
See also:
Index definition reference
Manage indexes in Cloud Firestore
How to export security and index rules from Firestore?

Updating Query in Firestore

I am new to Firestore, need some help on firestore update.
I have following structure and wants to update "employee name" property. Not sure how to select and update.
Department:[
Name: Accounts
Employee:[
{Name :David,
Age :25},
{Name:Paul,
Age:27}
]
]
Here is what I was trying to do:
let depempCollectionRef = admin.firestore().collection('DepEmployee').doc('depempid')
depempCollectionRef.Department.Employee
.update({ name: 'Scott' },{merge:true})
.then(function() { console.log("Document successfully updated!"); })
Employee is just an embedded data structure in your Firestore document – so you can't address it through the reference directly. As far as Firestore is concerned, Employee is just an attribute on the Department document as Name is.
Before I propose a solution, let me point out two things:
If using update, you don't need {merge: true}. You use {merge: true} together with set to get an update-like behavior, if the document already exists.
I wouldn't use an Array of employees. It might make more sense to store the employees in their own collection in Firestore and then just list their reference IDs (= foreign keys) here. As a general rule of thumb: try to keep your data structure flat. Also use Arrays only, if you need to maintain a certain order of items.
A) If you have a separate collection for employees, updating the name is as easy as:
employeeCollection.doc('101').update({name: 'Scott'})
B) If you want to store employee data within your department document, I would still store them as a map with IDs (instead of an Array) and then access them with dot notation:
Department:[
Name: Accounts
Employees:{
101: {
Name :David,
Age :25
},
102: {
Name:Paul,
Age:27
}
}
]
depempCollectionRef.Department
.set({ ['101.name']: 'Scott' }, {merge:true})
C) And if you really want to store the data embedded in an Array, I believe you have to read and update the whole Array (not sure, if there is a better solution):
const employeesSnap = await depempCollectionRef.Department.get()
const updatedEmployees = changeNameOfScottInArray()
depempCollectionRef.Department
.update({ 'Employees': updatedEmployees })
I didn't test this code, but I hope you get the gist of it!
I'd recommend you flatten your data structure by creating a separate Employee collection and then just referencing them by their foreign key in your department (solution A).

Firebase pushing array - Javascript

I am using Firebase to store information for a workout application.
I user adds a workout name and then I push it to the database. I can continue pushing these but my issue is that it does not seem to be pushing as an array just an object. See the screen shots below...
As you can see in the console log picture the workouts property is an object not an array like I expect.
The code I'm using to push it:
let newWorkout = {
title: 'title',
exercises: [{
name: 'pulldownsnsn',
sets: 4
}]}
let ref = firebase.database().ref("/userProfile/"+this.userId);
ref.child("workouts").push(newWorkout);
The Firebase Database stores lists of data in a different format, to cater for the multi-user and offline aspects of modern web. The -K... are called push IDs and are the expected behavior when you call push() on a database reference.
See this blog post on how Firebase handles arrays, this blog post on the format of those keys, and the Firebase documentation on adding data to lists.
Arrays are handy, but they are a distributed database nightmare for one simple reason: index element identification is not reliable when elements get pushed or deleted. Firebase database instead uses keys for element identification:
// javascript object
['hello', 'world']
// database object
{ -PKQdFz22Yu: 'hello', -VxzzHd1Umr: 'world' }
It gets tricky when using push(), because it does not actually behaves like a normal push, but rather as a key generation followed by object modification.
Example usage
firebase.database().ref('/uri/to/list').push(
newElement,
err => console.log(err ? 'error while pushing' : 'successful push')
)
Heres an example from firebase documentation:
const admin = require('firebase-admin');
// ...
const washingtonRef = db.collection('cities').doc('DC');
// Atomically add a new region to the "regions" array field.
const unionRes = await washingtonRef.update({
regions: admin.firestore.FieldValue.arrayUnion('greater_virginia')
});
// Atomically remove a region from the "regions" array field.
const removeRes = await washingtonRef.update({
regions: admin.firestore.FieldValue.arrayRemove('east_coast')
});
More info on this firebase documentation.

Select all the fields in a mongoose schema

I want to obtain all the fields of a schema in mongoose. Now I am using the following code:
let Client = LisaClient.model('Client', ClientSchema)
let query = Client.findOne({ 'userclient': userclient })
query.select('clientname clientdocument client_id password userclient')
let result = yield query.exec()
But I want all the fields no matter if they are empty. As always, in advance thank you
I'm not sure if you want all fields in a SQL-like way, or if you want them all in a proper MongoDB way.
If you want them in the proper MongoDB way, then just remove the query.select line. That line is saying to only return the fields listed in it.
If you meant in a SQL-like way, MongoDB doesn't work like that. Each document only has the fields you put in when it was inserted. If when you inserted the document, you only gave it certain fields, that document will only have those fields, even if other documents in other collections have different fields.
To determine all available fields in the collection, you'd have to find all the documents, loop through them all and build an object with all the different keys you find.
If you need each document returned to always have the fields that you specify in your select, you'll just have to transform your object once it's returned.
const fields = ['clientname', 'clientdocument', 'client_id', 'password', 'userclient'];
let Client = LisaClient.model('Client', ClientSchema)
let query = Client.findOne({ 'userclient': userclient })
query.select(fields.join(' '))
let result = yield query.exec()
fields.forEach(field => result[field] = result[field]);
That forEach loop will set all the fields you want to either the value in the result (if it was there) or to undefined if it wasn't.
MongoDB is schemaless and does not have tables, each collection can have different types of items.Usually the objects are somehow related or have a common base type.
Retrive invidual records using
db.collectionName.findOne() or db.collectionName.find().pretty()
To get all key names you need to MapReduce
mapReduceKeys = db.runCommand({
"mapreduce": "collection_name",
"map": function() {
for (var key in this) {
emit(key, null);
}
},
"reduce": function(key, stuff) {
return null;
},
"out": "collection_name" + "_keys"
})
Then run distinct on the resulting collection so as to find all the keys
db[mapReduceKeys.result].distinct("_id") //["foo", "bar", "baz", "_id", ...]

Mongoose: multiple query populate in a single call

In Mongoose, I can use a query populate to populate additional fields after a query. I can also populate multiple paths, such as
Person.find({})
.populate('books movie', 'title pages director')
.exec()
However, this would generate a lookup on book gathering the fields for title, pages and director - and also a lookup on movie gathering the fields for title, pages and director as well. What I want is to get title and pages from books only, and director from movie. I could do something like this:
Person.find({})
.populate('books', 'title pages')
.populate('movie', 'director')
.exec()
which gives me the expected result and queries.
But is there any way to have the behavior of the second snippet using a similar "single line" syntax like the first snippet? The reason for that, is that I want to programmatically determine the arguments for the populate function and feed it in. I cannot do that for multiple populate calls.
After looking into the sourcecode of mongoose, I solved this with:
var populateQuery = [{path:'books', select:'title pages'}, {path:'movie', select:'director'}];
Person.find({})
.populate(populateQuery)
.execPopulate()
you can also do something like below:
{path:'user',select:['key1','key2']}
You achieve that by simply passing object or array of objects to populate() method.
const query = [
{
path:'books',
select:'title pages'
},
{
path:'movie',
select:'director'
}
];
const result = await Person.find().populate(query).lean();
Consider that lean() method is optional, it just returns raw json rather than mongoose object and makes code execution a little bit faster! Don't forget to make your function (callback) async!
This is how it's done based on the Mongoose JS documentation http://mongoosejs.com/docs/populate.html
Let's say you have a BookCollection schema which contains users and books
In order to perform a query and get all the BookCollections with its related users and books you would do this
models.BookCollection
.find({})
.populate('user')
.populate('books')
.lean()
.exec(function (err, bookcollection) {
if (err) return console.error(err);
try {
mongoose.connection.close();
res.render('viewbookcollection', { content: bookcollection});
} catch (e) {
console.log("errror getting bookcollection"+e);
}
//Your Schema must include path
let createdData =Person.create(dataYouWant)
await createdData.populate([{path:'books', select:'title pages'},{path:'movie', select:'director'}])

Categories

Resources