Anthony Chu Contact Me

Using Mongoose Discriminators to Store Multiple Entities in a Single Cosmos DB Collection

Monday, May 29, 2017

When transitioning to Cosmos DB (formerly DocumentDB), a common misconception is that collections are equivalent to tables in relational databases.

The guidance for Cosmos DB is actually to store multiple types of entities with different schemas in the same collection. The main reason for this is that documents in the same DocumentDB collection can participate in transactions (via stored procedures).

While applications using Cosmos DB's MongoDB API don't really take advantage of transactions, there's still a huge reason for using a single collection for multiple entity types... to save money. Cosmos DB charges per collection.

In this article, we'll take a look at using discriminators in Mongoose (a popular MongoDB library for Node.js) to store multiple entities in a single Cosmos DB collection.

Default behavior with Mongoose

By default, Mongoose will save each model in its own collection. Here's an example:

const Order = mongoose.model('Order', new mongoose.Schema({
    orderDate: { type: Date, default: Date.now },
    items: [String]
}));

const Customer = mongoose.model('Customer', new mongoose.Schema({
    firstName: { type: String },
    lastName: { type: String },
    email: { type: String }
}));


const order = new Order({
    items: [
        "apple",
        "orange",
        "pear"
    ]
});
order.save((err, savedOrder) => {
    console.log(JSON.stringify(savedOrder));
});

const customer = new Customer({
    firstName: "John",
    lastName: "Doe",
    email: "john@doe.com"
});
customer.save((err, savedCustomer) => {
    console.log(JSON.stringify(savedCustomer));
});

Here we have two models: Order and Customer. If we run this, we'll end up with two collections in CosmosDB:

2 collections

Currently, the minimum cost of Cosmos DB is $25/month (400 Request Units). That means it'll cost us $25 for every model we add to our application.

Mongoose with discriminators

Discriminators in Mongoose allows us to specify one property (key) in a document that we'll use to discriminate between different types of entities, and allow us to store different types of entities in the same collection.

Instead of creating the two models directly, we'll first create a base model and specify a discriminatorKey and a name for the common collection. Then we'll use the discriminator() method on the base model to create the actual models that we'll be using:

const baseOptions = {
    discriminatorKey: '__type',
    collection: 'data'
}
const Base = mongoose.model('Base', new mongoose.Schema({}, baseOptions));

const Order = Base.discriminator('Order', new mongoose.Schema({
    orderDate: { type: Date, default: Date.now },
    items: [String]
}));

const Customer = Base.discriminator('Customer', new mongoose.Schema({
    firstName: { type: String },
    lastName: { type: String },
    email: { type: String }
}));

Notice it's a very minor change from the original version. We can then use the exact same code as we did above to create the documents:

const order = new Order({
    items: [
        "apple",
        "orange",
        "pear"
    ]
});
order.save((err, savedOrder) => {
    console.log(JSON.stringify(savedOrder));
});

const customer = new Customer({
    firstName: "John",
    lastName: "Doe",
    email: "john@doe.com"
});
customer.save((err, savedCustomer) => {
    console.log(JSON.stringify(savedCustomer));
});

And we end up with a single data collection with both documents in there. Each entity is discriminated by the __type property in the document. We only pay for one collection!

1 collection

Querying the data

As we'd expect, Mongoose takes care of applying the filter on the discriminator when we make a query. For instance, this will query all the orders (and only the orders):

Order.find((err, res) => {
    res.forEach(r => console.log(JSON.stringify(r)));
});

Source code

https://github.com/anthonychu/mongoose-cosmosdb