Firestore Many-to-Many: Part 3 - maps
Tags: firestore, firebase, rxjs, angular
Published: 2021-08-28 17:38:07

## Update 3/24/24
This post is out-of-date. Please see my latest post on Many-to-Many Maps.
https://code.build/p/firestore-many-to-many-maps-gm3B5X
___
## Original Post
___

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