When developing an application, it is natural for the data structure to evolve. When working with a relational database, you can run migrations as needed. But how to handle schema evolution when your data is stored in a NoSQL database like Cloud Firestore?
To illustrate this guide, we’ll use a simple example from my recruitment app for Classic WoW guilds.
A bit of context first. WoW is a multi-player game that follows a common class-design pattern. Players choose classes for their avatars (eg. warrior, mage or paladin) and each class has access to three specializations. While these specializations do not matter much in the current game, they will be important in the future.
At the moment, the app stores guild recruitment openings as an array of classes. But I want guild leaders to be able to manage the recruitment for given specializations, not only classes. The code will speak for itself.
However, we cannot run a migration against Firestore to update the whole database schema. Well, we could try. But I wouldn’t advise it — it’s hard to test and it can get complex with nested data structure. So how do we do? Just update all our components so they can handle all different schemas? Hell, no.
We’ll use converters, which Firebase implements as FirestoreDataConverter. You can see converters like fancy setters and getters. Converters only do two things: they format the data inserted in, or retrieved from, the database.
Here’s the thing: we will convert our data as soon as we retrieve it from Firestore. If the data is structured with the old schema, we will update it to follow the new one. Updating our application will then become trivial. All other components will only have to deal with the latest, updated structure.
Now that we have our converter, we just need to use it when accessing the database.
And that’s it!
We’ve done three things:
- Encapsulate the schema migration logic in a utility function
- Ensure our app only deals with one version of the data structure
- Enable our app to deal with schema evolution without touching the database
And that gives us three benefits: (1) migration logic is testable, (2) front-end components can easily validate props data, and (3) our app remains scalable.