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

Commit bd853b06 by Mick Hansen

refactor(belongsToMany): change the docs a bit and improve restriction handling

1 parent b6de183a
...@@ -81,16 +81,13 @@ Project.hasMany(User, {as: 'Workers'}) ...@@ -81,16 +81,13 @@ 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. We could just leave it the way it is and let it be a one-way association. This will add the attribute ProjectId or `project_id` to User. Instances of Project will get the accessors getWorkers and setWorkers. We could just leave it the way it is and let it be a one-way association.
But we want more! Let's define it the other way around by creating a many to many assocation in the next section: But we want more! Let's define it the other way around by creating a many to many assocation in the next section:
## Many-To-Many associations ## Belongs-To-Many associations
Many-To-Many associations are used to connect sources with multiple targets. Furthermore the targets can also have connections to multiple sources. Belongs-To-Many associations are used to connect sources with multiple targets. Furthermore the targets can also have connections to multiple sources.
```js ```js
// again the Project association to User Project.belongsToMany(User);
Project.hasMany(User) User.belongsToMany(Project);
 
// now comes the association between User and Project
User.hasMany(Project)
``` ```
This will remove the attribute `ProjectId` (or `project_id`) from User and create a new model called ProjectsUsers with the equivalent foreign keys `ProjectId`(or `project_id`) and `UserId` (or `user_id`). Whether the attributes are camelcase or not depends on the two models joined by the table (in this case User and Project). This will remove the attribute `ProjectId` (or `project_id`) from User and create a new model called ProjectsUsers with the equivalent foreign keys `ProjectId`(or `project_id`) and `UserId` (or `user_id`). Whether the attributes are camelcase or not depends on the two models joined by the table (in this case User and Project).
...@@ -99,8 +96,8 @@ This will add methods `getUsers`, `setUsers`, `addUsers` to `Project`, and `getP ...@@ -99,8 +96,8 @@ This will add methods `getUsers`, `setUsers`, `addUsers` to `Project`, and `getP
Sometimes you may want to rename your models when using them in associations. Let's define users as workers and projects as tasks by using the alias (`as`) option: Sometimes you may want to rename your models when using them in associations. Let's define users as workers and projects as tasks by using the alias (`as`) option:
```js ```js
User.hasMany(Project, { as: 'Tasks', through: 'worker_tasks' }) User.belongsToMany(Project, { as: 'Tasks', through: 'worker_tasks' })
Project.hasMany(User, { as: 'Workers', through: 'worker_tasks' }) Project.belongsToMany(User, { as: 'Workers', through: 'worker_tasks' })
``` ```
Notice how we used the `through` option together with the alias in the code above. This is needed to tell sequelize that the two `hasMany` calls are actually two sides of the same association. If you don't use an alias (as shown in the first example of this section) this matching happens Notice how we used the `through` option together with the alias in the code above. This is needed to tell sequelize that the two `hasMany` calls are actually two sides of the same association. If you don't use an alias (as shown in the first example of this section) this matching happens
...@@ -109,13 +106,9 @@ automagically, but with aliassed assocations `through` is required. ...@@ -109,13 +106,9 @@ automagically, but with aliassed assocations `through` is required.
Of course you can also define self references with hasMany: Of course you can also define self references with hasMany:
```js ```js
Person.hasMany(Person, { as: 'Children' }) Person.belongsToMany(Person, { as: 'Children' })
// This will create the table ChildrenPersons which stores the ids of the objects. // This will create the table ChildrenPersons which stores the ids of the objects.
 
