不要怂,就是干,撸起袖子干!

Commit 5941bfe7 by Sushant Committed by GitHub

change(model): new options.underscored implementation (#9304)

1 parent 0cbb7b9f
# Associations
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
const User = sequelize.define('User', {
name: Sequelize.STRING,
email: Sequelize.STRING
});
const Project = 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:
```js
const Task = sequelize.define('task', { title: Sequelize.STRING });
const User = sequelize.define('user', { username: Sequelize.STRING });
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
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 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
const Task = sequelize.define('task', {
title: Sequelize.STRING
}, {
underscored: true
});
const User = 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
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.
```js
const Document = sequelize.define('document', {
author: Sequelize.STRING
});
const Version = 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:
```js
Document.hasMany(Version);
Document.belongsTo(Version, {
as: 'Current',
foreignKey: 'currentVersionId',
constraints: false
});
```
Which will allow us to sync the tables correctly:
```sql
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.
```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`.
```js
const User = this.sequelize.define('user', {/* attributes */})
const Company = this.sequelize.define('company', {/* attributes */});
User.belongsTo(Company); // Will add companyId to user
// will add companyId to user
User.belongsTo(Company);
const User = this.sequelize.define('user', {/* attributes */}, {underscored: true})
const Company = this.sequelize.define('company', {
......@@ -38,7 +262,8 @@ const Company = this.sequelize.define('company', {
}
});
User.belongsTo(Company); // Will add company_uuid to user
// will add companyUuid to user with field company_uuid
User.belongsTo(Company);
```
In cases where `as` has been defined it will be used in place of the target model name.
......@@ -191,7 +416,7 @@ const Project = sequelize.define('project', {/* ... */})
Project.hasMany(User, {as: 'Workers'})
```
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`
```js
const Comment = this.sequelize.define('comment', {
title: Sequelize.STRING,
commentable: Sequelize.STRING,
commentable_id: Sequelize.INTEGER
});
Comment.prototype.getItem = function(options) {
return this['get' + this.get('commentable').substr(0, 1).toUpperCase() + this.get('commentable').substr(1)](options);
};
Post.hasMany(this.Comment, {
foreignKey: 'commentable_id',
constraints: false,
scope: {
commentable: 'post'
}
});
Comment.belongsTo(this.Post, {
foreignKey: 'commentable_id',
constraints: false,
as: 'post'
});
Image.hasMany(this.Comment, {
foreignKey: 'commentable_id',
constraints: false,
scope: {
commentable: 'image'
}
});
Comment.belongsTo(this.Image, {
foreignKey: 'commentable_id',
constraints: false,
as: 'image'
});
```
`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:
```js
image.getComments()
SELECT * FROM comments WHERE commentable_id = 42 AND commentable = 'image';
image.createComment({
title: 'Awesome!'
})
INSERT INTO comments (title, commentable_id, commentable) VALUES ('Awesome!', 42, 'image');
image.addComment(comment);
UPDATE comments SET commentable_id = 42, commentable = 'image'
```
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
const ItemTag = 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
}
});
const Tag = 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`.* INNER JOIN `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.
......@@ -621,125 +699,197 @@ project.setUsers([user1, user2]).then(() => {
// result would be true
})
```
## Advance Concepts
## Foreign Keys
### Scopes
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.
```js
const Task = this.sequelize.define('task', { title: Sequelize.STRING })
const User = this.sequelize.define('user', { username: Sequelize.STRING })
 
