Understanding Sequelize Models and Validations
Understanding the Problem
When we build applications that store data, we need to ensure that the data is stored correctly and consistently. Think of it like maintaining a library - we need rules about how books are categorized, what information we keep about each book, and how we ensure that information is accurate and complete.
In our case, we're working with colors. Just as a library needs to ensure each book has a unique ISBN number and a proper title, we need to make sure each color has a unique name and that the name isn't empty. We'll implement these rules at two levels:
First, at the database level (like having a librarian check books as they arrive), and second, at the model level in our application (like having a computer system verify information before it even reaches the librarian).
Core Concepts
Models vs Migrations
Let's understand the distinction between models and migrations using our library analogy:
A migration is like the blueprint for building or modifying library shelves. It specifies the physical structure - how many shelves, how they're arranged, what size they need to be. In database terms, this means creating tables and specifying their columns and constraints.
A model is like the rules and procedures for managing books on those shelves. It defines how we interact with the books - how we add them, find them, and update their information. In database terms, this means defining how our application interacts with the data in our tables.
Step by Step Implementation
Step 1: Generating the Model and Migration
First, we'll generate our model and migration files:
npx sequelize model:generate --name Color --attributes name:string
This command creates two files:
- A migration file in the migrations directory
- A model file in the models directory
Step 2: Setting Up the Migration
Let's modify our migration file to include proper constraints:
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('Colors', {
id: {
allowNull: false, // Cannot be empty
autoIncrement: true, // Automatically increases
primaryKey: true, // Unique identifier
type: Sequelize.INTEGER // Whole number
},
name: {
type: Sequelize.STRING,
allowNull: false, // Name cannot be empty
unique: true // No duplicate names allowed
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
}
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('Colors');
}
};
Step 3: Setting Up the Model
Now let's add validations to our model:
'use strict';
const { Model } = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Color extends Model {
static associate(models) {
// define associations here
}
}
Color.init({
name: {
type: DataTypes.STRING,
allowNull: false, // Database-level: Cannot be empty
unique: true, // Database-level: Must be unique
validate: { // Model-level validations
notNull: true, // Ensures value isn't null
notEmpty: true, // Ensures string isn't empty
isString(value) { // Custom validation
if (typeof value !== 'string') {
throw new Error('Name must be a string!');
}
}
}
}
}, {
sequelize,
modelName: 'Color',
});
return Color;
};
Understanding Validations Deeply
Database-Level Constraints
Database constraints are like the physical rules of our library shelves. They ensure that:
The name column:
- Cannot be empty (allowNull: false)
- Must be unique (unique: true)
These constraints are enforced by the database itself, making them very reliable but also somewhat inflexible. They're like having unbreakable rules built into the physical structure of our library.
Model-Level Validations
Model validations are like having a smart assistant check everything before it even reaches the library shelves. They can:
For the name attribute:
- Verify the value isn't null (notNull: true)
- Ensure the string isn't empty (notEmpty: true)
- Check that the value is actually a string (custom validation)
These validations are more flexible and can include custom logic, but they only work when going through our Sequelize model.
Testing and Verification
After setting up our model and running migrations, we need to verify everything works correctly:
Testing Database Constraints
sqlite3 db/dev.db
.schema Colors // Shows table structure
.quit // Exits SQLite
The output should show our constraints:
CREATE TABLE `Colors` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`name` VARCHAR(255) NOT NULL UNIQUE,
`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updatedAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
Testing Model Validations
Our test file checks several scenarios:
// These should fail validation
await expect(models.Color.build({}).validate()).to.be.rejected; // Empty name
await expect(models.Color.build({name: []}).validate()).to.be.rejected; // Non-string name
// This should pass validation
await expect(models.Color.build({name: 'Purple'}).validate()).to.be.fulfilled;
// This tests uniqueness
await expect(models.Color.create({name: 'Red'})).to.be.fulfilled;
await expect(models.Color.create({name: 'Red'})).to.be.rejected;
Common Pitfalls and Solutions
Understanding Validation Layers
Remember that model validations and database constraints work together but at different times:
Model validations run first:
// This validation happens in JavaScript, before any SQL
const color = Color.build({ name: [] });
try {
await color.validate(); // Will fail here due to model validation
await color.save();
} catch (error) {
console.error('Validation failed:', error);
}
Database constraints run when saving:
// Even if model validation passes, database constraints might fail
try {
await Color.create({ name: 'Red' }); // Works first time
await Color.create({ name: 'Red' }); // Fails due to unique constraint
} catch (error) {
console.error('Database constraint violated:', error);
}
Best Practices
When implementing validations:
- Always use both model and database validations for critical constraints
- Keep validation logic consistent between your model and migration
- Use model validations for complex rules that can't be expressed in the database
- Test both successful and failing scenarios
Taking It Further
Adding Custom Validations
We can add more sophisticated validations as needed. For example, adding the isPrimary field:
isPrimary: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
validate: {
isPrimaryColor(value) {
if (this.name) {
const primaryColors = ['Red', 'Blue', 'Yellow'];
if (value && !primaryColors.includes(this.name)) {
throw new Error('Only Red, Blue, and Yellow can be primary colors');
}
}
}
}
}
This validation ensures that only actual primary colors can be marked as primary, demonstrating how we can create complex, business-logic validations at the model level.