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:
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!
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
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:
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!
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)));
});