Learn to manage data persistence using object-oriented models and ORM tools in JavaScript. Explore Sequelize and Mongoose for effective database interactions.
In this section, we will explore how to manage data persistence using object-oriented models and ORM (Object-Relational Mapping) tools in JavaScript. By the end of this chapter, you will understand how to define data models as classes, perform CRUD operations, handle relationships between entities, and manage data validation and schema management within models.
Object-Relational Mapping (ORM) is a programming technique that allows you to interact with a database using an object-oriented paradigm. Instead of writing raw SQL queries, you can work with database records as if they were objects in your programming language. This abstraction layer simplifies database interactions, making it easier to manage data and maintain your code.
JavaScript has several popular ORM libraries that facilitate database interactions. Two of the most widely used are Sequelize and Mongoose.
Sequelize is a promise-based Node.js ORM for SQL databases like PostgreSQL, MySQL, MariaDB, SQLite, and Microsoft SQL Server. It provides a powerful set of features for managing database operations using JavaScript.
Mongoose is an ODM (Object Data Modeling) library for MongoDB and Node.js. It provides a straightforward, schema-based solution to model your application data and includes built-in type casting, validation, query building, and business logic hooks.
In ORM, data models are typically defined as classes. These classes represent the structure of your database tables or collections. Let’s see how to define models using Sequelize and Mongoose.
To define a model in Sequelize, you create a class that extends Sequelize’s Model
class. Here’s an example of defining a User
model:
const { Sequelize, DataTypes, Model } = require('sequelize');
const sequelize = new Sequelize('sqlite::memory:'); // Example for SQLite
class User extends Model {}
User.init({
// Define attributes
firstName: {
type: DataTypes.STRING,
allowNull: false
},
lastName: {
type: DataTypes.STRING
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true
}
}, {
// Other model options
sequelize, // We need to pass the connection instance
modelName: 'User' // We need to choose the model name
});
// Sync all defined models to the DB
sequelize.sync();
In Mongoose, you define a schema and then create a model from it. Here’s how you can define a User
model:
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
firstName: {
type: String,
required: true
},
lastName: String,
email: {
type: String,
required: true,
unique: true
}
});
const User = mongoose.model('User', userSchema);
CRUD stands for Create, Read, Update, and Delete. These are the basic operations you can perform on a database. Let’s see how to perform these operations using Sequelize and Mongoose.
Create: To create a new record, use the create
method.
async function createUser() {
const user = await User.create({
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com'
});
console.log(user.toJSON());
}
Read: To read records, use the findAll
or findOne
methods.
async function getUsers() {
const users = await User.findAll();
console.log(users);
}
Update: To update a record, use the update
method.
async function updateUser(id) {
await User.update({ lastName: 'Smith' }, {
where: {
id: id
}
});
}
Delete: To delete a record, use the destroy
method.
async function deleteUser(id) {
await User.destroy({
where: {
id: id
}
});
}
Create: To create a new document, use the save
method.
async function createUser() {
const user = new User({
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com'
});
await user.save();
console.log(user);
}
Read: To read documents, use the find
or findOne
methods.
async function getUsers() {
const users = await User.find();
console.log(users);
}
Update: To update a document, use the updateOne
method.
async function updateUser(id) {
await User.updateOne({ _id: id }, { lastName: 'Smith' });
}
Delete: To delete a document, use the deleteOne
method.
async function deleteUser(id) {
await User.deleteOne({ _id: id });
}
In databases, relationships between entities are crucial for representing complex data structures. ORM tools provide mechanisms to define and manage these relationships.
Sequelize supports various types of associations, such as One-to-One
, One-to-Many
, and Many-to-Many
. Let’s explore how to define these associations.
One-to-One: Use hasOne
and belongsTo
methods.
class Profile extends Model {}
Profile.init({
bio: DataTypes.TEXT
}, { sequelize, modelName: 'Profile' });
User.hasOne(Profile);
Profile.belongsTo(User);
One-to-Many: Use hasMany
and belongsTo
methods.
class Post extends Model {}
Post.init({
title: DataTypes.STRING,
content: DataTypes.TEXT
}, { sequelize, modelName: 'Post' });
User.hasMany(Post);
Post.belongsTo(User);
Many-to-Many: Use belongsToMany
method with a junction table.
class Tag extends Model {}
Tag.init({
name: DataTypes.STRING
}, { sequelize, modelName: 'Tag' });
Post.belongsToMany(Tag, { through: 'PostTags' });
Tag.belongsToMany(Post, { through: 'PostTags' });
In Mongoose, relationships are handled by referencing documents or embedding them.
Referencing Documents: Use ObjectId
to reference another document.
const profileSchema = new mongoose.Schema({
bio: String,
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }
});
const Profile = mongoose.model('Profile', profileSchema);
Embedding Documents: Embed one document inside another.
const postSchema = new mongoose.Schema({
title: String,
content: String,
tags: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Tag' }]
});
const Post = mongoose.model('Post', postSchema);
Data validation and schema management are essential for ensuring data integrity and consistency. ORM tools provide built-in mechanisms to handle these aspects.
Sequelize allows you to define validation rules within your model definitions.
User.init({
firstName: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notEmpty: true
}
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: true
}
}
}, {
sequelize,
modelName: 'User'
});
Mongoose provides a rich set of validation options within schemas.
const userSchema = new mongoose.Schema({
firstName: {
type: String,
required: true,
trim: true
},
email: {
type: String,
required: true,
unique: true,
match: /.+\@.+\..+/
}
});
Now that we’ve covered the basics of working with databases using ORM tools, it’s time to experiment. Try modifying the code examples to:
To better understand how ORM manages relationships, let’s visualize a simple database schema with Sequelize associations.
erDiagram USER ||--o{ PROFILE : hasOne USER ||--o{ POST : hasMany POST ||--o{ TAG : belongsToMany
In this diagram, we see how a USER
can have one PROFILE
, many POSTS
, and how POSTS
can have many TAGS
.
Remember, working with databases using ORM tools is a powerful way to manage data in your applications. As you continue to explore and experiment, you’ll gain a deeper understanding of how to leverage these tools effectively. Keep practicing, stay curious, and enjoy the journey of mastering databases in JavaScript!