User.hasMany(Task)
Task.belongsTo(User)
```
#### 1:n
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`
Will generate the following SQL:
```js
const Post = sequelize.define('post', {
title: Sequelize.STRING,
text: Sequelize.STRING
});
```sql
CREATE TABLE IF NOT EXISTS `User` (
`id` INTEGER PRIMARY KEY,
`username` VARCHAR(255)
);
const Image = sequelize.define('image', {
title: Sequelize.STRING,
link: Sequelize.STRING
});
CREATE TABLE IF NOT EXISTS `Task` (
`id` INTEGER PRIMARY KEY,
`title` VARCHAR(255),
`user_id` INTEGER REFERENCES `User` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
);
```
const Comment = sequelize.define('comment', {
title: Sequelize.STRING,
commentable: Sequelize.STRING,
commentableId: Sequelize.INTEGER
});
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) {
return this[
'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
const Document = this.sequelize.define('document', {
author: Sequelize.STRING
})
const Version = 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:
```js
Document.hasMany(Version)
Document.belongsTo(Version, { as: 'Current', foreignKey: 'current_version_id', constraints: false})
```
image.getComments()
// SELECT "id", "title", "commentable", "commentableId", "createdAt", "updatedAt" FROM "comments" AS "comment" WHERE "comment"."commentable" = 'image' AND "comment"."commentableId" = 1;
Which will allow us to sync the tables correctly:
image.createComment({
title: 'Awesome!'
})
// INSERT INTO "comments" ("id","title","commentable","commentableId","createdAt","updatedAt") VALUES (DEFAULT,'Awesome!','image',1,'2018-04-17 05:36:40.454 +00:00','2018-04-17 05:36:40.454 +00:00') RETURNING *;
```sql
CREATE TABLE IF NOT EXISTS `Document` (
`id` INTEGER PRIMARY KEY,
`author` VARCHAR(255),
`current_version_id` INTEGER
);
CREATE TABLE IF NOT EXISTS `Version` (
`id` INTEGER PRIMARY KEY,
`timestamp` DATETIME,
`document_id` INTEGER REFERENCES `Document` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
);
image.addComment(comment);
// 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
const ItemTag = 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"
const Tag = 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`):
```js
Post.belongsToMany(Tag, {
through: {
model: ItemTag,
unique: false,
scope: {
taggable: 'post'
}
},
scope: {
status: 'pending'
},
as: 'pendingTags',
foreignKey: 'taggableId',
constraints: false
});
post.getPendingTags();
```
```sql
SELECT
"tag"."id",
"tag"."name",
"tag"."status",
"tag"."createdAt",
"tag"."updatedAt",
"item_tag"."id" AS "item_tag.id",
"item_tag"."tagId" AS "item_tag.tagId",
"item_tag"."taggable" AS "item_tag.taggable",
"item_tag"."taggableId" AS "item_tag.taggableId",
"item_tag"."createdAt" AS "item_tag.createdAt",
"item_tag"."updatedAt" AS "item_tag.updatedAt"
FROM
"tags" AS "tag"
INNER JOIN "item_tags" AS "item_tag" ON "tag"."id" = "item_tag"."tagId"
AND "item_tag"."taggableId" = 1
AND "item_tag"."taggable" = 'post'
WHERE
("tag"."status" = 'pending');
```
`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
#### BelongsTo / HasMany / HasOne association
Consider the following models:
......@@ -748,13 +898,13 @@ const Product = this.sequelize.define('product', {
title: Sequelize.STRING
});
const User = this.sequelize.define('user', {
first_name: Sequelize.STRING,
last_name: Sequelize.STRING
firstName: Sequelize.STRING,
lastName: Sequelize.STRING
});
const Address = this.sequelize.define('address', {
type: Sequelize.STRING,
line_1: Sequelize.STRING,
line_2: Sequelize.STRING,
line1: Sequelize.STRING,
line2: Sequelize.STRING,
city: Sequelize.STRING,
state: Sequelize.STRING,
zip: Sequelize.STRING,
......@@ -771,11 +921,11 @@ A new `Product`, `User`, and one or more `Address` can be created in one step in
return Product.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.
```js
const Creator = Product.belongsTo(User, {as: 'creator'});
const Creator = Product.belongsTo(User, { as: 'creator' });
return Product.create({
title: 'Chair',
creator: {
first_name: 'Matt',
last_name: 'Hansen'
firstName: 'Matt',
lastName: 'Hansen'
}
}, {
include: [ Creator ]
});
```
### Creating elements of a "HasMany" or "BelongsToMany" association
#### HasMany / BelongsToMany association
Let's introduce the ability to associate a product with many tags. Setting up the models could look like:
......@@ -840,14 +990,14 @@ Product.create({
And, we can modify this example to support an alias as well:
```js
const Categories = Product.hasMany(Tag, {as: 'categories'});
const Categories = Product.hasMany(Tag, { as: 'categories' });
Product.create({
id: 1,
title: 'Chair',
categories: [
{id: 1, name: 'Alpha'},
{id: 2, name: 'Beta'}
{ id: 1, name: 'Alpha' },
{ id: 2, name: 'Beta' }
]
}, {
include: [{
......
......@@ -530,8 +530,8 @@ const Bar = sequelize.define('bar', { /* bla */ }, {
// timestamps are enabled
paranoid: true,
// don't use camelcase for automatically added attributes but underscore style
// so updatedAt will be updated_at
// Will automatically set field option for all attributes to snake cased name.
// Does not override attribute with field option already defined
underscored: true,
// disable the modification of table names; By default, sequelize will automatically
......
......@@ -80,20 +80,22 @@ const AssociationError = require('./../errors').AssociationError;
* 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.
*/
class Association {
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
*
* @returns <Array>, built objects
*/
toInstanceArray(input) {
if (!Array.isArray(input)) {
input = [input];
}
return objs.map(function(obj) {
if (!(obj instanceof this.target)) {
const tmpInstance = {};
tmpInstance[this.target.primaryKeyAttribute] = obj;
return this.target.build(tmpInstance, {
isNewRecord: false
});
}
return obj;
}, this);
return input.map(element => {
if (element instanceof this.target) return element;
const tmpInstance = {};
tmpInstance[this.target.primaryKeyAttribute] = element;
return this.target.build(tmpInstance, { isNewRecord: false });
});
}
inspect() {
return this.as;
}
......
......@@ -121,12 +121,11 @@ class BelongsToMany extends Association {
}
this.foreignKeyAttribute = {};
this.foreignKey = this.options.foreignKey || Utils.camelizeIf(
this.foreignKey = this.options.foreignKey || Utils.camelize(
[
Utils.underscoredIf(this.source.options.name.singular, this.source.options.underscored),
this.source.options.name.singular,
this.source.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
].join('_')
);
}
......@@ -139,17 +138,11 @@ class BelongsToMany extends Association {
}
this.otherKeyAttribute = {};
this.otherKey = this.options.otherKey || Utils.camelizeIf(
this.otherKey = this.options.otherKey || Utils.camelize(
[
Utils.underscoredIf(
this.isSelfAssociation ?
Utils.singularize(this.as) :
this.target.options.name.singular,
this.target.options.underscored
),
this.isSelfAssociation ? Utils.singularize(this.as) : this.target.options.name.singular,
this.target.primaryKeyAttribute
].join('_'),
!this.target.options.underscored
].join('_')
);
}
......@@ -188,13 +181,13 @@ class BelongsToMany extends Association {
this.otherKey = this.paired.foreignKey;
}
if (this.paired.otherKeyDefault) {
// If paired otherKey was inferred we should make sure to clean it up before adding a new one that matches the foreignKey
// If paired otherKey was inferred we should make sure to clean it up
// before adding a new one that matches the foreignKey
if (this.paired.otherKey !== this.foreignKey) {
delete this.through.model.rawAttributes[this.paired.otherKey];
this.paired.otherKey = this.foreignKey;
this.paired._injectAttributes();
}
this.paired.otherKey = this.foreignKey;
this.paired.foreignIdentifier = this.foreignKey;
delete this.paired.foreignIdentifierField;
}
}
......@@ -226,8 +219,7 @@ class BelongsToMany extends Association {
// the id is in the target table
// or in an extra table which connects two tables
injectAttributes() {
_injectAttributes() {
this.identifier = this.foreignKey;
this.foreignIdentifier = this.otherKey;
......@@ -302,6 +294,8 @@ class BelongsToMany extends Association {
this.through.model.rawAttributes[this.foreignKey] = _.extend(this.through.model.rawAttributes[this.foreignKey], sourceAttribute);
this.through.model.rawAttributes[this.otherKey] = _.extend(this.through.model.rawAttributes[this.otherKey], targetAttribute);
this.through.model.refreshAttributes();
this.identifierField = this.through.model.rawAttributes[this.foreignKey].field || this.foreignKey;
this.foreignIdentifierField = this.through.model.rawAttributes[this.otherKey].field || this.otherKey;
......@@ -309,8 +303,6 @@ class BelongsToMany extends Association {
this.paired.foreignIdentifierField = this.through.model.rawAttributes[this.paired.otherKey].field || this.paired.otherKey;
}
this.through.model.refreshAttributes();
this.toSource = new BelongsTo(this.through.model, this.source, {
foreignKey: this.foreignKey
});
......
......@@ -41,17 +41,15 @@ class BelongsTo extends Association {
}
if (!this.foreignKey) {
this.foreignKey = Utils.camelizeIf(
this.foreignKey = Utils.camelize(
[
Utils.underscoredIf(this.as, this.source.options.underscored),
this.as,
this.target.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
].join('_')
);
}
this.identifier = this.foreignKey;
if (this.source.rawAttributes[this.identifier]) {
this.identifierField = this.source.rawAttributes[this.identifier].field || this.identifier;
}
......@@ -75,7 +73,7 @@ class BelongsTo extends Association {
}
// the id is in the source table
injectAttributes() {
_injectAttributes() {
const newAttributes = {};
newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, {
......@@ -92,10 +90,10 @@ class BelongsTo extends Association {
Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.target, this.source, this.options, this.targetKeyField);
Utils.mergeDefaults(this.source.rawAttributes, newAttributes);
this.identifierField = this.source.rawAttributes[this.foreignKey].field || this.foreignKey;
this.source.refreshAttributes();
this.identifierField = this.source.rawAttributes[this.foreignKey].field || this.foreignKey;
Helpers.checkNamingCollision(this);
return this;
......
......@@ -54,8 +54,8 @@ class HasMany extends Association {
}
/*
* Foreign key setup
*/
* Foreign key setup
*/
if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
......@@ -64,12 +64,11 @@ class HasMany extends Association {
}
if (!this.foreignKey) {
this.foreignKey = Utils.camelizeIf(
this.foreignKey = Utils.camelize(
[
Utils.underscoredIf(this.source.options.name.singular, this.source.options.underscored),
this.source.options.name.singular,
this.source.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
].join('_')
);
}
......@@ -78,25 +77,25 @@ class HasMany extends Association {
this.foreignKeyField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
}
/*
* Source key setup
*/
this.sourceKey = this.options.sourceKey || this.source.primaryKeyAttribute;
if (this.target.rawAttributes[this.sourceKey]) {
this.sourceKeyField = this.source.rawAttributes[this.sourceKey].field || this.sourceKey;
} else {
this.sourceKeyField = this.sourceKey;
}
this.sourceIdentifier = this.sourceKey;
if (this.source.fieldRawAttributesMap[this.sourceKey]) {
this.sourceKeyAttribute = this.source.fieldRawAttributesMap[this.sourceKey].fieldName;
if (this.source.rawAttributes[this.sourceKey]) {
this.sourceKeyAttribute = this.sourceKey;
this.sourceKeyField = this.source.rawAttributes[this.sourceKey].field || this.sourceKey;
} else {
this.sourceKeyAttribute = this.source.primaryKeyAttribute;
this.sourceKeyField = this.source.primaryKeyField;
}
this.sourceIdentifier = this.sourceKey;
this.associationAccessor = this.as;
// Get singular and plural names, trying to uppercase the first letter, unless the model forbids it
const plural = Utils.uppercaseFirst(this.options.name.plural);
const singular = Utils.uppercaseFirst(this.options.name.singular);
this.associationAccessor = this.as;
this.accessors = {
get: 'get' + plural,
set: 'set' + plural,
......@@ -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() {
const newAttributes = {};
const constraintOptions = _.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
const constraintOptions = _.clone(this.options);
newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, {
type: this.options.keyType || this.source.rawAttributes[this.sourceKeyAttribute].type,
allowNull: true
......@@ -126,15 +127,17 @@ class HasMany extends Association {
constraintOptions.onDelete = constraintOptions.onDelete || (target.allowNull ? 'SET NULL' : 'CASCADE');
constraintOptions.onUpdate = constraintOptions.onUpdate || 'CASCADE';
}
Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.source, this.target, constraintOptions, this.sourceKeyField);
Utils.mergeDefaults(this.target.rawAttributes, newAttributes);
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
this.foreignKeyField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
this.target.refreshAttributes();
this.source.refreshAttributes();
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
this.foreignKeyField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
this.sourceKeyField = this.source.rawAttributes[this.sourceKey].field || this.sourceKey;
Helpers.checkNamingCollision(this);
return this;
......
......@@ -40,12 +40,11 @@ class HasOne extends Association {
}
if (!this.foreignKey) {
this.foreignKey = Utils.camelizeIf(
this.foreignKey = Utils.camelize(
[
Utils.underscoredIf(Utils.singularize(this.options.as || this.source.name), this.target.options.underscored),
Utils.singularize(this.options.as || this.source.name),
this.source.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
].join('_')
);
}
......@@ -71,7 +70,7 @@ class HasOne extends Association {
}
// the id is in the target table
injectAttributes() {
_injectAttributes() {
const newAttributes = {};
const keyType = this.source.rawAttributes[this.source.primaryKeyAttribute].type;
......@@ -79,9 +78,6 @@ class HasOne extends Association {
type: this.options.keyType || keyType,
allowNull: true
});
Utils.mergeDefaults(this.target.rawAttributes, newAttributes);
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
if (this.options.constraints !== false) {
const target = this.target.rawAttributes[this.foreignKey] || newAttributes[this.foreignKey];
......@@ -89,11 +85,13 @@ class HasOne extends Association {
this.options.onUpdate = this.options.onUpdate || 'CASCADE';
}
Helpers.addForeignKeyConstraints(this.target.rawAttributes[this.foreignKey], this.source, this.target, this.options);
Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.source, this.target, this.options);
Utils.mergeDefaults(this.target.rawAttributes, newAttributes);
// Sync attributes and setters/getters to Model prototype
this.target.refreshAttributes();
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
Helpers.checkNamingCollision(this);
return this;
......
'use strict';
const _ = require('lodash');
function checkNamingCollision(association) {
if (association.source.rawAttributes.hasOwnProperty(association.as)) {
throw new Error(
'Naming collision between attribute \'' + association.as +
'\' and association \'' + association.as + '\' on model ' + association.source.name +
`Naming collision between attribute '${association.as}'` +
` and association '${association.as}' on model ${association.source.name}` +
'. To remedy this, change either foreignKey or as in your association definition'
);
}
......@@ -15,14 +13,12 @@ exports.checkNamingCollision = checkNamingCollision;
function addForeignKeyConstraints(newAttribute, source, target, options, key) {
// FK constraints are opt-in: users must either set `foreignKeyConstraints`
// on the association, or request an `onDelete` or `onUpdate` behaviour
// on the association, or request an `onDelete` or `onUpdate` behavior
if (options.foreignKeyConstraint || options.onDelete || options.onUpdate) {
// Find primary keys: composite keys not supported with this approach
const primaryKeys = _.chain(source.rawAttributes).keys()
.filter(key => source.rawAttributes[key].primaryKey)
.map(key => source.rawAttributes[key].field || key).value();
const primaryKeys = Object.keys(source.primaryKeys)
.map(primaryKeyAttribute => source.rawAttributes[primaryKeyAttribute].field || primaryKeyAttribute);
if (primaryKeys.length === 1) {
if (source._schema) {
......
......@@ -7,16 +7,21 @@ const HasMany = require('./has-many');
const BelongsToMany = require('./belongs-to-many');
const BelongsTo = require('./belongs-to');
function isModel(model, sequelize) {
return model
&& model.prototype
&& model.prototype instanceof sequelize.Model;
}
const Mixin = {
hasMany(target, options) { // testhint options:none
if (!target || !target.prototype || !(target.prototype instanceof this.sequelize.Model)) {
throw new Error(this.name + '.hasMany called with something that\'s not a subclass of Sequelize.Model');
hasMany(target, options = {}) { // testhint options:none
if (!isModel(target, this.sequelize)) {
throw new Error(`${this.name}.hasMany called with something that's not a subclass of Sequelize.Model`);
}
const source = this;
// Since this is a mixin, we'll need a unique letiable name for hooks (since Model will override our hooks option)
options = options || {};
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = options.hooks;
......@@ -26,32 +31,31 @@ const Mixin = {
const association = new HasMany(source, target, options);
source.associations[association.associationAccessor] = association;
association.injectAttributes();
association._injectAttributes();
association.mixin(source.prototype);
return association;
},
belongsToMany(targetModel, options) { // testhint options:none
if (!targetModel || !targetModel.prototype || !(targetModel.prototype instanceof this.sequelize.Model)) {
throw new Error(this.name + '.belongsToMany called with something that\'s not a subclass of Sequelize.Model');
belongsToMany(target, options = {}) { // testhint options:none
if (!isModel(target, this.sequelize)) {
throw new Error(`${this.name}.belongsToMany called with something that's not a subclass of Sequelize.Model`);
}
const sourceModel = this;
const source = this;
// Since this is a mixin, we'll need a unique letiable name for hooks (since Model will override our hooks option)
options = options || {};
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = options.hooks;
options.timestamps = options.timestamps === undefined ? this.sequelize.options.timestamps : options.timestamps;
options = _.extend(options, _.omit(sourceModel.options, ['hooks', 'timestamps', 'scopes', 'defaultScope']));
options = _.extend(options, _.omit(source.options, ['hooks', 'timestamps', 'scopes', 'defaultScope']));
// the id is in the foreign table or in a connecting table
const association = new BelongsToMany(sourceModel, targetModel, options);
sourceModel.associations[association.associationAccessor] = association;
const association = new BelongsToMany(source, target, options);
source.associations[association.associationAccessor] = association;
association.injectAttributes();
association.mixin(sourceModel.prototype);
association._injectAttributes();
association.mixin(source.prototype);
return association;
},
......@@ -76,15 +80,14 @@ const Mixin = {
// The logic for hasOne and belongsTo is exactly the same
function singleLinked(Type) {
return function(target, options) { // testhint options:none
if (!target || !target.prototype || !(target.prototype instanceof this.sequelize.Model)) {
throw new Error(this.name + '.' + Utils.lowercaseFirst(Type.toString()) + ' called with something that\'s not a subclass of Sequelize.Model');
return function(target, options = {}) { // testhint options:none
if (!isModel(target, this.sequelize)) {
throw new Error(`${this.name}.${Utils.lowercaseFirst(Type.name)} called with something that's not a subclass of Sequelize.Model`);
}
const source = this;
// Since this is a mixin, we'll need a unique letiable name for hooks (since Model will override our hooks option)
options = options || {};
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = options.hooks;
......@@ -92,7 +95,7 @@ function singleLinked(Type) {
const association = new Type(source, target, _.extend(options, source.options));
source.associations[association.associationAccessor] = association;
association.injectAttributes();
association._injectAttributes();
association.mixin(source.prototype);
return association;
......@@ -100,7 +103,6 @@ function singleLinked(Type) {
}
Mixin.hasOne = singleLinked(HasOne);
Mixin.belongsTo = singleLinked(BelongsTo);
module.exports = Mixin;
......
......@@ -682,8 +682,7 @@ class Model {
* @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 {String} [options.name.singular=Utils.singularize(modelName)]
......@@ -695,9 +694,9 @@ class Model {
* @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
* @param {String} [options.schema='public']
* @param {String} [options.engine]
......@@ -747,7 +746,6 @@ class Model {
validate: {},
freezeTableName: false,
underscored: false,
underscoredAll: false,
paranoid: false,
rejectOnEmpty: false,
whereCollection: null,
......@@ -766,10 +764,10 @@ class Model {
this.associations = {};
this._setupHooks(options.hooks);
this.underscored = this.underscored || this.underscoredAll;
this.underscored = this.options.underscored;
if (!this.options.tableName) {
this.tableName = this.options.freezeTableName ? this.name : Utils.underscoredIf(Utils.pluralize(this.name), this.options.underscoredAll);
this.tableName = this.options.freezeTableName ? this.name : Utils.underscoredIf(Utils.pluralize(this.name), this.underscored);
} else {
this.tableName = this.options.tableName;
}
......@@ -789,7 +787,6 @@ class Model {
});
this.rawAttributes = _.mapValues(attributes, (attribute, name) => {
attribute = this.sequelize.normalizeAttribute(attribute);
if (attribute.type === undefined) {
......@@ -804,20 +801,22 @@ class Model {
});
this.primaryKeys = {};
this._timestampAttributes = {};
// Setup names of timestamp attributes
this._timestampAttributes = {};
if (this.options.timestamps) {
if (this.options.createdAt !== false) {
this._timestampAttributes.createdAt = this.options.createdAt || Utils.underscoredIf('createdAt', this.options.underscored);
this._timestampAttributes.createdAt = this.options.createdAt || 'createdAt';
}
if (this.options.updatedAt !== false) {
this._timestampAttributes.updatedAt = this.options.updatedAt || Utils.underscoredIf('updatedAt', this.options.underscored);
this._timestampAttributes.updatedAt = this.options.updatedAt || 'updatedAt';
}
if (this.options.paranoid && this.options.deletedAt !== false) {
this._timestampAttributes.deletedAt = this.options.deletedAt || Utils.underscoredIf('deletedAt', this.options.underscored);
this._timestampAttributes.deletedAt = this.options.deletedAt || 'deletedAt';
}
}
// Setup name for version attribute
if (this.options.version) {
this._versionAttribute = typeof this.options.version === 'string' ? this.options.version : 'version';
}
......@@ -947,7 +946,7 @@ class Model {
definition._modelAttribute = true;
if (definition.field === undefined) {
definition.field = name;
definition.field = Utils.underscoredIf(name, this.underscored);
}
if (definition.primaryKey === true) {
......
......@@ -15,8 +15,6 @@ const chai = require('chai'),
describe(Support.getTestDialectTeaser('BelongsToMany'), () => {
describe('getAssociations', () => {
beforeEach(function() {
const self = this;
this.User = this.sequelize.define('User', { username: DataTypes.STRING });
this.Task = this.sequelize.define('Task', { title: DataTypes.STRING, active: DataTypes.BOOLEAN });
......@@ -25,13 +23,13 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => {
return this.sequelize.sync({ force: true }).then(() => {
return Promise.all([
self.User.create({ username: 'John'}),
self.Task.create({ title: 'Get rich', active: true}),
self.Task.create({ title: 'Die trying', active: false})
this.User.create({ username: 'John'}),
this.Task.create({ title: 'Get rich', active: true}),
this.Task.create({ title: 'Die trying', active: false})
]);
}).spread((john, task1, task2) => {
self.tasks = [task1, task2];
self.user = john;
this.tasks = [task1, task2];
this.user = john;
return john.setTasks([task1, task2]);
});
});
......@@ -1255,8 +1253,8 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => {
const attributes = this.sequelize.model('user_places').rawAttributes;
expect(attributes.place_id).to.be.ok;
expect(attributes.user_id).to.be.ok;
expect(attributes.PlaceId.field).to.equal('place_id');
expect(attributes.UserId.field).to.equal('user_id');
});
it('should infer otherKey from paired BTM relationship with a through string defined', function() {
......@@ -2276,10 +2274,12 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => {
PersonChildren = this.sequelize.define('PersonChildren', {}, {underscored: true});
Children = Person.belongsToMany(Person, { as: 'Children', through: PersonChildren});
expect(Children.foreignKey).to.equal('person_id');
expect(Children.otherKey).to.equal('child_id');
expect(Children.foreignKey).to.equal('PersonId');
expect(Children.otherKey).to.equal('ChildId');
expect(PersonChildren.rawAttributes[Children.foreignKey]).to.be.ok;
expect(PersonChildren.rawAttributes[Children.otherKey]).to.be.ok;
expect(PersonChildren.rawAttributes[Children.foreignKey].field).to.equal('person_id');
expect(PersonChildren.rawAttributes[Children.otherKey].field).to.equal('child_id');
});
});
});
......@@ -12,7 +12,7 @@ const chai = require('chai'),
describe(Support.getTestDialectTeaser('BelongsTo'), () => {
describe('Model.associations', () => {
it('should store all assocations when associting to the same table multiple times', function() {
it('should store all associations when associating to the same table multiple times', function() {
const User = this.sequelize.define('User', {}),
Group = this.sequelize.define('Group', {});
......@@ -20,7 +20,9 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
Group.belongsTo(User, { foreignKey: 'primaryGroupId', as: 'primaryUsers' });
Group.belongsTo(User, { foreignKey: 'secondaryGroupId', as: 'secondaryUsers' });
expect(Object.keys(Group.associations)).to.deep.equal(['User', 'primaryUsers', 'secondaryUsers']);
expect(
Object.keys(Group.associations)
).to.deep.equal(['User', 'primaryUsers', 'secondaryUsers']);
});
});
......@@ -62,7 +64,6 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
});
describe('getAssociation', () => {
if (current.dialect.supports.transactions) {
it('supports transactions', function() {
return Support.prepareTransactionTest(this.sequelize).then(sequelize => {
......@@ -452,13 +453,14 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
});
describe('foreign key', () => {
it('should lowercase foreign keys when using underscored', function() {
it('should setup underscored field with foreign keys when using underscored', function() {
const User = this.sequelize.define('User', { username: Sequelize.STRING }, { underscored: true }),
Account = this.sequelize.define('Account', { name: Sequelize.STRING }, { underscored: true });
User.belongsTo(Account);
expect(User.rawAttributes.account_id).to.exist;
expect(User.rawAttributes.AccountId).to.exist;
expect(User.rawAttributes.AccountId.field).to.equal('account_id');
});
it('should use model name when using camelcase', function() {
......@@ -468,6 +470,7 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
User.belongsTo(Account);
expect(User.rawAttributes.AccountId).to.exist;
expect(User.rawAttributes.AccountId.field).to.equal('AccountId');
});
it('should support specifying the field of a foreign key', function() {
......@@ -497,15 +500,114 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
return user.getAccount();
});
}).then(user => {
// the sql query should correctly look at task_id instead of taskId
expect(user).to.not.be.null;
return User.findOne({
where: {username: 'foo'},
include: [Account]
});
}).then(task => {
expect(task.Account).to.exist;
}).then(user => {
// the sql query should correctly look at account_id instead of AccountId
expect(user.Account).to.exist;
});
});
it('should set foreignKey on foreign table', function() {
const Mail = this.sequelize.define('mail', {}, { timestamps: false });
const Entry = this.sequelize.define('entry', {}, { timestamps: false });
const User = this.sequelize.define('user', {}, { timestamps: false });
Entry.belongsTo(User, {
as: 'owner',
foreignKey: {
name: 'ownerId',
allowNull: false
}
});
Entry.belongsTo(Mail, {
as: 'mail',
foreignKey: {
name: 'mailId',
allowNull: false
}
});
Mail.belongsToMany(User, {
as: 'recipients',
through: 'MailRecipients',
otherKey: {
name: 'recipientId',
allowNull: false
},
foreignKey: {
name: 'mailId',
allowNull: false
},
timestamps: false
});
Mail.hasMany(Entry, {
as: 'entries',
foreignKey: {
name: 'mailId',
allowNull: false
}
});
User.hasMany(Entry, {
as: 'entries',
foreignKey: {
name: 'ownerId',
allowNull: false
}
});
return this.sequelize.sync({ force: true })
.then(() => User.create({}))
.then(() => Mail.create({}))
.then(mail =>
Entry.create({ mailId: mail.id, ownerId: 1 })
.then(() => Entry.create({ mailId: mail.id, ownerId: 1 }))
// set recipients
.then(() => mail.setRecipients([1]))
)
.then(() => Entry.findAndCount({
offset: 0,
limit: 10,
order: [['id', 'DESC']],
include: [
{
association: Entry.associations.mail,
include: [
{
association: Mail.associations.recipients,
through: {
where: {
recipientId: 1
}
},
required: true
}
],
required: true
}
]
})).then(result => {
expect(result.count).to.equal(2);
expect(result.rows[0].get({ plain: true })).to.deep.equal(
{
id: 2,
ownerId: 1,
mailId: 1,
mail: {
id: 1,
recipients: [{
id: 1,
MailRecipients: {
mailId: 1,
recipientId: 1
}
}]
}
}
);
});
});
});
......@@ -643,7 +745,6 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
});
});
});
}
// NOTE: mssql does not support changing an autoincrement primary key
......@@ -677,17 +778,15 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
});
});
}
});
describe('Association column', () => {
describe('association column', () => {
it('has correct type and name for non-id primary keys with non-integer type', function() {
const User = this.sequelize.define('UserPKBT', {
username: {
type: DataTypes.STRING
}
}),
self = this;
username: {
type: DataTypes.STRING
}
});
const Group = this.sequelize.define('GroupPKBT', {
name: {
......@@ -698,14 +797,14 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
User.belongsTo(Group);
return self.sequelize.sync({ force: true }).then(() => {
return this.sequelize.sync({ force: true }).then(() => {
expect(User.rawAttributes.GroupPKBTName.type).to.an.instanceof(DataTypes.STRING);
});
});
it('should support a non-primary key as the association column on a target without a primary key', function() {
const User = this.sequelize.define('User', { username: DataTypes.STRING }),
Task = this.sequelize.define('Task', { title: DataTypes.STRING });
const User = this.sequelize.define('User', { username: DataTypes.STRING });
const Task = this.sequelize.define('Task', { title: DataTypes.STRING });
User.removeAttribute('id');
Task.belongsTo(User, { foreignKey: 'user_name', targetKey: 'username'});
......@@ -782,8 +881,8 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
});
});
describe('Association options', () => {
it('can specify data type for autogenerated relational keys', function() {
describe('association options', () => {
it('can specify data type for auto-generated relational keys', function() {
const User = this.sequelize.define('UserXYZ', { username: DataTypes.STRING }),
dataTypes = [DataTypes.INTEGER, DataTypes.BIGINT, DataTypes.STRING],
self = this,
......@@ -878,97 +977,4 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
.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() {
const Mail = this.sequelize.define('mail', {}, { timestamps: false });
const Entry = this.sequelize.define('entry', {}, { timestamps: false });
const User = this.sequelize.define('user', {}, { timestamps: false });
Entry.belongsTo(User, { as: 'owner', foreignKey: { name: 'ownerId', allowNull: false } });
Entry.belongsTo(Mail, {
as: 'mail',
foreignKey: {
name: 'mailId',
allowNull: false
}
});
Mail.belongsToMany(User, {
as: 'recipients',
through: 'MailRecipients',
otherKey: {
name: 'recipientId',
allowNull: false
},
foreignKey: {
name: 'mailId',
allowNull: false
},
timestamps: false
});
Mail.hasMany(Entry, {
as: 'entries',
foreignKey: {
name: 'mailId',
allowNull: false
}
});
User.hasMany(Entry, {
as: 'entries',
foreignKey: {
name: 'ownerId',
allowNull: false
}
});
return this.sequelize.sync({ force: true })
.then(() => User.create({}))
.then(() => Mail.create({}))
.then(mail =>
Entry.create({ mailId: mail.id, ownerId: 1 })
.then(() => Entry.create({ mailId: mail.id, ownerId: 1 }))
// set recipients
.then(() => mail.setRecipients([1]))
)
.then(() => Entry.findAndCount({
offset: 0,
limit: 10,
order: [['id', 'DESC']],
include: [
{
association: Entry.associations.mail,
include: [
{
association: Mail.associations.recipients,
through: {
where: {
recipientId: 1
}
},
required: true
}
],
required: true
}
]
})).then(result => {
expect(result.count).to.equal(2);
expect(result.rows[0].get({ plain: true })).to.deep.equal(
{
id: 2,
ownerId: 1,
mailId: 1,
mail: {
id: 1,
recipients: [{
id: 1,
MailRecipients: {
mailId: 1,
recipientId: 1
}
}]
}
}
);
});
});
});
});
\ No newline at end of file
......@@ -443,7 +443,6 @@ describe(Support.getTestDialectTeaser('HasMany'), () => {
});
describe('(1:N)', () => {
describe('hasSingle', () => {
beforeEach(function() {
this.Article = this.sequelize.define('Article', { 'title': DataTypes.STRING });
......@@ -1121,7 +1120,7 @@ describe(Support.getTestDialectTeaser('HasMany'), () => {
});
});
describe('Foreign key constraints', () => {
describe('foreign key constraints', () => {
describe('1:m', () => {
it('sets null by default', function() {
const Task = this.sequelize.define('Task', { title: DataTypes.STRING }),
......@@ -1299,14 +1298,32 @@ describe(Support.getTestDialectTeaser('HasMany'), () => {
expect(tasks).to.have.length(1);
});
});
}
});
});
describe('Association options', () => {
it('can specify data type for autogenerated relational keys', function() {
it('should setup underscored field with foreign keys when using underscored', function() {
const User = this.sequelize.define('User', { username: Sequelize.STRING }, { underscored: true });
const Account = this.sequelize.define('Account', { name: Sequelize.STRING }, { underscored: true });
User.hasMany(Account);
expect(Account.rawAttributes.UserId).to.exist;
expect(Account.rawAttributes.UserId.field).to.equal('user_id');
});
it('should use model name when using camelcase', function() {
const User = this.sequelize.define('User', { username: Sequelize.STRING }, { underscored: false });
const Account = this.sequelize.define('Account', { name: Sequelize.STRING }, { underscored: false });
User.hasMany(Account);
expect(Account.rawAttributes.UserId).to.exist;
expect(Account.rawAttributes.UserId.field).to.equal('UserId');
});
it('can specify data type for auto-generated relational keys', function() {
const User = this.sequelize.define('UserXYZ', { username: DataTypes.STRING }),
dataTypes = [Sequelize.INTEGER, Sequelize.BIGINT, Sequelize.STRING],
self = this,
......@@ -1372,7 +1389,7 @@ describe(Support.getTestDialectTeaser('HasMany'), () => {
}
});
User.hasMany(Project, { foreignKey: Project.rawAttributes.user_id});
User.hasMany(Project, { foreignKey: Project.rawAttributes.user_id });
expect(Project.rawAttributes.user_id).to.be.ok;
expect(Project.rawAttributes.user_id.references.model).to.equal(User.getTableName());
......@@ -1504,7 +1521,10 @@ describe(Support.getTestDialectTeaser('HasMany'), () => {
this.Task = this.sequelize.define('Task',
{ title: Sequelize.STRING, userEmail: Sequelize.STRING, taskStatus: Sequelize.STRING });
this.User.hasMany(this.Task, {foreignKey: 'userEmail', sourceKey: 'mail'});
this.User.hasMany(this.Task, {
foreignKey: 'userEmail',
sourceKey: 'email'
});
return this.sequelize.sync({ force: true });
});
......
......@@ -10,7 +10,7 @@ const chai = require('chai'),
describe(Support.getTestDialectTeaser('HasOne'), () => {
describe('Model.associations', () => {
it('should store all assocations when associting to the same table multiple times', function() {
it('should store all associations when associating to the same table multiple times', function() {
const User = this.sequelize.define('User', {}),
Group = this.sequelize.define('Group', {});
......@@ -18,7 +18,9 @@ describe(Support.getTestDialectTeaser('HasOne'), () => {
Group.hasOne(User, { foreignKey: 'primaryGroupId', as: 'primaryUsers' });
Group.hasOne(User, { foreignKey: 'secondaryGroupId', as: 'secondaryUsers' });
expect(Object.keys(Group.associations)).to.deep.equal(['User', 'primaryUsers', 'secondaryUsers']);
expect(
Object.keys(Group.associations)
).to.deep.equal(['User', 'primaryUsers', 'secondaryUsers']);
});
});
......@@ -59,8 +61,7 @@ describe(Support.getTestDialectTeaser('HasOne'), () => {
});
});
describe('getAssocation', () => {
describe('getAssociation', () => {
if (current.dialect.supports.transactions) {
it('supports transactions', function() {
return Support.prepareTransactionTest(this.sequelize).then(sequelize => {
......@@ -386,13 +387,14 @@ describe(Support.getTestDialectTeaser('HasOne'), () => {
});
describe('foreign key', () => {
it('should lowercase foreign keys when using underscored', function() {
it('should setup underscored field with foreign keys when using underscored', function() {
const User = this.sequelize.define('User', { username: Sequelize.STRING }, { underscored: true }),
Account = this.sequelize.define('Account', { name: Sequelize.STRING }, { underscored: true });
Account.hasOne(User);
expect(User.rawAttributes.account_id).to.exist;
expect(User.rawAttributes.AccountId).to.exist;
expect(User.rawAttributes.AccountId.field).to.equal('account_id');
});
it('should use model name when using camelcase', function() {
......@@ -402,6 +404,7 @@ describe(Support.getTestDialectTeaser('HasOne'), () => {
Account.hasOne(User);
expect(User.rawAttributes.AccountId).to.exist;
expect(User.rawAttributes.AccountId.field).to.equal('AccountId');
});
it('should support specifying the field of a foreign key', function() {
......
......@@ -15,6 +15,14 @@ const Promise = current.Promise;
const AssociationError = require(__dirname + '/../../../lib/errors').AssociationError;
describe(Support.getTestDialectTeaser('belongsToMany'), () => {
it('throws when invalid model is passed', () => {
const User = current.define('User');
expect(() => {
User.belongsToMany();
}).to.throw('User.belongsToMany called with something that\'s not a subclass of Sequelize.Model');
});
it('should not inherit scopes from parent to join table', () => {
const A = current.define('a'),
B = current.define('b', {}, {
......
......@@ -7,6 +7,14 @@ const chai = require('chai'),
current = Support.sequelize;
describe(Support.getTestDialectTeaser('belongsTo'), () => {
it('throws when invalid model is passed', () => {
const User = current.define('User');
expect(() => {
User.belongsTo();
}).to.throw('User.belongsTo called with something that\'s not a subclass of Sequelize.Model');
});
it('should not override custom methods with association mixin', () => {
const methods = {
getTask: 'get',
......
......@@ -13,6 +13,14 @@ const chai = require('chai'),
Promise = current.Promise;
describe(Support.getTestDialectTeaser('hasMany'), () => {
it('throws when invalid model is passed', () => {
const User = current.define('User');
expect(() => {
User.hasMany();
}).to.throw('User.hasMany called with something that\'s not a subclass of Sequelize.Model');
});
describe('optimizations using bulk create, destroy and update', () => {
const User =current.define('User', { username: DataTypes.STRING }),
Task = current.define('Task', { title: DataTypes.STRING });
......
......@@ -8,6 +8,14 @@ const chai = require('chai'),
current = Support.sequelize;
describe(Support.getTestDialectTeaser('hasOne'), () => {
it('throws when invalid model is passed', () => {
const User = current.define('User');
expect(() => {
User.hasOne();
}).to.throw('User.hasOne called with something that\'s not a subclass of Sequelize.Model');
});
it('properly use the `as` key to generate foreign key name', () => {
const User = current.define('User', { username: DataTypes.STRING }),
Task = current.define('Task', { title: DataTypes.STRING });
......
'use strict';
const chai = require('chai'),
expect = chai.expect,
Support = require(__dirname + '/../support'),
DataTypes = require(__dirname + '/../../../lib/data-types'),
Sequelize = require('../../../index');
describe(Support.getTestDialectTeaser('Model'), () => {
describe('options.underscored', () => {
beforeEach(function() {
this.N = this.sequelize.define('N', {
id: {
type: DataTypes.CHAR(10),
primaryKey: true,
field: 'n_id'
}
}, {
underscored: true
});
this.M = this.sequelize.define('M', {
id: {
type: Sequelize.CHAR(20),
primaryKey: true,
field: 'm_id'
}
}, {
underscored: true
});
this.NM = this.sequelize.define('NM', {});
});
it('should properly set field when defining', function() {
expect(this.N.rawAttributes['id'].field).to.equal('n_id');
expect(this.M.rawAttributes['id'].field).to.equal('m_id');
});
it('hasOne does not override already defined field', function() {
this.N.rawAttributes['mId'] = {
type: Sequelize.CHAR(20),
field: 'n_m_id'
};
this.N.refreshAttributes();
expect(this.N.rawAttributes['mId'].field).to.equal('n_m_id');
this.M.hasOne(this.N, { foreignKey: 'mId' });
expect(this.N.rawAttributes['mId'].field).to.equal('n_m_id');
});
it('belongsTo does not override already defined field', function() {
this.N.rawAttributes['mId'] = {
type: Sequelize.CHAR(20),
field: 'n_m_id'
};
this.N.refreshAttributes();
expect(this.N.rawAttributes['mId'].field).to.equal('n_m_id');
this.N.belongsTo(this.M, { foreignKey: 'mId' });
expect(this.N.rawAttributes['mId'].field).to.equal('n_m_id');
});
it('hasOne/belongsTo does not override already defined field', function() {
this.N.rawAttributes['mId'] = {
type: Sequelize.CHAR(20),
field: 'n_m_id'
};
this.N.refreshAttributes();
expect(this.N.rawAttributes['mId'].field).to.equal('n_m_id');
this.N.belongsTo(this.M, { foreignKey: 'mId' });
this.M.hasOne(this.N, { foreignKey: 'mId' });
expect(this.N.rawAttributes['mId'].field).to.equal('n_m_id');
});
it('hasMany does not override already defined field', function() {
this.M.rawAttributes['nId'] = {
type: Sequelize.CHAR(20),
field: 'nana_id'
};
this.M.refreshAttributes();
expect(this.M.rawAttributes['nId'].field).to.equal('nana_id');
this.N.hasMany(this.M, { foreignKey: 'nId' });
this.M.belongsTo(this.N, { foreignKey: 'nId' });
expect(this.M.rawAttributes['nId'].field).to.equal('nana_id');
});
it('belongsToMany does not override already defined field', function() {
this.NM = this.sequelize.define('NM', {
n_id: {
type: Sequelize.CHAR(10),
field: 'nana_id'
},
m_id: {
type: Sequelize.CHAR(20),
field: 'mama_id'
}
}, {
underscored: true
});
this.N.belongsToMany(this.M, { through: this.NM, foreignKey: 'n_id' });
this.M.belongsToMany(this.N, { through: this.NM, foreignKey: 'm_id' });
expect(this.NM.rawAttributes['n_id'].field).to.equal('nana_id');
expect(this.NM.rawAttributes['m_id'].field).to.equal('mama_id');
});
});
});
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!