Postulate is the best way to take and share notes for classes, research, and other learning.
When developing an application, it's common to make changes to the database schema as you go. In a relational database, such a change necessarily modifies all previously entered rows of data; in a database managed through an ORM, there's likely an automatic schema migration feature built in.
MongoDB, however, is document-based rather than relational, meaning that different documents in a collection can have different fields. Furthermore, it doesn't use an ORM by default, and even mongoose doesn't have easy built in migration features.
This means that when you want to add a field to a set of documents -- for example, the recent addition of the privacy
field to the Post model in Postulate -- you have to make sure you're handling migrations correctly, i.e. ensuring that old documents get new fields added to them.
Thankfully, a library called migrate-mongo makes these manual migrations fairly painless. Here's how to use it to make a schema migration.
Run npm install -g migrate-mongo
.
Run migrate-mongo init
in your project directory to create a migrate-mongo-config.js
file and migrations
directory.
I replace the config file's contents with the following, using dot-env
to securely load in my MongoDB URI with password included:
require("dotenv").config(); const config = { mongodb: { url: process.env.MONGODB_URL, options: { useNewUrlParser: true, useUnifiedTopology: true, } }, migrationsDir: "migrations", changelogCollectionName: "changelog", migrationFileExtension: ".js" }; module.exports = config;
Run migrate-mongo create [migration-name]
to create a new migration file in the migrations
folder. This file exports two functions, up
and down
. up
should contain the code to carry out the operation, while down
should contain the code to revert it.
When developing fast, I often don't even write a down
function, and have only a very straightforward up
function to be carried out across all existing documents in a collection. To add a field with an empty array as the default value, for example, I used the following code:
const mongoose = require("mongoose"); module.exports = { async up(db, client) { await db.collection("snippets").updateMany({}, { $set: { linkedPosts: [] }, }) }, };
For the privacy
field example mentioned above, I set the field to a fixed default value:
const mongoose = require("mongoose"); module.exports = { async up(db, client) { await db.collection("posts").updateMany({}, { $set: { privacy: "public" }, }) }, };
In a more complicated migration, I turned embedded documents into standalone ones in a separate collection:
module.exports = { async up(db, client) { const users = db.collection("users"); const usersCursor = await users.find(); while (await usersCursor.hasNext()) { const userData = await usersCursor.next(); if (userData.updates) { for (let update of userData.updates) { await db.collection("updates").insertOne({ ...update, userId: userData._id, }); } await db.collection("users").updateOne({id: userData._id}, { $set: {updates: []}, }); } } }, };
Once you've created your migration functions, run migrate-mongo up
to run them.
This carries out the migration, and also creates a new document in a changelog
collection in MongoDB. This document allows the migration to be reverted using migrate-mongo down [options]
. It also allows you to re-run a migration script by simply deleting the corresponding document and re-running migrate-mongo up
.
With that, you can make clean and complex if needed migrations when making changes to your MongoDB schema!
Founder and dev notes from building Postulate