Cloud Firestore

Firestore was introduced into the Firebase platform on October 3rd, 2017. It is a superior alternative (in most situations) to the Realtime Database that is covered in Chapter 3.

What is Firestore?

Firestore is a NoSQL document-oriented database, similar to MongoDB, CouchDB, and AWS DynamoDB.

It works by storing JSON-like data into documents, then organizes them into collections that can be queried. All data is contained on the document, while a collection just serves as a container. Documents can contain their own nested subcollections of documents, leading to a hierarchical structure. The end result is a database that can model complex relationships and make multi-property compound queries.

Unlike a table in a SQL database, a Firestore document does not adhere to a data schema. In other words, document-ABC can look completely different from document-XYZ in the same collection. However, it is a good practice to keep data structures as consistent as possible across collections. Firestore automatically indexes documents by their properties, so your ability to query a collection is optimized by a consistent document structure.

The goal of this chapter is to introduce data modeling best practices and teach you how perform common tasks with Firestore in Angular.

2.0 Cloud Firestore versus Realtime Database

Problem

You’re not sure if you should use Firestore or the Realtime Database.

Solution

I follow a simple rule - use Firestore, unless you have a good reason not to.

However, if you can answer TRUE to ALL statements below, the Realtime Database might worth exploring.

  1. You make frequent queries to a small dataset.
  2. You do not require complex querying, filtering, sorting.
  3. You do not need to model data relationships.

If you responded FALSE to any of these statements, use Firestore.

Realtime Database billing is weighted heavily on data storage, while Cloud Firestore is weighted on bandwidth. Cost savings could make Realtime Database a compelling option when you have high-bandwidth demands on a lightweight dataset.

Why are there two databases in Firebase?

Firebase won’t tell you this outright, but the Realtime Database has its share of frustrating caveats. Exhibit A: querying/filtering data is very limited. Exhibit B: nesting data is impossible on large datasets, requiring you to denormalize at the global level. Lucky for you, Firestore addresses these issues head on, which means you’re in great shape if you’re just starting a new app. Realtime Database is still around because it would be risky/impossible to migrate the gazillions of bytes of data from Realtime Database to Firestore. So Google decided to add a second database to the platform and not deal with the data migration problem.

2.1 Data Structuring

Problem

You want to know how to structure your data in Firestore.

Solution

You already know JavaScript, so think of a collection as an Array and a document as an Object.

What’s Inside a Document?

A document contains JSON-like data that includes all of the expected primitive datatypes like strings, numbers, dates, booleans, and null - as well as objects and arrays.

Documents also have several custom datatypes. A GeoPoint will automatically validate latitude and longitude coordinates, while a DocumentReference can point to another document in your database. We will see these special datatypes in action later in the chapter.

Best Practices

Firestore pushes you to form a hierarchy of data relationships. You start with (1) a collection in the root of the database, then (2) add a document inside of it, then (3) add another collection inside that document, then (4) repeat steps 2 and 3 as many times as you need.

  1. Always think about HOW the data will be queried. Your goal is to make data retrieval fast and efficient.
  2. Collections can be large, but documents should be small.
  3. If a document becomes too large, consider nesting data in a deeper collection.

Let’s take a look at some common examples.

Example: Blog Posts and Comments

In this example, we have a collection of posts with some basic content data, but posts can also receive comments from users. We could save new comments directly on the document, but would that scale well if we had 10,000 comments? No, the memory in the app would blow up trying to retrieve this data. In fact, Firestore will throw an error for violating the 1 Mb document size limit well before reaching this point. A better approach is to nest a comments subcollection under each document and query it separately from the post data. Document retrieval is shallow - only the top level data is returned, while nested collections can be retrieved separately.

 1 ++postsCollection
 2     postDoc
 3       - author
 4       - title
 5       - content
 6       ++commentsCollection
 7           commentDocFoo
 8             - text
 9           commentDocBar
10             - text

Example: Group Chat

For group chat, we can use two root level collections called users and chats. The user document is simple - just a place to keep basic user data like email, username, etc.