// You can also reference the same Model without creating a junction
// table (but only if each object will have just one 'parent'). If you need that,
// use the option foreignKey and set through to null
Comment.hasMany(Comment, { as: 'Children', foreignKey: 'ParentId', through: null })
``` ```
By default, sequelize will handle everything related to the join table for you. However, sometimes you might want some more control over the table. This is where th e`through` options comes in handy. By default, sequelize will handle everything related to the join table for you. However, sometimes you might want some more control over the table. This is where th e`through` options comes in handy.
...@@ -123,8 +116,8 @@ By default, sequelize will handle everything related to the join table for you. ...@@ -123,8 +116,8 @@ By default, sequelize will handle everything related to the join table for you.
If you just want to control the name of the join table, you can pass a string: If you just want to control the name of the join table, you can pass a string:
```js ```js
Project.hasMany(User, {through: 'project_has_users'}) Project.belongsToMany(User, {through: 'project_has_users'})
User.hasMany(Project, {through: 'project_has_users'}) User.belongsToMany(Project, {through: 'project_has_users'})
``` ```
If you want additional attributes in your join table, you can define a model for the join table in sequelize, before you define the association, and then tell sequelize that it should use that model for joining, instead of creating a new one: If you want additional attributes in your join table, you can define a model for the join table in sequelize, before you define the association, and then tell sequelize that it should use that model for joining, instead of creating a new one:
...@@ -136,8 +129,8 @@ UserProjects = sequelize.define('UserProjects', { ...@@ -136,8 +129,8 @@ UserProjects = sequelize.define('UserProjects', {
status: DataTypes.STRING status: DataTypes.STRING
}) })
   
User.hasMany(Project, { through: UserProjects }) User.belongsToMany(Project, { through: UserProjects })
Project.hasMany(User, { through: UserProjects }) Project.belongsToMany(User, { through: UserProjects })
``` ```
To add a new project to a user and set it's status, you pass an extra object to the setter, which contains the attributes for the join table To add a new project to a user and set it's status, you pass an extra object to the setter, which contains the attributes for the join table
...@@ -166,7 +159,7 @@ By default sequelize will use the model name (the name passed to `sequelize.defi ...@@ -166,7 +159,7 @@ By default sequelize will use the model name (the name passed to `sequelize.defi
As we've already seen, you can alias models in associations using `as`. In single assocations (has one and belongs to), the alias should be singular, while for many associations (has many) it should be plural. Sequelize then uses the [inflection ][0]library to convert the alias to its singular form. However, this might not always work for irregular or non-english words. In this case, you can provide both the plural and the singular form of the alias: As we've already seen, you can alias models in associations using `as`. In single assocations (has one and belongs to), the alias should be singular, while for many associations (has many) it should be plural. Sequelize then uses the [inflection ][0]library to convert the alias to its singular form. However, this might not always work for irregular or non-english words. In this case, you can provide both the plural and the singular form of the alias:
```js ```js
User.hasMany(Project, { as: { singular: 'task', plural: 'tasks' }}) User.belongsToMany(Project, { as: { singular: 'task', plural: 'tasks' }})
// Notice that inflection has no problem singularizing tasks, this is just for illustrative purposes. // Notice that inflection has no problem singularizing tasks, this is just for illustrative purposes.
``` ```
...@@ -180,7 +173,7 @@ var Project = sequelize.define('project', attributes, { ...@@ -180,7 +173,7 @@ var Project = sequelize.define('project', attributes, {
} }
}) })
   
User.hasMany(Project); User.belongsToMany(Project);
``` ```
This will add the functions `add/set/get Tasks` to user instances. This will add the functions `add/set/get Tasks` to user instances.
...@@ -190,8 +183,8 @@ This will add the functions `add/set/get Tasks` to user instances. ...@@ -190,8 +183,8 @@ This will add the functions `add/set/get Tasks` to user instances.
Because Sequelize is doing a lot of magic, you have to call `Sequelize.sync` after setting the associations! Doing so will allow you the following: Because Sequelize is doing a lot of magic, you have to call `Sequelize.sync` after setting the associations! Doing so will allow you the following:
```js ```js
Project.hasMany(Task) Project.belongsToMany(Task)
Task.hasMany(Project) Task.belongsToMany(Project)
   
