Data Modeling allows how to relate data to other data in the same or different collection such as embedded or referenced while Schema Design allows determmine how data will be organized, validated and structured. It's very similiar to Classes in Object Oriented Programming.
Both Data Modeling and Schema Design are crucial in the development of databases and plays a significant role in ensuring data integrity, efficiency, and usability.
mongoose npm package runs top of MongoDB like a framework and it ensures to implement both data modeling & schema design.
// User Schema Design
const userSchema = new mongoose.Schema({
username: {
type: String,
minlength: [3, "@username cannot be shorter than 3 characters."],
required: [true, "@username is required."],
unique: true,
trim: true,
},
// ...
password: {
type: String,
minlength: [8, "Password cannot be shorter than 8 characters."],
maxlength: [32, "Password cannot be longer than 32 characters."],
required: [true, "Password is required."],
trim: true,
select: false,
},
// Password validation
passwordConfirm: {
type: String,
required: [true, "Please confirm your password."],
validate: {
validator: function (value) {
return value === this.password;
},
message: "Password doesn't match.",
},
},
active: {
type: Boolean,
default: true,
select: false,
},
});
Fields in a mongoose schema object could be any data types like Int, Array, String, Boolean, or even ObjectId and can have various properties such as unique, required, max-min length, select etc.
const userSchema = new mongoose.Schema({
...
// Referencing
followers: [
type: mongoose.Schema.Types.ObjectID
]
})
One to one relation, as the name suggests requires one entity to have an exclusive relationship with another entity and vice versa. In this case, 1 user can only have 1 username.
{
"username": "hsyntes"
}
One to many relation occurs when an instance of an entity has one or more related instances of another entity and keeps the documents as referenced by id or embedded.
const userSchema = new mongoose.Schema({
...
// Embedded
"followers": [
{
"_id": ObjectId('618'),
"username": "xyz",
},
{
"_id": ObjectId('23'),
"username": "abc"
}
// ...
]
// Referencing
"followers": [
ObjectId("618"),
ObjectId("23")
// ...
]
});
Many-to-many relation occurs when instances of at least two entities can both be related to multiple instances of another entity. In this case, 1 user can have many followers, but 1 follower can also be in many users.
// Also known Two-Way Referencing
{
"_id": ObjectId('618'),
"username": "hsyntes",
"followers": [
ObjectId('67'),
]
},
{
"_id": ObjectId('67'),
"username": "xyz",
"followers": [
ObjectId('618')
]
},
{
"_id": ObjectId('23'),
"username": "abc",
"followers": [
ObjectId('618'),
ObjectId('67')
]
}
One document keeps another document(s) as a virtual. The referenced documents actually aren't there, but it will exist when the document is querired.
Virtual Referencing or Populating would be the best solution for higher performance.
const userSchema = new mongoose.Schema({
username: {
type: String,
// ...
},
});
// "User" will be the unique Schema Key for this collection
const User = mongoose.model("User", userSchema);
The virtual field(s) will never exist under this document except for the document is queried with findOne.
// User Schema
const userSchema = new mongoose.Schema({
username: {
type: String,
unique: true,
// ...
},
},
// Enable virtuals
{ versionKey: false,
toJSON: { virtuals: true },
toObject: { virtuals: true }
}
);
// Virtual Populating
userSchema.virtual("Posts", { // Keep documents under "posts" field
ref: "Posts" // Schema key in another collection for reference
foreginField: "postedBy",
localField: "_id"
});
// Query Middleware
userSchema.pre('findOne', function (next) {
this.populate('posts');
next();
})
When the document is queried with 'findOne', the posts will be fetched that the user document related.
exports.createPost = async (req, res, next) => {
try {
const post = await Post.create({
title: req.body.title,
text: req.body.text,
// The current user that create this document will be settled as referenced
postedBy: req.user._id,
});
//...
}
}
$addToSet and $pull operators prevent duplicated fields when a field added/removed from an array in MongoDB.
// Starting the transaction
const session = await mongoose.startSession();
session.startTransaction();
await Post.findById(id).session(session);
await Post.findByIdAndUpdate(
id,
{
$addToSet: { likes: req.user._id },
},
{ new: true, runValidators: true }
);
await Post.findByIdAndUpdate(id, {
$pull: { likes: req.user._id },
});
Session transaction starts a multi-document transaction associated with the session. Multi-document transactions are available for both shared clusters and replica sets.
Want to see an example with a real application, please have a look at my instamern project. You can reach out advanced data modeling & schema design with aggregation pipeline, aggregation middleware, setting references with unique, etc.