Basic Concepts
Source & Target
Let's first begin with a basic concept that you will see used in most associations, source and target model. Suppose you are trying to add an association between two Models. Here we are adding a hasOne
association between User
and Project
.
class User extends Model {}
User.init({
name: Sequelize.STRING,
email: Sequelize.STRING
}, {
sequelize,
modelName: 'user'
});
class Project extends Model {}
Project.init({
name: Sequelize.STRING
}, {
sequelize,
modelName: 'project'
});
User.hasOne(Project);
User
model (the model that the function is being invoked on) is the source. Project
model (the model being passed as an argument) is the target.
Foreign Keys
When you create associations between your models in sequelize, foreign key references with constraints will automatically be created. The setup below:
class Task extends Model {}
Task.init({ title: Sequelize.STRING }, { sequelize, modelName: 'task' });
class User extends Model {}
User.init({ username: Sequelize.STRING }, { sequelize, modelName: 'user' });
User.hasMany(Task); // Will add userId to Task model
Task.belongsTo(User); // Will also add userId to Task model
Will generate the following SQL:
CREATE TABLE IF NOT EXISTS "users" (
"id" SERIAL,
"username" VARCHAR(255),
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
PRIMARY KEY ("id")
);
CREATE TABLE IF NOT EXISTS "tasks" (
"id" SERIAL,
"title" VARCHAR(255),
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
"userId" INTEGER REFERENCES "users" ("id") ON DELETE
SET
NULL ON UPDATE CASCADE,
PRIMARY KEY ("id")
);
The relation between tasks
and users
model injects the userId
foreign key on tasks
table, and marks it as a reference to the users
table. By default userId
will be set to NULL
if the referenced user is deleted, and updated if the id of the userId
updated. These options can be overridden by passing onUpdate
and onDelete
options to the association calls. The validation options are RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL
.
For 1:1 and 1:m associations the default option is SET NULL
for deletion, and CASCADE
for updates. For n:m, the default for both is CASCADE
. This means, that if you delete or update a row from one side of an n:m association, all the rows in the join table referencing that row will also be deleted or updated.
underscored option
Sequelize allow setting underscored
option for Model. When true
this option will set thefield
option on all attributes to the underscored version of its name. This also applies toforeign keys generated by associations.
Let's modify last example to use underscored
option.
class Task extends Model {}
Task.init({
title: Sequelize.STRING
}, {
underscored: true,
sequelize,
modelName: 'task'
});
class User extends Model {}
User.init({
username: Sequelize.STRING
}, {
underscored: true,
sequelize,
modelName: 'user'
});
// Will add userId to Task model, but field will be set to `user_id`
// This means column name will be `user_id`
User.hasMany(Task);
// Will also add userId to Task model, but field will be set to `user_id`
// This means column name will be `user_id`
Task.belongsTo(User);
Will generate the following SQL:
CREATE TABLE IF NOT EXISTS "users" (
"id" SERIAL,
"username" VARCHAR(255),
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL,
PRIMARY KEY ("id")
);
CREATE TABLE IF NOT EXISTS "tasks" (
"id" SERIAL,
"title" VARCHAR(255),
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL,
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL,
"user_id" INTEGER REFERENCES "users" ("id") ON DELETE
SET
NULL ON UPDATE CASCADE,
PRIMARY KEY ("id")
);
With the underscored option attributes injected to model are still camel cased but field
option is set to their underscored version.
Cyclic dependencies & Disabling constraints
Adding constraints between tables means that tables must be created in the database in a certain order, when using sequelize.sync
. If Task
has a reference to User
, the users
table must be created before the tasks
table can be created. This can sometimes lead to circular references, where sequelize cannot find an order in which to sync. Imagine a scenario of documents and versions. A document can have multiple versions, and for convenience, a document has a reference to its current version.
class Document extends Model {}
Document.init({
author: Sequelize.STRING
}, { sequelize, modelName: 'document' });
class Version extends Model {}
Version.init({
timestamp: Sequelize.DATE
}, { sequelize, modelName: 'version' });
Document.hasMany(Version); // This adds documentId attribute to version
Document.belongsTo(Version, {
as: 'Current',
foreignKey: 'currentVersionId'
}); // This adds currentVersionId attribute to document
However, the code above will result in the following error: Cyclic dependency found. documents is dependent of itself. Dependency chain: documents -> versions => documents
.
In order to alleviate that, we can pass constraints: false
to one of the associations:
Document.hasMany(Version);
Document.belongsTo(Version, {
as: 'Current',
foreignKey: 'currentVersionId',
constraints: false
});
Which will allow us to sync the tables correctly:
CREATE TABLE IF NOT EXISTS "documents" (
"id" SERIAL,
"author" VARCHAR(255),
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
"currentVersionId" INTEGER,
PRIMARY KEY ("id")
);
CREATE TABLE IF NOT EXISTS "versions" (
"id" SERIAL,
"timestamp" TIMESTAMP WITH TIME ZONE,
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
"documentId" INTEGER REFERENCES "documents" ("id") ON DELETE
SET
NULL ON UPDATE CASCADE,
PRIMARY KEY ("id")
);
Enforcing a foreign key reference without constraints
Sometimes you may want to reference another table, without adding any constraints, or associations. In that case you can manually add the reference attributes to your schema definition, and mark the relations between them.
class Trainer extends Model {}
Trainer.init({
firstName: Sequelize.STRING,
lastName: Sequelize.STRING
}, { sequelize, modelName: 'trainer' });
// Series will have a trainerId = Trainer.id foreign reference key
// after we call Trainer.hasMany(series)
class Series extends Model {}
Series.init({
title: Sequelize.STRING,
subTitle: Sequelize.STRING,
description: Sequelize.TEXT,
// Set FK relationship (hasMany) with `Trainer`
trainerId: {
type: Sequelize.INTEGER,
references: {
model: Trainer,
key: 'id'
}
}
}, { sequelize, modelName: 'series' });
// Video will have seriesId = Series.id foreign reference key
// after we call Series.hasOne(Video)
class Video extends Model {}
Video.init({
title: Sequelize.STRING,
sequence: Sequelize.INTEGER,
description: Sequelize.TEXT,
// set relationship (hasOne) with `Series`
seriesId: {
type: Sequelize.INTEGER,
references: {
model: Series, // Can be both a string representing the table name or a Sequelize model
key: 'id'
}
}
}, { sequelize, modelName: 'video' });
Series.hasOne(Video);
Trainer.hasMany(Series);