Project.create()... Project.create()...
Task.create()... Task.create()...
......
...@@ -104,14 +104,14 @@ module.exports = (function() { ...@@ -104,14 +104,14 @@ module.exports = (function() {
} }
if (typeof this.through.model === 'string') { if (typeof this.through.model === 'string') {
if (!this.sequelize.isDefined(this.through.model)) {
this.through.model = this.sequelize.define(this.through.model, {}, _.extend(this.options, { this.through.model = this.sequelize.define(this.through.model, {}, _.extend(this.options, {
tableName: this.through.model, tableName: this.through.model,
indexes: {}, //we dont want indexes here (as referenced in #2416) indexes: {}, //we dont want indexes here (as referenced in #2416)
paranoid: false // A paranoid join table does not make sense paranoid: false // A paranoid join table does not make sense
})); }));
} else {
if (this.targetAssociation) { this.through.model = this.sequelize.model(this.through.model);
this.targetAssociation.through.model = this.through.model;
} }
} }
...@@ -176,7 +176,6 @@ module.exports = (function() { ...@@ -176,7 +176,6 @@ module.exports = (function() {
} }
}); });
// define a new model, which connects the models
var sourceKey = this.source.rawAttributes[this.source.primaryKeyAttribute] var sourceKey = this.source.rawAttributes[this.source.primaryKeyAttribute]
, sourceKeyType = sourceKey.type , sourceKeyType = sourceKey.type
, sourceKeyField = sourceKey.field || this.source.primaryKeyAttribute , sourceKeyField = sourceKey.field || this.source.primaryKeyAttribute
...@@ -186,18 +185,6 @@ module.exports = (function() { ...@@ -186,18 +185,6 @@ module.exports = (function() {
, sourceAttribute = Utils._.defaults(this.foreignKeyAttribute, { type: sourceKeyType }) , sourceAttribute = Utils._.defaults(this.foreignKeyAttribute, { type: sourceKeyType })
, targetAttribute = Utils._.defaults(this.otherKeyAttribute, { type: targetKeyType }); , targetAttribute = Utils._.defaults(this.otherKeyAttribute, { type: targetKeyType });
if (this.options.constraints !== false) {
sourceAttribute.references = this.source.getTableName();
sourceAttribute.referencesKey = sourceKeyField;
sourceAttribute.onDelete = this.options.onDelete || 'CASCADE';
sourceAttribute.onUpdate = this.options.onUpdate || 'CASCADE';
targetAttribute.references = this.target.getTableName();
targetAttribute.referencesKey = targetKeyField;
targetAttribute.onDelete = this.options.onDelete || 'CASCADE';
targetAttribute.onUpdate = this.options.onUpdate || 'CASCADE';
}
if (this.primaryKeyDeleted === true) { if (this.primaryKeyDeleted === true) {
targetAttribute.primaryKey = sourceAttribute.primaryKey = true; targetAttribute.primaryKey = sourceAttribute.primaryKey = true;
} else if (this.through.unique !== false) { } else if (this.through.unique !== false) {
...@@ -217,6 +204,27 @@ module.exports = (function() { ...@@ -217,6 +204,27 @@ module.exports = (function() {
}; };
} }
if (this.options.constraints !== false) {
sourceAttribute.references = this.source.getTableName();
sourceAttribute.referencesKey = sourceKeyField;
// For the source attribute the passed option is the priority
sourceAttribute.onDelete = this.options.onDelete || this.through.model.rawAttributes[this.identifier].onDelete;
sourceAttribute.onUpdate = this.options.onUpdate || this.through.model.rawAttributes[this.identifier].onUpdate;
if (!sourceAttribute.onDelete) sourceAttribute.onDelete = 'CASCADE';
if (!sourceAttribute.onUpdate) sourceAttribute.onUpdate = 'CASCADE';
targetAttribute.references = this.target.getTableName();
targetAttribute.referencesKey = targetKeyField;
// But the for target attribute the previously defined option is the priority (since it could've been set by another belongsToMany call)
targetAttribute.onDelete = this.through.model.rawAttributes[this.foreignIdentifier].onDelete || this.options.onDelete;
targetAttribute.onUpdate = this.through.model.rawAttributes[this.foreignIdentifier].onUpdate || this.options.onUpdate;
if (!targetAttribute.onDelete) sourceAttribute.onDelete = 'CASCADE';
if (!targetAttribute.onUpdate) sourceAttribute.onUpdate = 'CASCADE';
}
this.through.model.rawAttributes[this.identifier] = Utils._.extend(this.through.model.rawAttributes[this.identifier], sourceAttribute); this.through.model.rawAttributes[this.identifier] = Utils._.extend(this.through.model.rawAttributes[this.identifier], sourceAttribute);
this.through.model.rawAttributes[this.foreignIdentifier] = Utils._.extend(this.through.model.rawAttributes[this.foreignIdentifier], targetAttribute); this.through.model.rawAttributes[this.foreignIdentifier] = Utils._.extend(this.through.model.rawAttributes[this.foreignIdentifier], targetAttribute);
......
...@@ -1483,7 +1483,6 @@ describe(Support.getTestDialectTeaser("BelongsToMany"), function() { ...@@ -1483,7 +1483,6 @@ describe(Support.getTestDialectTeaser("BelongsToMany"), function() {
}); });
describe("Foreign key constraints", function() { describe("Foreign key constraints", function() {
describe('n:m', function () {
beforeEach(function () { beforeEach(function () {
this.Task = this.sequelize.define('task', { title: DataTypes.STRING }); this.Task = this.sequelize.define('task', { title: DataTypes.STRING });
this.User = this.sequelize.define('user', { username: DataTypes.STRING }); this.User = this.sequelize.define('user', { username: DataTypes.STRING });
...@@ -1568,29 +1567,29 @@ describe(Support.getTestDialectTeaser("BelongsToMany"), function() { ...@@ -1568,29 +1567,29 @@ describe(Support.getTestDialectTeaser("BelongsToMany"), function() {
, self = this; , self = this;
self.User.belongsToMany(self.Task, { onDelete: 'RESTRICT'}); self.User.belongsToMany(self.Task, { onDelete: 'RESTRICT'});
self.Task.belongsToMany(self.User); // Implicit CASCADE self.Task.belongsToMany(self.User, { onDelete: 'CASCADE'});
return this.sequelize.sync({ force: true }).bind({}).then(function() { return this.sequelize.sync({ force: true, logging: true }).bind({}).then(function() {
return Promise.all([ return Sequelize.Promise.join(
self.User.create({ id: 67, username: 'foo' }), self.User.create({ id: 67, username: 'foo' }),
self.Task.create({ id: 52, title: 'task' }), self.Task.create({ id: 52, title: 'task' }),
self.User.create({ id: 89, username: 'bar' }), self.User.create({ id: 89, username: 'bar' }),
self.Task.create({ id: 42, title: 'kast' }), self.Task.create({ id: 42, title: 'kast' })
]); );
}).spread(function (user1, task1, user2, task2) { }).spread(function (user1, task1, user2, task2) {
this.user1 = user1; this.user1 = user1;
this.task1 = task1; this.task1 = task1;
this.user2 = user2; this.user2 = user2;
this.task2 = task2; this.task2 = task2;
return Promise.all([ return Sequelize.Promise.join(
user1.setTasks([task1]), user1.setTasks([task1]),
task2.setUsers([user2]) task2.setUsers([user2])
]); );
}).then(function () { }).then(function () {
return Promise.all([ return Sequelize.Promise.join(
this.user1.destroy().catch(self.sequelize.ForeignKeyConstraintError, spy), // Fails because of RESTRICT constraint this.user1.destroy().catch(self.sequelize.ForeignKeyConstraintError, spy), // Fails because of RESTRICT constraint
this.task2.destroy() this.task2.destroy()
]); );
}).then(function () { }).then(function () {
expect(spy).to.have.been.calledOnce; expect(spy).to.have.been.calledOnce;
return self.sequelize.model('tasksusers').findAll({ where: { taskId: this.task2.id }}); return self.sequelize.model('tasksusers').findAll({ where: { taskId: this.task2.id }});
...@@ -1640,7 +1639,6 @@ describe(Support.getTestDialectTeaser("BelongsToMany"), function() { ...@@ -1640,7 +1639,6 @@ describe(Support.getTestDialectTeaser("BelongsToMany"), function() {
}); });
}); });
}); });
});
describe("Association options", function() { describe("Association options", function() {
describe('allows the user to provide an attribute definition object as foreignKey', function () { describe('allows the user to provide an attribute definition object as foreignKey', function () {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!