A chat document stores basic data about a chat room, such as the participating users. Each room has a nested collection of messages (just like the previous example). However, the message makes a reference to the associated user document, allowing us to query additional data about the user if we so choose.

A document reference is very similar to a foreign key in a SQL database. It is just a pointer to a document that exists at some other location in the database.

 1 ++usersCollection
 2     userDoc
 3       - username
 4       - email
 5 
 6 ++chatsCollection
 7     chatDoc
 8       - users[]
 9     ++messagesCollection
10         messageDocFoo
11           - text
12           - userDocReference
13         messageDocBar
14           - userDocReference

Example: Stars, Hearts, Likes, Votes, Etc.

In the graphic above, we can see how the movies collection and users collection have a two-way connection through the middle-man stars collection. All data about a relationship is kept in the star document - data never needs to change on the connected user/movie documents directly.

Having a root collection structure allows us to query both “Movie reviews” and “User reviews” independently. This would not be possible if stars were nested as a sub collection. This is similar to a many-to-many-through relationship in a SQL database.

 1 ++usersCollection
 2     userDoc
 3       - username
 4       - email
 5 
 6 ++starsCollection
 7     starDoc
 8       - userId
 9       - movieId
10       - value
11 
12 ++moviesCollection
13     movieDoc
14       - title
15       - plot

2.2 Collection Retrieval

Problem

You want to retrieve a collection of documents.

Solution

A collection of documents in Firestore is like a table of rows in a SQL database, or a list of objects in the Realtime Database. When we retrieve a collection in Angular, the endgame is to generate an Observable array of objects [{...data}, {...data}, {...data}] that we can show the end user.

The examples in this chapter will use the TypeScript Book interface below. AngularFire requires a type to be specified, but you can opt out with the any type, for example AngularFirestoreCollection<any>.

1 export interface Book {
2   author: string;
3   title: string:
4   content: string;
5 }

I am setting up the code in an Angular component, but you can also extract this logic into a service to make it available (injectable) to multiple components.

Reading data in AngularFire is accomplished by (1) making a reference to its location in Firestore, (2) requesting an Observable with valueChanges(), and (3) subscribing to the Observable.

Steps 1 and 2: book-info.component.ts

 1 import { Component, OnInit } from '@angular/core';
 2 import { Observable }        from 'rxjs';
 3 import {
 4   AngularFirestore,
 5   AngularFirestoreCollection,
 6   AngularFirestoreDocument
 7 } from 'angularfire2/firestore';
 8 
 9 @Component({
10   selector: 'book-info',
11   templateUrl: './book-info.component.html',
12   styleUrls: ['./book-info.component.scss']
13 })
14 export class BookInfoComponent implements OnInit {
15 
16   constructor(private afs: AngularFirestore) {}
17 
18   booksCollection: AngularFireCollection<Book>;
19   booksObservable: Observable<Book[]>;
20 
21     ngOnInit() {
22         // Step 1: Make a reference
23         this.booksCollection = this.afs.collection('books');
24 
25         // Step 2: Get an observable of the data
26         this.booksObservable = this.booksCollection.valueChanges();
27     }
28 
29 }

Step 3: book-info.component.html

The ideal way to handle an Observable subscription is with the async pipe in the HTML. Angular will subscribe (and unsubscribe) automatically, making your code concise and maintainable.

1 <!-- Step 3: Subscribe to the data -->
2 <ul>
3   <li *ngFor="let book of booksObservable | async">
4     {{ book.title }} by {{ book.author }}
5   </li>
6 </ul>

Step 3 (alternative): book-info.component.ts

It is also possible to subscribe directly in the Typescript. You just need to remember to unsubscribe to avoid memory leaks. Modify the component code with the following changes to handle the subscription manually.

 1 import { Subscription }      from 'rxjs';
 2 
 3 /// ...omitted
 4 
 5     sub: Subscription;
 6 
 7     ngOnInit() {
 8 
 9         /// ...omitted
10 
11         // Step 3: Subscribe
12         this.sub = this.booksObservable.subscribe(books => console.log(books))
13     }
14 
15     ngOnDestroy() {
16         this.sub.unsubscribe()
17     }
18 
19 }

End Preview