Home
> Firestore Many-to-Many: array-contains-all
📧 Subscribe to our Newsletter for course and post updates!

Firestore Many-to-Many: array-contains-all

9 min read

Jonathan Gamble

jdgamble555 on Sunday, March 24, 2024 (last modified on Thursday, April 25, 2024)

When using arrays, you can easily perform an OR query. However, Firestore does not natively have an AND query, and you can’t combine array-contains clauses. Here is a workaround method I created.

TL;DR#

You can search for one item in array with array-contains, for one item OR another item with array-contains-any, and you can search for one item AND another item with this workaround simulating array-contains-all. This is best for storing unlimited items in sets of 1-5 items. You can use native sorting techniques and other filters without a problem. However, if you don’t want a workaround and you don’t need complex sorting, you should consider using the Map version.

array-contains-any#

First, we need to remember the OR query versions.

Get all students taking EITHER class X OR class Y#

	 query(
   collection(db, 'students'), 
   where('classes', 'array-contains-any', [class1ID, class2ID])
 );

Get all classes EITHER student X OR student Y is taking#

	 query(
   collection(db, 'classes'), 
   where('students', 'array-contains-any', [student1ID, student2ID])
 );

You create your own index using every combination of items in the array, which gives you query options. You could do this on the front end or on the back end in Firebase Cloud Functions.

Reusable Functions#

You can add these to a separate file and reuse them.

Delimiter#

Use whatever delimiter you want.

	const DELIMETER = '.';

Add a New Array#

	function addArray(arr: string[]): string[] {

    // declare new array
    const allSubsets: string[][] = [[]];

    // go through each item
    for (const element of arr) {
    
        // go through each existing item in array
        const newSubsets = allSubsets.map(subset => [...subset, element]);
        
        // create new set
        allSubsets.push(...newSubsets);
    }

    // remove first empty array item
    allSubsets.shift();
    
    // return sorted array separated by delimeter
    return allSubsets.map(subset => subset.sort().join(DELIMETER));
}

Get an Existing Array#

	function showArray(s: string[]) {
    return s.filter(item => !item.includes(DELIMETER)).sort();
}

Query an Array#

	function containsArray(s: string[] | string) {
    return typeof s === 'string'
        ? s
        : s.sort().join(DELIMETER);
}

Usage#

These three functions are needed to add, update, and query a document in your Firestore collection.

Add#

The addArray function will create the indexes automatically with the desired diameter.

	await addDoc(collection(db, 'students'), {
  classes: addArray([
    class1,
    class2,
    class3
  ])
});

Query#

To query a document, you must filter all results containing the delimiter, as you only want the real classes to show. The showArray function will do this.

	// get student record by ID
const docSnap = await getDoc(
  doc(db, "students", studentID)
);

// get student data, filter classes
const data = docSnap.data();

const studentData = {
  ...data,
  id: docSnap.id,
  classes: showArray(data.classes)
};

If you’re using real-time, you will want to do the same thing before you display the data in an observable, signal, store, or effect. See Framework Setup.

Update#

When you update your arrays, you can’t use arrayUnion or arrayRemove. You will have to replace the existing array with a new one. You can use the JavaScript spread operator to combine the arrays. You usually have already read the document you’re updating in your production app. Don’t get charged another read for data you already have read in your client.

	// reuse the classes array in studentData we have just read
updateDoc(doc(db, 'students', studentID), {
  classes: addArray([
    ...studentData.classes,
    class4,
    class5,
    class6
  ])
});

Filtering#

The previous three functions must be used to simulate array-contains-all. You can now use the containsArray function inside your actual array-contains function.

Get all students taking both Class X AND Class Y#

	query(
  collection(db, 'students'),
  where('classes', 'array-contains', containsArray([class1, class2]))
);

How this works#

In your actual database, you’re creating an index for the unique sets of items.

	classes: ["math", "biology", "science"];

You can see what this actually looks like in your Firestore document. Here, we are using . as the delimiter, but you could use anything.

array-contains-all Firestore index

You can also see how this could become overwhelming with just a small set of data. If you have three items, your combinations will be 7 items. In mathematics, there is a combinations formula.

	C(max) = C(3,1) + C(3,2) + C(3,3)

Without getting into the details with factorials, this means 3 items have 7 combinations, 4 items have 15 combinations, and 5 items have 31 combinations! 10 items would need an array of 1024 items!

When to use this#

Now you can see why array-contains-all does not exist. At the very least, they should figure out how to make it work for a few items. This is the perfect workaround if you need to store infinite items in small sets (maybe 1 to 5 items). It will also allow you to sort better without workarounds.

In the meantime, I have added it to my Firebase Wish List.

J


Related Posts

🔥 Authentication

🔥 Counting Documents

🔥 Data Modeling

🔥 Framework Setup

🔥Under Construction

Newsletter

📧 Get our course and blog post updates!

© 2024 Code.Build