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

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'})
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:
## 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
// again the Project association to User
Project.hasMany(User)
 
// now comes the association between User and Project
User.hasMany(Project)
Project.belongsToMany(User);
User.belongsToMany(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
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
User.hasMany(Project, { as: 'Tasks', through: 'worker_tasks' })
Project.hasMany(User, { as: 'Workers', through: 'worker_tasks' })
User.belongsToMany(Project, { as: 'Tasks', 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
......@@ -109,13 +106,9 @@ automagically, but with aliassed assocations `through` is required.
Of course you can also define self references with hasMany:
```js
Person.hasMany(Person, { as: 'Children' })
Person.belongsToMany(Person, { as: 'Children' })
// 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.
......@@ -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:
```js
Project.hasMany(User, {through: 'project_has_users'})
User.hasMany(Project, {through: 'project_has_users'})
Project.belongsToMany(User, {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:
......@@ -136,8 +129,8 @@ UserProjects = sequelize.define('UserProjects', {
status: DataTypes.STRING
})
 
User.hasMany(Project, { through: UserProjects })
Project.hasMany(User, { through: UserProjects })
User.belongsToMany(Project, { 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
......@@ -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:
```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.
```
......@@ -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.
......@@ -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:
```js
Project.hasMany(Task)
Task.hasMany(Project)
Project.belongsToMany(Task)
Task.belongsToMany(Project)
 
Project.create()...
Task.create()...
......
......@@ -104,14 +104,14 @@ module.exports = (function() {
}
if (typeof this.through.model === 'string') {
this.through.model = this.sequelize.define(this.through.model, {}, _.extend(this.options, {
tableName: this.through.model,
indexes: {}, //we dont want indexes here (as referenced in #2416)
paranoid: false // A paranoid join table does not make sense
}));
if (this.targetAssociation) {
this.targetAssociation.through.model = this.through.model;
if (!this.sequelize.isDefined(this.through.model)) {
this.through.model = this.sequelize.define(this.through.model, {}, _.extend(this.options, {
tableName: this.through.model,
indexes: {}, //we dont want indexes here (as referenced in #2416)
paranoid: false // A paranoid join table does not make sense
}));
} else {
this.through.model = this.sequelize.model(this.through.model);
}
}
......@@ -176,7 +176,6 @@ module.exports = (function() {
}
});
// define a new model, which connects the models
var sourceKey = this.source.rawAttributes[this.source.primaryKeyAttribute]
, sourceKeyType = sourceKey.type
, sourceKeyField = sourceKey.field || this.source.primaryKeyAttribute
......@@ -186,18 +185,6 @@ module.exports = (function() {
, sourceAttribute = Utils._.defaults(this.foreignKeyAttribute, { type: sourceKeyType })
, 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) {
targetAttribute.primaryKey = sourceAttribute.primaryKey = true;
} else if (this.through.unique !== false) {
......@@ -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.foreignIdentifier] = Utils._.extend(this.through.model.rawAttributes[this.foreignIdentifier], targetAttribute);
......
......@@ -1483,18 +1483,58 @@ describe(Support.getTestDialectTeaser("BelongsToMany"), function() {
});
describe("Foreign key constraints", function() {
describe('n:m', function () {
beforeEach(function () {
this.Task = this.sequelize.define('task', { title: DataTypes.STRING });
this.User = this.sequelize.define('user', { username: DataTypes.STRING });
this.UserTasks = this.sequelize.define('tasksusers', { userId: DataTypes.INTEGER, taskId: DataTypes.INTEGER });
beforeEach(function () {
this.Task = this.sequelize.define('task', { title: DataTypes.STRING });
this.User = this.sequelize.define('user', { username: DataTypes.STRING });
this.UserTasks = this.sequelize.define('tasksusers', { userId: DataTypes.INTEGER, taskId: DataTypes.INTEGER });
});
it("can cascade deletes both ways by default", function () {
var self = this;
this.User.belongsToMany(this.Task);
this.Task.belongsToMany(this.User);
return this.sequelize.sync({ force: true }).bind({}).then(function() {
return Promise.all([
self.User.create({ id: 67, username: 'foo' }),
self.Task.create({ id: 52, title: 'task' }),
self.User.create({ id: 89, username: 'bar' }),
self.Task.create({ id: 42, title: 'kast' }),
]);
}).spread(function (user1, task1, user2, task2) {
this.user1 = user1;
this.task1 = task1;
this.user2 = user2;
this.task2 = task2;
return Promise.all([
user1.setTasks([task1]),
task2.setUsers([user2])
]);
}).then(function () {
return Promise.all([
this.user1.destroy(),
this.task2.destroy()
]);
}).then(function () {
return Promise.all([
self.sequelize.model('tasksusers').findAll({ where: { userId: this.user1.id }}),
self.sequelize.model('tasksusers').findAll({ where: { taskId: this.task2.id }})
]);
}).spread(function (tu1, tu2) {
expect(tu1).to.have.length(0);
expect(tu2).to.have.length(0);
});
});
it("can cascade deletes both ways by default", function () {
var self = this;
if (current.dialect.supports.constraints.restrict) {
it("can restrict deletes both ways", function () {
var self = this
, spy = sinon.spy();
this.User.belongsToMany(this.Task);
this.Task.belongsToMany(this.User);
this.User.belongsToMany(this.Task, { onDelete: 'RESTRICT'});
this.Task.belongsToMany(this.User, { onDelete: 'RESTRICT'});
return this.sequelize.sync({ force: true }).bind({}).then(function() {
return Promise.all([
......@@ -1514,131 +1554,89 @@ describe(Support.getTestDialectTeaser("BelongsToMany"), function() {
]);
}).then(function () {
return Promise.all([
this.user1.destroy(),
this.task2.destroy()
this.user1.destroy().catch(self.sequelize.ForeignKeyConstraintError, spy), // Fails because of RESTRICT constraint
this.task2.destroy().catch(self.sequelize.ForeignKeyConstraintError, spy)
]);
}).then(function () {
return Promise.all([
self.sequelize.model('tasksusers').findAll({ where: { userId: this.user1.id }}),
self.sequelize.model('tasksusers').findAll({ where: { taskId: this.task2.id }})
]);
}).spread(function (tu1, tu2) {
expect(tu1).to.have.length(0);
expect(tu2).to.have.length(0);
expect(spy).to.have.been.calledTwice;
});
});
if (current.dialect.supports.constraints.restrict) {
it("can restrict deletes both ways", function () {
var self = this
, spy = sinon.spy();
this.User.belongsToMany(this.Task, { onDelete: 'RESTRICT'});
this.Task.belongsToMany(this.User, { onDelete: 'RESTRICT'});
return this.sequelize.sync({ force: true }).bind({}).then(function() {
return Promise.all([
self.User.create({ id: 67, username: 'foo' }),
self.Task.create({ id: 52, title: 'task' }),
self.User.create({ id: 89, username: 'bar' }),
self.Task.create({ id: 42, title: 'kast' }),
]);
}).spread(function (user1, task1, user2, task2) {
this.user1 = user1;
this.task1 = task1;
this.user2 = user2;
this.task2 = task2;
return Promise.all([
user1.setTasks([task1]),
task2.setUsers([user2])
]);
}).then(function () {
return Promise.all([
this.user1.destroy().catch(self.sequelize.ForeignKeyConstraintError, spy), // Fails because of RESTRICT constraint
this.task2.destroy().catch(self.sequelize.ForeignKeyConstraintError, spy)
]);
}).then(function () {
expect(spy).to.have.been.calledTwice;
});
});
it("can cascade and restrict deletes", function () {
var spy = sinon.spy()
, self = this;
it("can cascade and restrict deletes", function () {
var spy = sinon.spy()
, self = this;
self.User.belongsToMany(self.Task, { onDelete: 'RESTRICT'});
self.Task.belongsToMany(self.User); // Implicit CASCADE
self.User.belongsToMany(self.Task, { onDelete: 'RESTRICT'});
self.Task.belongsToMany(self.User, { onDelete: 'CASCADE'});
return this.sequelize.sync({ force: true }).bind({}).then(function() {
return Promise.all([
self.User.create({ id: 67, username: 'foo' }),
self.Task.create({ id: 52, title: 'task' }),
self.User.create({ id: 89, username: 'bar' }),
self.Task.create({ id: 42, title: 'kast' }),
]);
}).spread(function (user1, task1, user2, task2) {
this.user1 = user1;
this.task1 = task1;
this.user2 = user2;
this.task2 = task2;
return Promise.all([
user1.setTasks([task1]),
task2.setUsers([user2])
]);
}).then(function () {
return Promise.all([
this.user1.destroy().catch(self.sequelize.ForeignKeyConstraintError, spy), // Fails because of RESTRICT constraint
this.task2.destroy()
]);
}).then(function () {
expect(spy).to.have.been.calledOnce;
return self.sequelize.model('tasksusers').findAll({ where: { taskId: this.task2.id }});
}).then(function(usertasks) {
// This should not exist because deletes cascade
expect(usertasks).to.have.length(0);
});
});
}
it("should be possible to remove all constraints", function () {
var self = this;
this.User.belongsToMany(this.Task, { constraints: false });
this.Task.belongsToMany(this.User, { constraints: false });
return this.sequelize.sync({ force: true }).bind({}).then(function() {
return Promise.all([
return this.sequelize.sync({ force: true, logging: true }).bind({}).then(function() {
return Sequelize.Promise.join(
self.User.create({ id: 67, username: 'foo' }),
self.Task.create({ id: 52, title: 'task' }),
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) {
this.user1 = user1;
this.task1 = task1;
this.user2 = user2;
this.task2 = task2;
return Promise.all([
return Sequelize.Promise.join(
user1.setTasks([task1]),
task2.setUsers([user2])
]);
);
}).then(function () {
return Promise.all([
this.user1.destroy(),
return Sequelize.Promise.join(
this.user1.destroy().catch(self.sequelize.ForeignKeyConstraintError, spy), // Fails because of RESTRICT constraint
this.task2.destroy()
]);
);
}).then(function () {
return Promise.all([
self.sequelize.model('tasksusers').findAll({ where: { userId: this.user1.id }}),
self.sequelize.model('tasksusers').findAll({ where: { taskId: this.task2.id }}),
]);
}).spread(function (ut1, ut2) {
expect(ut1).to.have.length(1);
expect(ut2).to.have.length(1);
expect(spy).to.have.been.calledOnce;
return self.sequelize.model('tasksusers').findAll({ where: { taskId: this.task2.id }});
}).then(function(usertasks) {
// This should not exist because deletes cascade
expect(usertasks).to.have.length(0);
});
});
}
it("should be possible to remove all constraints", function () {
var self = this;
this.User.belongsToMany(this.Task, { constraints: false });
this.Task.belongsToMany(this.User, { constraints: false });
return this.sequelize.sync({ force: true }).bind({}).then(function() {
return Promise.all([
self.User.create({ id: 67, username: 'foo' }),
self.Task.create({ id: 52, title: 'task' }),
self.User.create({ id: 89, username: 'bar' }),
self.Task.create({ id: 42, title: 'kast' }),
]);
}).spread(function (user1, task1, user2, task2) {
this.user1 = user1;
this.task1 = task1;
this.user2 = user2;
this.task2 = task2;
return Promise.all([
user1.setTasks([task1]),
task2.setUsers([user2])
]);
}).then(function () {
return Promise.all([
this.user1.destroy(),
this.task2.destroy()
]);
}).then(function () {
return Promise.all([
self.sequelize.model('tasksusers').findAll({ where: { userId: this.user1.id }}),
self.sequelize.model('tasksusers').findAll({ where: { taskId: this.task2.id }}),
]);
}).spread(function (ut1, ut2) {
expect(ut1).to.have.length(1);
expect(ut2).to.have.length(1);
});
});
});
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!