SPEAKER 1: So we spent a lot of time
in our last video talking about how you can make your Cloud
Firestore Database even more useful with a liberal
sprinkling of Cloud Functions.
We talked about why Cloud Functions are useful,
how they work under the hood, and we built out one function
to remove some really awful language from our reviews.
Ew, that's disgusting.
But what else are Cloud Functions
good for beyond ridding the world of fat-free dairy
products?
Let's dig into some common patterns
for using Cloud Functions, and think
about how you might want to use them in your own apps.
So I don't know if you noticed, but I'm a big fan
of lists of five on the series.
Ah, remember those days?
I had so much more hair back then.
Well, it just so happens that today's episode also works
nicely as a top five list.
So let's start by talking about my number one favorite use
for Cloud Functions, and that's using them
as an alternate solution for when your security rules start
to get too complex.
For example, imagine we're working on a multiplayer chess
game powered by Cloud Firestore.
We'd probably have some kind of games collection, where
each document contains the game's current state, a list
of players, whose turn it is, and maybe like a history of all
of our players moves.
So now when it's a player's turn,
we'd want to have that client send up
the player's new move, the new state of the board,
and maybe an updated history list.
But there's a lot of information we can't entirely trust here.
For starters, is this a game that the player is actually
participating in?
And if so, is it their turn?
And is the move a legal one?
Are they really appending their most recent move
to the list of previous moves or are they secretly
changing the history of the game?
And most importantly, does the state of the game board
accurately reflect what would have happened when
our player makes that move?
Now, in theory, this is all stuff
that we could handle through a sufficiently complex set
of security rules.
But I am not a fan of overly complex security rules.
These things sit at the frontline of your app,
they're kind of hard to debug, and you've got players
actively trying to break them.
So my general preference is to keep these as simple
as possible.
So how can you do this?
How can you keep your security real simple
while still allowing this kind of complex behavior?
Well, this is where Cloud Functions can help.
See, instead of having to worry about having our clients
muck with these games state documents directly,
my recommendation would be to lock these down
entirely so that no client can modify them.
This set of documents is going to remain read-only.
Instead, I'm going to create a completely separate
incoming moves collection.
Now, when a player decides to take a turn in their app,
they can submit their move by adding a document
to this collection.
And this document would be really simple,
consisting of nothing more than the player
ID, the game they're looking to participate in,
and the move that they want to make.
But because of that, the security rules
here can be very simple.
We'll only let clients create new documents
where the player ID equals their own user ID,
and, well, that's probably it, right?
Everything else can essentially be forbidden.
Then, once this document has been added to the pending moves
collection, our Cloud Function can take over.
When it sees a new document, it can check and see
if this game is active, if the player
ID is, in fact, one of the players
this game, if it's really their turn,
and then, most importantly, our function
can do all the heavy lifting of taking this move,
making sure it's legal, and calculating the final state
of the board for us.
Now, because this is all happening server-side
in a secure environment, we know this
is a function we can trust, which
means that Cloud Firestore will let this function go ahead
and alter the official game state document,
while keeping those documents completely locked down
for our suspicious-looking clients.
In the meantime, if we assume that our clients still
have real-time listeners set up on these game state documents,
they can still go ahead and receive these updates
as they're applied, so this change
will get reflected pretty quickly in our clients.
And as long as we're here, you can still
have your Cloud Function do some nice things,
like send a notification to your opponent using, like,
Firebase Cloud Messaging to tell them that it's their turn.
Now, the biggest drawback here is
that you don't get all that nice latency
compensation I was talking about a couple of videos ago, right?
Like, when you make a change to that incoming moves collection,
we can't update your cache data right away
the way we would if you were writing directly to that game
state document, which means that you don't get to see the change
reflected in your official game state
until the write has come back from the server.
It also means that if you're offline,
that pending change will be cued up and sent when you come back
online.
And, until then, your game will remain in its former state.
But still, for something like an online chess game,
I think these are reasonable trade-offs
to make as far as the user experience goes.
Now, going back to our restaurant review app,
we'll probably want to update a restaurant's average rating
every time a new review is written or edited.
But again, I'd be kind of nervous about having
clients update the restaurant document directly.
Not only do you need to make sure clients don't change
fields they're not supposed to, you
have to make sure this new calculated average is accurate
and not some crazy extreme value.
And that's going to be kind of hard to do with security rules
alone.
So again, this seems like a good opportunity
for a Cloud Function to do that work behind the scenes for us.
OK, let's move on to a second common use
case for Cloud Functions, and that's keeping
your denormalized data in sync.
So going back to our restaurant review app,
let's suppose that when we display reviews in our app,
we'll want include a little bit of information
about the author, like maybe their display name,
and a link to a profile photo.
Now in these situations, in order
to keep our NoSQL queries nice and NoSQL-y,
we would normalize some of that data.
Specifically, when a user writes a review,
we'd probably add their name and profile picture
to the review document.
And all this is fine and dandy, but what
happens when a user changes, say, the URL of their profile
picture?
Well, we not only need to change it like up here in their user
document, but also in every review they've ever written.
And yes, in theory, we could have the client do this for us.
But I'm not crazy about that idea.
For one thing, it kind of feels weird to have our client do
this much processing on our database,
you know, particularly given how flaky mobile phone
connections can be.
Plus, all that work involves extra data and battery costs
for our users.
And again, the security rules needed to allow all of this
could get a little messy.
So this is another situation where Cloud Functions might
prove to be really useful.
We can have a Cloud Function set up
to run whenever a user document is edited.
If it turns out that our user is editing their name or profile
picture, well great, our Cloud Function
can run a collection group query against every review for which
they're the author, and then change the author URL field
in that denormalized data.
And as I noted in an earlier video,
this is something that could be made
more efficient with batch writes, or slightly more atomic
with a transaction.
Or, heck, we could even combine this pattern
with the previous one, and not let users directly
edit their own user documents at all.
I mean, these documents might contain
other information we don't want our users to mess with.
Instead, they could write a document
to a completely separate pending user changes collection,
and then our Cloud Function could
take care of both editing the original user document
and changing all that denormalized data.
Now, there is one issue here that we need to watch out for--
by default Cloud Functions terminate after they've
been running for one minute.
And that might not be enough time for your function
to completely change all the documents it
needs to if you had a really, really large database.
Now, you can increase this default time
to nine minutes, which will be adequate for most situations.
But if you are running, say, a massive database with, like,
millions or billions of records that all contain
normalized data that need to be updated,
you could run up against this limitation.
And at that point, you might need
to consider other options, like standing up
a separate cloud-hosted server and running the denormalization
scripts from there.
But even in those situations, you
could use a Cloud Function to observe your database
and kick off the script on your dedicated server
when it sees that a user document has changed.
Now, on that note, let's move on to use case number
three, which is using Cloud Functions to perform
occasional database maintenance.
Now, around this time last year, the Cloud Functions team
added the ability to run your scripts
on a regularly occurring schedule, basically
like a cron job.
Now, this can be used for a lot of different tasks,
like generating weekly summary reports based on your data,
but it's also a nice opportunity keep
your database neat and tidy.
For instance, let's say we want to save drafts of restaurant
reviews written by our users.
Seems like a good idea.
But maybe we don't want to keep these things around forever,
right?
Like, at some point our database is
going to be full of half-abandoned reviews the way
my life is full of half-abandoned dreams.
"The Distress Signal," a new novel by Todd [INAUDIBLE]..
Chapter one.
Lucy always won.
Ugh, I give up.
(SINGING) Your face like a--
Ugh, not for me.
Hello, and welcome to the [? "Snack ?] [? Bowl" ?]
our weekly podcast on all things snack rel--
I'm sorry this is dumb.
I just-- I can't.
Oh, my god.
That got dark.
So we decide, hey, let's remove any draft reviews that
are older than, say, 45 days.
But again, how do we do something
like this with a client?
We could ask every client at the beginning of the session
to go and query every draft review that the user has,
and then delete ones that are older than 45 days.
But again, that just feels odd to have your client
doing that much work.
And what are you going to do about people who uninstall
your app and never come back?
You want to delete their drafts, too,
and they can't do that if their client isn't using your app.
So this is clearly a good opportunity
for a Cloud Function to come in query
all of your half-written reviews and delete ones that
haven't been modified in the last 45 days.
You can set it up to run once a night or once
every couple of nights.
And just like that, your database is nice and clean.
And I'm sure there are other places we could
apply this pattern as well.
Maybe we want to use it to put old user
accounts into an archive state when we realize they haven't
been accessed in six months.
Or maybe we decide, hey, we don't
need to recalculate a restaurant's average review
score every time a review is updated.
We could get away with just updating all of our restaurants
once a week instead.
Again, something we could do with the Cloud Function--
although an operation this big might run up
against our nine-minute timer, depending
on how popular our app is.
If we reach that point, it might be worth
having it update 1/7 of our restaurants each day
instead of doing the whole thing once a week.
OK, let's move on to pattern number four.
And this kind of gets into something we haven't really
talked about yet in this series, which
is that Cloud Firestore is a fantastic NoSQL database.
But for some of you out there, it's really less of a database,
and more like a super fancy real-time querying and caching
service built on top of your legacy database.
So yes, maybe for a restaurant review app, or a chess game,
or some brand-new app that you're building from scratch,
it might make sense to have Cloud Firestore
power all of your backend data.
But if you're an airline that's got
a 25-year-old system running on existing infrastructure,
or a bank, maybe the idea of completely wiping out
your server with all of your existing business
logic and regulatory compliance and all of that
so you can start fresh with a NoSQL database is impractical.
It's understandable, but you're also
sitting there wishing that you could build
an app with real-time updates, and off-line support,
and latency compensation with like an infinitely scaling back
end, right?
Well, this is where you might need to think differently
about Cloud Firestore.
For a lot of companies out there,
particularly long-standing established ones,
Cloud Firestore isn't the database
that runs your entire infrastructure, right?
It's the sleek new client-facing layer
that lives on top of your old infrastructure, which
allows you to build modern-looking apps.
So imagine you are an airline that has, like,
flight information and prices all stored in a decades-old SQL
database hosted on your own personal server rack.
And maybe you want to build an app that
lets people search for flights, and check flight status, and so
on, but the idea of exposing that database
to millions of users at once is frightening.
Well, what you can do is take that flight data, pricing
information, and whatever else you think
your users will want to search for, and copy that onto Cloud
Firestore, essentially as a read-only layer
on top of your actual database.
Seems like a good solution, but how
would you keep all that data in sync?
Well, there's a few ways to do it.
As you know, Cloud Firestore has server SDKs
that run in many different languages,
and it'd probably be pretty easy to write server code
on your original server that updates Cloud Firestore
directly whenever it has to change some data.
It's a good idea, although it is a pretty tightly coupled
system.
Your code on this other server now
has to know exactly how your data on this completely
different system is organized.
And if your Cloud Firestore engineer
decides to make some changes without telling the other team,
you're going to have a bad time.
So in addition to Cloud Functions
triggering when users write to your database,
or create new accounts, or what have you,
Cloud Functions also supports a service
called Cloud Pub/Sub, which is essentially
a generic message-passing service.
So if you need to change the price of a journey
or update the status of a flight,
you can also go ahead and create a Pub/Sub message,
which essentially looks like some JSON describing
what's changed.
And after you've done that, you can set up
a couple of Cloud Functions to listen to these Pub/Sub
messages.
When a Cloud Function encounters one of these messages,
it can go ahead and update the appropriate sections
of your database as needed.
Then, your original database doesn't
need to know how your other database is structured.
It just needs to publish this somewhat generic-looking
message.
Now, while this does mean adding another product
to your workflow, it's a pretty useful one.
The Pub/Sub queue scales really nicely,
allowing for spiky workloads that
won't overload your system.
It can be called from services that
aren't running on Google Cloud.
And you can add some fine-grained access control
to your queue so services only get access to the events
that you want them to.
This also means it can be useful if you have lots of services,
not just Cloud Firestore, that might need access
to this kind of data.
And what about when your users want
to write back to your database?
How would they do that?
There's a number of different ways.
In some cases, like purchasing an airline ticket,
you're probably going to need to have your clients
talk to your original database.
And they would do that with direct rest
calls to an intermediary server, or whatever you've got
set up on that legacy system.
In other cases, you might not need
to communicate with your original database at all.
Like maybe your users want to just take personal notes
about their trips.
For this user-only information, you could probably just keep it
on Cloud Firestore.
Then, you get the advantages of the client SDKs,
like offline support, which is probably
important on an airplane, and this information
can just continue to live on Cloud Firestore.
But there will also be cases where you want that information
living in both systems, like maybe a list of a user's
favorite flights, for instance.
In those cases, maybe you want the convenience of making calls
through the client SDK to your Cloud Firestore database,
but you also need a copy of this information on your legacy
server.
Again, this is a place where Cloud Functions
could come in quite handy.
As your function sees that a new favorite has
been added to a user's list of trips,
it could make an outside call to your legacy system, which
could then go ahead and make whatever changes it
needs in order to keep the two data sets in sync.
OK, finally, let's talk about one more interesting
use case for Cloud Functions.
And that's basically using Cloud Functions to build
your own custom API on top of your Cloud Firestore database,
or whatever other infrastructure you might be running.
So we've talked a lot about how Cloud Functions can trigger
automatically based on events happening in your project,
like a document being written to Cloud Firestore,
or a user account being added in Firebase [INAUDIBLE],,
and so on.
But they can also be called directly.
You can set up functions so that you can call them directly
over plain old HTTPS.
And, yes, this includes GET, PUT, POST requests,
the whole gamut.
Or is that ga-MUT?
Gamut.
And while that's useful, Firebase Cloud Functions
also comes with some pretty nifty client libraries
that simplify this process even further using something
known as a callable function.
A callable function is, again, to oversimplify
a little bit, a wrapper around HTTP calls,
but one that reduces a lot of the work
you would normally need in order to talk to a Cloud Function
directly.
For instance, if your user is signed in,
that information is sent across and automatically
verified by the server so you can
do things like perform user-specific actions
without having to, say, verify any signed hashes on your own.
And passing across data is as easy as sending a JSON
object along with your call.
Similarly, returning data is easy as well.
You're generally just going to pass back
JSON objects, which your clients can decode as needed.
And if your function encounters any problems,
callable functions do a pretty nice job
of simplifying error handling as well.
All this means that you can access these callable functions
using code that looks almost as if you're calling
some other local client method, no NS networking or volley
code in sight.
And that means you can quickly build up
your own application-specific API using callable functions
as a foundation.
So where would you want to use these callable functions?
Well, let's consider that chess example
I had back in the beginning of this video.
If you think about it, when we're writing that pending move
document to the database, all we're really doing
is sending a message to a Cloud Function.
We're just recording that message inside a document
as a way of passing that information over.
But maybe we don't need that intermediary step.
We could just as easily directly tell a Cloud Function, hey,
this is the move that our player wants to make in a game
by using a callable function.
Now, as in everything in life, there are trade-offs here.
On the plus side, we're no longer
adding all these document writes to our database,
and that saves us a few pennies here and there.
It's probably also a little more efficient
just talking to our function directly.
But the drawback is that we've lost offline support.
With the pending move document, that
could still be stored locally on our device
when the user is offline, and then uploaded later
when we come back online.
But a callable function, that's just going to fail immediately.
And we need to add our own custom logic
to retry this call again later when the user comes back
online.
Now, another good example is to use Cloud Functions
in conjunction with Cloud Firestore
to query our database in a way that's difficult or impossible
using native NoSQL techniques.
Remember back in episode five when
we thought about how we would want
to store a list of our user's favorite restaurants?
One option we explored early on was
to just store an array of restaurant IDs inside each user
document.
That felt pretty natural, but there's
no way to have Cloud Firestore natively perform
any kind of join that would give us the restaurant document
included in that array.
So we decided the NoSQL-y way of doing
this would be to create a completely separate collection
of user favorite documents where I could store the user
ID, the restaurant ID, and whatever information I might
need about this restaurant to populate a My Favorite
Restaurant screen.
And this is still a fine answer.
But it's a lot of documents with a lot of denormalized data
to keep in sync.
And if it turns out the View My Favorite Restaurants page ends
up being a feature that nobody really uses all that much,
maybe I haven't gotten the best bang for my buck here.
So another possibility would be to go back
to my original plan of just having an array of restaurant
IDs per user.
And then, having a callable Cloud Function
do the work of grabbing this array
and reading it in, and then fetching
the individual restaurant documents
based on the content of this array,
and then sending all that restaurant information back
to my device.
And, in fact, if you want to see me
do exactly that, I have a whole other video series where
you get to watch me fumble around in JavaScript
and build a function just like this.
It's linked in the description below.
Go check it out sometime.
Of course, by relying on a callable Cloud Function,
I do lose most of the benefits of making
a call through the client SDK.
For instance, I can no longer make this call real-time.
It's going to be a simple one-time fetch call.
And there's no native caching, so I
won't be able to look at my favorite restaurants
when I'm offline.
And all that nice latency compensation,
where we can first show the user their cached documents
and then update them with values from the server when
they show up a second later, well, that's gone, too.
I have to wait for a complete response from my Cloud Function
before I can show any results--
at least not without building my own cache, and offline support,
and latency compensation.
But if you think about it, this might
be a good minimum viable product type of solution,
so that I can quickly test out the idea of showing a user's
list of favorite restaurants.
And then, you can see whether this is a feature
that users actually care about.
At a later point, if this ends up
being a popular enough feature that is worth
going ahead and expanding out this full denormalized
collection, I can still do that once I've decided
it's worth doing.
And there's tons of other cases where a pattern like this
might apply--
things like performing OR queries
across different fields, or doing Greater Than or Less
Than queries on one field and sorting on another,
or even combining Cloud Firestore results
with results from other databases or services.
You can bundle that up in one easy-to-call API
thanks to Cloud Functions and callable functions.
So hopefully this has inspired you
to take a closer look at Cloud Functions
and consider how they might help you
build more powerful, consistent, or cleaner
Firestore-powered apps.
And then when you do, tell us about it.
Have you used Cloud Functions in interesting ways?
Let me know about in the comments.
I would be super interested to hear what you did.
Thanks for watching "Get to Know Cloud Firestore."
And now if you'll excuse me, I am late for a chess game.
Queen to f2.
You know what?
This game's crap.
[MUSIC PLAYING]