This section describes the various association types in sequelize. When calling a method such as `User.hasOne(Project)`, we say that the `User` model (the model that the function is being invoked on) is the __source__ and the `Project` model (the model being passed as an argument) is the __target__.
This section describes the various association types in sequelize. There are four type of
associations available in Sequelize
1. BelongsTo
2. HasOne
3. HasMany
4. BelongsToMany
## 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`.
```js
constUser=sequelize.define('User',{
name:Sequelize.STRING,
email:Sequelize.STRING
});
constProject=sequelize.define('Project',{
name:Sequelize.STRING
});
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:
User.hasMany(Task);// Will add userId to Task model
Task.belongsTo(User);// Will also add userId to Task model
```
Will generate the following SQL:
```sql
CREATETABLEIFNOTEXISTS"users"(
"id"SERIAL,
"username"VARCHAR(255),
"createdAt"TIMESTAMPWITHTIMEZONENOTNULL,
"updatedAt"TIMESTAMPWITHTIMEZONENOTNULL,
PRIMARYKEY("id")
);
CREATETABLEIFNOTEXISTS"tasks"(
"id"SERIAL,
"title"VARCHAR(255),
"createdAt"TIMESTAMPWITHTIMEZONENOTNULL,
"updatedAt"TIMESTAMPWITHTIMEZONENOTNULL,
"userId"INTEGERREFERENCES"users"("id")ONDELETE
SET
NULLONUPDATECASCADE,
PRIMARYKEY("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 the
`field` option on all attributes to the underscored version of its name. This also applies to
foreign keys generated by associations.
Let's modify last example to use `underscored` option.
```js
constTask=sequelize.define('task',{
title:Sequelize.STRING
},{
underscored:true
});
constUser=sequelize.define('user',{
username:Sequelize.STRING
},{
underscored:true
});
// 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:
```sql
CREATETABLEIFNOTEXISTS"users"(
"id"SERIAL,
"username"VARCHAR(255),
"created_at"TIMESTAMPWITHTIMEZONENOTNULL,
"updated_at"TIMESTAMPWITHTIMEZONENOTNULL,
PRIMARYKEY("id")
);
CREATETABLEIFNOTEXISTS"tasks"(
"id"SERIAL,
"title"VARCHAR(255),
"created_at"TIMESTAMPWITHTIMEZONENOTNULL,
"updated_at"TIMESTAMPWITHTIMEZONENOTNULL,
"user_id"INTEGERREFERENCES"users"("id")ONDELETE
SET
NULLONUPDATECASCADE,
PRIMARYKEY("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.
```js
constDocument=sequelize.define('document',{
author:Sequelize.STRING
});
constVersion=sequelize.define('version',{
timestamp:Sequelize.DATE
});
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:
#### 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.
```js
const Trainer = sequelize.define('trainer', {
firstName: Sequelize.STRING,
lastName: Sequelize.STRING
});
// Series will have a trainerId = Trainer.id foreign reference key
// after we call Trainer.hasMany(series)
const Series = sequelize.define('series', {
title: Sequelize.STRING,
subTitle: Sequelize.STRING,
description: Sequelize.TEXT,
// Set FK relationship (hasMany) with `Trainer`
trainerId: {
type: Sequelize.INTEGER,
references: {
model: Trainer,
key: 'id'
}
}
});
// Video will have seriesId = Series.id foreign reference key
// after we call Series.hasOne(Video)
const Video = sequelize.define('video', {
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'
}
}
});
Series.hasOne(Video);
Trainer.hasMany(Series);
```
## One-To-One associations
One-To-One associations are associations between exactly two models connected by a single foreign key.
...
...
@@ -22,13 +245,14 @@ Player.belongsTo(Team); // Will add a teamId attribute to Player to hold the pri
By default the foreign key for a belongsTo relation will be generated from the target model name and the target primary key name.
The default casing is `camelCase` however if the source model is configured with `underscored: true` the foreignKey will be`snake_case`.
The default casing is `camelCase`. If the source model is configured with `underscored: true` the foreignKey will be created with field`snake_case`.
This will add the attribute `projectId`or `project_id` to User. Instances of Project will get the accessors `getWorkers` and `setWorkers`.
This will add the attribute `projectId`to User. Depending on your setting for underscored the column in the table will either be called projectId or project_id. Instances of Project will get the accessors `getWorkers` and `setWorkers`.
Sometimes you may need to associate records on different columns, you may use `sourceKey` option:
...
...
@@ -286,153 +511,6 @@ User.findAll({
});
```
## Scopes
This section concerns association scopes. For a definition of association scopes vs. scopes on associated models, see [Scopes](/manual/tutorial/scopes.html).
Association scopes allow you to place a scope (a set of default attributes for `get` and `create`) on the association. Scopes can be placed both on the associated model (the target of the association), and on the through table for n:m relations.
#### 1:m
Assume we have tables Comment, Post, and Image. A comment can be associated to either an image or a post via `commentable_id` and `commentable` - we say that Post and Image are `Commentable`
`constraints: false,` disables references constraints - since the `commentable_id` column references several tables, we cannot add a `REFERENCES` constraint to it. Note that the Image -> Comment and Post -> Comment relations define a scope, `commentable: 'image'` and `commentable: 'post'` respectively. This scope is automatically applied when using the association functions:
The `getItem` utility function on `Comment` completes the picture - it simply converts the `commentable` string into a call to either `getImage` or `getPost`, providing an abstraction over whether a comment belongs to a post or an image. You can pass a normal options object as a parameter to `getItem(options)` to specify any where conditions or includes.
#### n:m
Continuing with the idea of a polymorphic model, consider a tag table - an item can have multiple tags, and a tag can be related to several items.
For brevity, the example only shows a Post model, but in reality Tag would be related to several other models.
```js
constItemTag=sequelize.define('item_tag',{
id:{
type:DataTypes.INTEGER,
primaryKey:true,
autoIncrement:true
},
tag_id:{
type:DataTypes.INTEGER,
unique:'item_tag_taggable'
},
taggable:{
type:DataTypes.STRING,
unique:'item_tag_taggable'
},
taggable_id:{
type:DataTypes.INTEGER,
unique:'item_tag_taggable',
references:null
}
});
constTag=sequelize.define('tag',{
name:DataTypes.STRING
});
Post.belongsToMany(Tag,{
through:{
model:ItemTag,
unique:false,
scope:{
taggable:'post'
}
},
foreignKey:'taggable_id',
constraints:false
});
Tag.belongsToMany(Post,{
through:{
model:ItemTag,
unique:false
},
foreignKey:'tag_id',
constraints:false
});
```
Notice that the scoped column (`taggable`) is now on the through model (`ItemTag`).
We could also define a more restrictive association, for example, to get all pending tags for a post by applying a scope of both the through model (`ItemTag`) and the target model (`Tag`):
```js
Post.hasMany(Tag,{
through:{
model:ItemTag,
unique:false,
scope:{
taggable:'post'
}
},
scope:{
status:'pending'
},
as:'pendingTags',
foreignKey:'taggable_id',
constraints:false
});
Post.getPendingTags();
```
```sql
SELECT`tag`.*INNERJOIN`item_tags`AS`item_tag`
ON`tag`.`id`=`item_tag`.`tagId`
AND`item_tag`.`taggable_id`=42
AND`item_tag`.`taggable`='post'
WHERE(`tag`.`status`='pending');
```
`constraints: false` disables references constraints on the `taggable_id` column. Because the column is polymorphic, we cannot say that it `REFERENCES` a specific table.
## Naming strategy
By default sequelize will use the model name (the name passed to `sequelize.define`) to figure out the name of the model when used in associations. For example, a model named `user` will add the functions `get/set/add User` to instances of the associated model, and a property named `.user` in eager loading, while a model named `User` will add the same functions, but a property named `.User` (notice the upper case U) in eager loading.
This section concerns association scopes. For a definition of association scopes vs. scopes on associated models, see [Scopes](/manual/tutorial/scopes.html).
When you create associations between your models in sequelize, foreign key references with constraints will automatically be created. The setup below:
Association scopes allow you to place a scope (a set of default attributes for `get` and `create`) on the association. Scopes can be placed both on the associated model (the target of the association), and on the through table for n:m relations.
Assume we have models Comment, Post, and Image. A comment can be associated to either an image or a post via `commentableId` and `commentable` - we say that Post and Image are `Commentable`
The relation between task and user injects the `user_id` foreign key on tasks, and marks it as a reference to the `User` table. By default `user_id` will be set to `NULL` if the referenced user is deleted, and updated if the id of the user id 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`.
Comment.prototype.getItem=function(options){
returnthis[
'get'+
this.get('commentable')
.substr(0,1)
.toUpperCase()+
this.get('commentable').substr(1)
](options);
};
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.
Post.hasMany(Comment,{
foreignKey:'commentableId',
constraints:false,
scope:{
commentable:'post'
}
});
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 User table must be created before the Task 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.
Comment.belongsTo(Post,{
foreignKey:'commentableId',
constraints:false,
as:'post'
});
```js
constDocument=this.sequelize.define('document',{
author:Sequelize.STRING
})
constVersion=this.sequelize.define('version',{
timestamp:Sequelize.DATE
})
Image.hasMany(Comment,{
foreignKey:'commentableId',
constraints:false,
scope:{
commentable:'image'
}
});
Document.hasMany(Version)// This adds document_id to version
Document.belongsTo(Version,{as:'Current',foreignKey:'current_version_id'})// This adds current_version_id to document
Comment.belongsTo(Image,{
foreignKey:'commentableId',
constraints:false,
as:'image'
});
```
However, the code above will result in the following error: `Cyclic dependency found. 'Document' is dependent of itself. Dependency Chain: Document -> Version => Document`. In order to alleviate that, we can pass `constraints: false` to one of the associations:
`constraints: false` disables references constraints, as `commentableId` column references several tables, we cannot add a `REFERENCES` constraint to it.
Note that the Image -> Comment and Post -> Comment relations define a scope, `commentable: 'image'` and `commentable: 'post'` respectively. This scope is automatically applied when using the association functions:
// SELECT "id", "title", "commentable", "commentableId", "createdAt", "updatedAt" FROM "comments" AS "comment" WHERE "comment"."commentable" = 'image' AND "comment"."commentableId" = 1;
// UPDATE "comments" SET "commentableId"=1,"commentable"='image',"updatedAt"='2018-04-17 05:38:43.948 +00:00' WHERE "id" IN (1)
```
### Enforcing a foreign key reference without constraints
The `getItem` utility function on `Comment` completes the picture - it simply converts the `commentable` string into a call to either `getImage` or `getPost`, providing an abstraction over whether a comment belongs to a post or an image. You can pass a normal options object as a parameter to `getItem(options)` to specify any where conditions or includes.
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.
#### n:m
Continuing with the idea of a polymorphic model, consider a tag table - an item can have multiple tags, and a tag can be related to several items.
```js
// Series has a trainer_id=Trainer.id foreign reference key after we call Trainer.hasMany(series)
const Series = sequelize.define('series', {
title: DataTypes.STRING,
sub_title: DataTypes.STRING,
description: DataTypes.TEXT,
// Set FK relationship (hasMany) with `Trainer`
trainer_id: {
type: DataTypes.INTEGER,
references: {
model: "trainer",
key: "id"
}
For brevity, the example only shows a Post model, but in reality Tag would be related to several other models.
```js
constItemTag=sequelize.define('item_tag',{
id:{
type:Sequelize.INTEGER,
primaryKey:true,
autoIncrement:true
},
tagId:{
type:Sequelize.INTEGER,
unique:'item_tag_taggable'
},
taggable:{
type:Sequelize.STRING,
unique:'item_tag_taggable'
},
taggableId:{
type:Sequelize.INTEGER,
unique:'item_tag_taggable',
references:null
}
})
const Trainer = sequelize.define('trainer', {
first_name: DataTypes.STRING,
last_name: DataTypes.STRING
});
// Video has a series_id=Series.id foreign reference key after we call Series.hasOne(Video)...
const Video = sequelize.define('video', {
title: DataTypes.STRING,
sequence: DataTypes.INTEGER,
description: DataTypes.TEXT,
// set relationship (hasOne) with `Series`
series_id: {
type: DataTypes.INTEGER,
references: {
model: Series, // Can be both a string representing the table name, or a reference to the model
key: "id"
constTag=sequelize.define('tag',{
name:Sequelize.STRING,
status:Sequelize.STRING
});
Post.belongsToMany(Tag,{
through:{
model:ItemTag,
unique:false,
scope:{
taggable:'post'
}
}
},
foreignKey:'taggableId',
constraints:false
});
Tag.belongsToMany(Post,{
through:{
model:ItemTag,
unique:false
},
foreignKey:'tagId',
constraints:false
});
Series.hasOne(Video);
Trainer.hasMany(Series);
```
## Creating with associations
Notice that the scoped column (`taggable`) is now on the through model (`ItemTag`).
We could also define a more restrictive association, for example, to get all pending tags for a post by applying a scope of both the through model (`ItemTag`) and the target model (`Tag`):
`constraints: false` disables references constraints on the `taggableId` column. Because the column is polymorphic, we cannot say that it `REFERENCES` a specific table.
### Creating with associations
An instance can be created with nested association in one step, provided all elements are new.
### Creating elements of a "BelongsTo", "Has Many" or "HasOne" association
@@ -771,11 +921,11 @@ A new `Product`, `User`, and one or more `Address` can be created in one step in
returnProduct.create({
title:'Chair',
user:{
first_name:'Mick',
last_name:'Broadstone',
firstName:'Mick',
lastName:'Broadstone',
addresses:[{
type:'home',
line_1:'100 Main St.',
line1:'100 Main St.',
city:'Austin',
state:'TX',
zip:'78704'
...
...
@@ -791,25 +941,25 @@ return Product.create({
Here, our user model is called `user`, with a lowercase u - This means that the property in the object should also be `user`. If the name given to `sequelize.define` was `User`, the key in the object should also be `User`. Likewise for `addresses`, except it's pluralized being a `hasMany` association.
### Creating elements of a "BelongsTo" association with an alias
#### BelongsTo association with an alias
The previous example can be extended to support an association alias.
* Note how we also specified `constraints: false` for profile picture. This is because we add a foreign key from user to picture (profilePictureId), and from picture to user (userId). If we were to add foreign keys to both, it would create a cyclic dependency, and sequelize would not know which table to create first, since user depends on picture, and picture depends on user. These kinds of problems are detected by sequelize before the models are synced to the database, and you will get an error along the lines of `Error: Cyclic dependency found. 'users' is dependent of itself`. If you encounter this, you should either disable some constraints, or rethink your associations completely.
*/
classAssociation{
constructor(source,target,options){
options=options||{};
constructor(source,target,options={}){
/**
* @type {Model}
*/
this.source=source;
/**
* @type {Model}
*/
this.target=target;
this.options=options;
this.scope=options.scope;
this.isSelfAssociation=this.source===this.target;
this.as=options.as;
/**
* The type of the association. One of `HasMany`, `BelongsTo`, `HasOne`, `BelongsToMany`
* @type {string}
...
...
@@ -106,22 +108,29 @@ class Association {
);
}
}
// Normalize input - may be array or single obj, instance or primary key - convert it to an array of built objects
toInstanceArray(objs){
if(!Array.isArray(objs)){
objs=[objs];
/**
* Normalize input
*
* @param input {Any}, it may be array or single obj, instance or primary key
@@ -113,9 +112,11 @@ class HasMany extends Association {
// the id is in the target table
// or in an extra table which connects two tables
injectAttributes(){
_injectAttributes(){
constnewAttributes={};
constconstraintOptions=_.clone(this.options);// Create a new options object for use with addForeignKeyConstraints, to avoid polluting this.options in case it is later used for a n:m
// Create a new options object for use with addForeignKeyConstraints, to avoid polluting this.options in case it is later used for a n:m
* @param {Boolean} [options.omitNull] Don't persist null values. This means that all columns with null values will not be saved
* @param {Boolean} [options.timestamps=true] Adds createdAt and updatedAt timestamps to the model.
* @param {Boolean} [options.paranoid=false] Calling `destroy` will not delete the model, but instead set a `deletedAt` timestamp if this is true. Needs `timestamps=true` to work
* @param {Boolean} [options.underscored=false] Converts all camelCased columns to underscored if true. Will not affect timestamp fields named explicitly by model options and will not affect fields with explicitly set `field` option
* @param {Boolean} [options.underscoredAll=false] Converts camelCased model names to underscored table names if true. Will not change model name if freezeTableName is set to true
* @param {Boolean} [options.underscored=false] Add underscored field to all attributes, this covers user defined attributes, timestamps and foreign keys. Will not affect attributes with explicitly set `field` option
* @param {Boolean} [options.freezeTableName=false] If freezeTableName is true, sequelize will not try to alter the model name to get the table name. Otherwise, the model name will be pluralized
* @param {Object} [options.name] An object with two attributes, `singular` and `plural`, which are used when this model is associated to others.
* @param {Boolean} [options.indexes[].unique=false] Should the index by unique? Can also be triggered by setting type to `UNIQUE`
* @param {Boolean} [options.indexes[].concurrently=false] PostgreSQL will build the index without taking any write locks. Postgres only
* @param {Array<String|Object>} [options.indexes[].fields] An array of the fields to index. Each field can either be a string containing the name of the field, a sequelize object (e.g `sequelize.fn`), or an object with the following attributes: `attribute` (field name), `length` (create a prefix index of length chars), `order` (the direction the column should be sorted in), `collate` (the collation (sort order) for the column)
* @param {String|Boolean} [options.createdAt] Override the name of the createdAt column if a string is provided, or disable it if false. Timestamps must be true. Not affected by underscored setting.
* @param {String|Boolean} [options.updatedAt] Override the name of the updatedAt column if a string is provided, or disable it if false. Timestamps must be true. Not affected by underscored setting.
* @param {String|Boolean} [options.deletedAt] Override the name of the deletedAt column if a string is provided, or disable it if false. Timestamps must be true. Not affected by underscored setting.
* @param {String|Boolean} [options.createdAt] Override the name of the createdAt attribute if a string is provided, or disable it if false. Timestamps must be true. Underscored field will be set with underscored setting.
* @param {String|Boolean} [options.updatedAt] Override the name of the updatedAt attribute if a string is provided, or disable it if false. Timestamps must be true. Underscored field will be set with underscored setting.
* @param {String|Boolean} [options.deletedAt] Override the name of the deletedAt attribute if a string is provided, or disable it if false. Timestamps must be true. Underscored field will be set with underscored setting.
* @param {String} [options.tableName] Defaults to pluralized model name, unless freezeTableName is true, in which case it uses model name verbatim
.throw('Naming collision between attribute \'person\' and association \'person\' on model car. To remedy this, change either foreignKey or as in your association definition');
});
});
});
describe('Association',()=>{
it('should set foreignKey on foreign table',function(){