Firestore Many-to-Many: Part 3 - maps

Firestore Many-to-Many: Part 3 - maps

9 min read

Both arrays and maps allow you to store probably somewhere around 10,000 items. This could vary, as your document size is really the only constraint storage-wise (1MB). Generally speaking, you can have up to 40,000 indexes per document, so you can have a lot of where clauses. See [here](https://firebase.google.com/docs/firestore/query-data/index-overview#indexing_limits) for constraints. ## Searching If you only need to search for 1 to 10 items at a time, arrays are always the way to go. They don't mess up your in-time indexing, and they seem self-explanatory. In fact, I would suggest arrays for most cases, as Firestore has made arrays easier. However, if you need to search for 11 to 40,000 items at a time, maps are the way to go. The only real problem is that sorting can be tricky. If you were to use the same example from the previous post, you would have something like this: ```typescript Classes / ClassID: { data... students: { studentID1: true, studentID2: true, ... } } Students / StudentID: { data... classes: { classID1: true, classID2: true } } ``` There are pros and cons of arrays vs maps, but you can generally accomplish the same goals. ### Add / Update I am not going to spend too much time here, as you should already understand the basics of writing to Firestore. Here is a [quick rundown](https://stackoverflow.com/a/46600599/271450). ```typescript const batch = this.afs.firestore.batch(); const studentID = this.afs.createId(); const classID = this.afs.createId(); const studentRef = this.afs.doc(`students/${studentID}`).ref; batch.set(studentRef, { name: 'tom', classes: { [classID]: true } }, { merge: true }); const classRef = this.afs.doc(`classes/${classID}`).ref; batch.set(classRef, { name: 'calculus', students: { [studentID]: true } }, { merge: true }); await batch.commit(); ``` See my [previous post](https://dev.to/jdgamble555/firestore-many-to-many-mf8) for why you use batch here. ### Query And to query without sorting, you don't need an index, and it is this simple. **Get all classes which studentID is taking.** ```typescript db.collection('classes') .where(`students.${studentID}`, '==', true); ``` #### OR VS AND Queries **Get all classes which StudentID_1 AND StudentID_2 are taking.** ```typescript db.collection('classes') .where(`students.${studentID_1}`, '==', true) .where(`students.${studentID_2}`, '==', true); ``` And for chaining... ```typescript const students = [studentID, studentID2,... ]; const results = this.afs.collection('classes', (ref: any) => students.reduce( (r: any, student: any) => r.where(`students.${student}`, '==', true) , ref) ).valueChanges({ idField: 'id' }); ``` **Get all classes which StudentID_1 OR StudentID_2 is taking.** There is unfortunately no way to get OR queries like there is with **array-contains-any**. However, you can combine results and filter them: ```typescript const students = [studentID, studentID2, studentID3,... ]; const results = combineLatest(students.map( (student: string) => this.afs.collection('classes', (ref: any) => ref.where(`students.${student}`, '==', true) ).valueChanges({ idField: 'id' }) )).pipe( // combine results map((a: any[]) => a.reduce( (acc: any[], cur: any) => acc.concat(cur) // filter duplicates ).filter( (b: any, n: number, a: any[]) => a.findIndex( (v: any) => v.id === b.id) === n // sort by id ).sort((a: any, b: any) => { const f = 'id'; if (a[f] < b[f]) { return -1; } if (b[f] < a[f]) { return 1; } return 0; })) ); ``` **Note:** Sorting this query (the only OR example I know is possible) would just require you to sort on the front end. See [add a map to sort](https://dev.to/jdgamble555/firestore-many-to-many-mf8) on part 1. You can use rxjs operators or array operators here, but I suspect array operators will be quicker. ### Sorting **Get all classes StudentID is taking sorted by startDate** You can't use `orderBy()` when you are using maps like this, unless you limit the name of your items (students, or tags i.e.), and you index them before hand. So, we have work-arounds. #### Method 1 Put the field you want to sort by as the value in every single map. If you add a value, you need to copy the name value. If you update the name value, you need to update all map values. This is a pain, but it works... ```typescript { name: 'jon', students: { 523k1s: 'jon', 392922: 'jon', ... } } ``` Ok, I already lied to you. You can use `orderBy()`, but without a where clause to get the desired result like so: ```typescript db.collection('classes') .orderBy(`students.${studentID}`) ``` OR ```typescript db.collection('classes') .where(`students.${studentID}`, '>', ' '); ``` **Note:** A space is the equivalent to 0 in ascii. #### Method 2 This only works on one field at a time, so for multiple fields you have to think about the doc ID. This is the [default sort order](https://firebase.google.com/docs/firestore/query-data/order-limit-data#order_and_limit_data). Basically you need to create a compound index on the document ID: ```typescript const docID = new Date() + '__' + this.afs.createId(); ``` **Note:** Notice you can't use `firebase.firestore.FieldValue.serverTimestamp()` here, but theoretically you could fix this in a firebase function by copying the document, and creating a new one with the correct date. It also can be any field, not necessarily the date. **Get all classes StudentID and StudentID2 are taking sorted by startDate** ```typescript db.collection('classes') .where(`students.${studentID}`, '==', true) .where(`students.${studentID2}`, '==', true) ``` And it will auto sort the way you like it. Next up... a non-scalable follower-feed... but it is kind-of scalable... J
manytomany
datamodeling