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

Commit a3d22627 by Pavel Evstigneev Committed by Jan Aagaard Meier

V3 Fix sourceKey for hasMany, issue: #4258 (#6652)

* Fixes hasMany issue in #4258

added a source key field to allow the source table to have a custom
foreign key that is not primary, defaults to the primary if not given

* fix mistake on belongs to

* More support for sourceKey in hasMnay

* Add docs for sourceKey in hasMnay

* Fix jslint error in has-many.test.js

* Fix failing tests

* Add unique index for sourceKey tests
1 parent 446b3d01
# Future
- [FIXED] `removeColumn` method to support dropping primaryKey column (MSSQL) [#7081](https://github.com/sequelize/sequelize/pull/7081)
- [ADDED] Support `sourceKey` for `hasMany` relationships [#4258](https://github.com/sequelize/sequelize/issues/4258)
# 3.29.0
- [FIXED] Transaction Name too long, transaction savepoints for SQL Server [#6972](https://github.com/sequelize/sequelize/pull/6972)
......
......@@ -192,6 +192,18 @@ 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 association in the next section:
Sometimes you may need to associate records on different columns, you may use `sourceKey` option:
```js
var City = sequelize.define('city', { countryCode: Sequelize.STRING });
var Country = sequelize.define('country', { isoCode: Sequelize.STRING });
// Here we can connect countries and cities base on country code
Country.hasMany(City, {foreignKey: 'countryCode', sourceKey: 'isoCode'});
City.belongsTo(Country, {foreignKey: 'countryCode', targetKey: 'isoCode'});
```
## Belongs-To-Many associations
Belongs-To-Many associations are used to connect sources with multiple targets. Furthermore the targets can also have connections to multiple sources.
......
......@@ -84,6 +84,14 @@ var HasMany = function(source, target, options) {
this.foreignKeyField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
}
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;
this.associationAccessor = this.as;
// Get singular and plural names, trying to uppercase the first letter, unless the model forbids it
......@@ -203,7 +211,7 @@ HasMany.prototype.injectAttributes = function() {
var newAttributes = {};
var 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
newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, {
type: this.options.keyType || this.source.rawAttributes[this.source.primaryKeyAttribute].type,
type: this.options.keyType || this.source.rawAttributes[this.sourceKey].type,
allowNull : true
});
......@@ -212,7 +220,7 @@ HasMany.prototype.injectAttributes = function() {
constraintOptions.onDelete = constraintOptions.onDelete || (target.allowNull ? 'SET NULL' : 'CASCADE');
constraintOptions.onUpdate = constraintOptions.onUpdate || 'CASCADE';
}
Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.source, this.target, constraintOptions);
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;
......@@ -280,7 +288,7 @@ HasMany.prototype.get = function(instances, options) {
if (instances) {
values = instances.map(function (instance) {
return instance.get(association.source.primaryKeyAttribute, {raw: true});
return instance.get(association.sourceKey, {raw: true});
});
if (options.limit && instances.length > 1) {
......@@ -298,7 +306,7 @@ HasMany.prototype.get = function(instances, options) {
delete options.groupedLimit;
}
} else {
where[association.foreignKey] = instance.get(association.source.primaryKeyAttribute, {raw: true});
where[association.foreignKey] = instance.get(association.sourceKey, {raw: true});
}
......@@ -324,7 +332,7 @@ HasMany.prototype.get = function(instances, options) {
var result = {};
instances.forEach(function (instance) {
result[instance.get(association.source.primaryKeyAttribute, {raw: true})] = [];
result[instance.get(association.sourceKey, {raw: true})] = [];
});
results.forEach(function (instance) {
......@@ -439,7 +447,7 @@ HasMany.prototype.set = function(sourceInstance, targetInstances, options) {
updateWhere = {};
update = {};
update[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute);
update[association.foreignKey] = sourceInstance.get(association.sourceKey);
_.assign(update, association.scope);
updateWhere[association.target.primaryKeyAttribute] = unassociatedObjects.map(function(unassociatedObject) {
......@@ -469,7 +477,7 @@ HasMany.prototype.add = function(sourceInstance, targetInstances, options) {
targetInstances = association.toInstanceArray(targetInstances);
update[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute);
update[association.foreignKey] = sourceInstance.get(association.sourceKey);
_.assign(update, association.scope);
where[association.target.primaryKeyAttribute] = targetInstances.map(function (unassociatedObject) {
......@@ -494,7 +502,7 @@ HasMany.prototype.remove = function(sourceInstance, targetInstances, options) {
update[association.foreignKey] = null;
where[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute);
where[association.foreignKey] = sourceInstance.get(association.sourceKey);
where[association.target.primaryKeyAttribute] = targetInstances.map(function (targetInstance) {
return targetInstance.get(association.target.primaryKeyAttribute);
});
......@@ -529,7 +537,7 @@ HasMany.prototype.create = function(sourceInstance, values, options) {
});
}
values[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute);
values[association.foreignKey] = sourceInstance.get(association.sourceKey);
if (options.fields) options.fields.push(association.foreignKey);
return association.target.create(values, options);
};
......
......@@ -162,6 +162,7 @@ Mixin.belongsTo = singleLinked(BelongsTo);
* @param {boolean} [options.hooks=false] Set to true to run before-/afterDestroy hooks when an associated model is deleted because of a cascade. For example if `User.hasOne(Profile, {onDelete: 'cascade', hooks:true})`, the before-/afterDestroy hooks for profile will be called when a user is deleted. Otherwise the profile will be deleted without invoking any hooks
* @param {string|object} [options.as] The alias of this model. If you provide a string, it should be plural, and will be singularized using node.inflection. If you want to control the singular version yourself, provide an object with `plural` and `singular` keys. See also the `name` option passed to `sequelize.define`. If you create multiple associations between the same tables, you should provide an alias to be able to distinguish between them. If you provide an alias when creating the association, you should provide the same alias when eager loading and when getting associated models. Defaults to the pluralized name of target
* @param {string|object} [options.foreignKey] The name of the foreign key in the target table or an object representing the type definition for the foreign column (see `Sequelize.define` for syntax). When using an object, you can add a `name` property to set the name of the column. Defaults to the name of source + primary key of source
* @param {string} [options.sourceKey] The name of the field to use as the key for the association in the source table. Defaults to the primary key of the source table
* @param {object} [options.scope] A key/value set that will be used for association create and find defaults on the target. (sqlite not supported for N:M)
* @param {string} [options.onDelete='SET NULL|CASCADE'] SET NULL if foreignKey allows nulls, CASCADE if otherwise
* @param {string} [options.onUpdate='CASCADE']
......
......@@ -1710,7 +1710,7 @@ var QueryGenerator = {
left.primaryKeyAttribute
, fieldLeft = association instanceof BelongsTo ?
association.identifierField :
left.rawAttributes[left.primaryKeyAttribute].field
left.rawAttributes[association.sourceIdentifier || left.primaryKeyAttribute].field
/* Attributes for the right side */
, right = include.model
......
......@@ -1226,4 +1226,63 @@ describe(Support.getTestDialectTeaser('HasMany'), function() {
.throw ('Naming collision between attribute \'user\' and association \'user\' on model user. To remedy this, change either foreignKey or as in your association definition');
});
});
describe('sourceKey', function() {
beforeEach(function() {
var User = this.sequelize.define('UserXYZ',
{ username: Sequelize.STRING, email: Sequelize.STRING },
{ indexes: [ {fields: ['email'], unique: true} ] }
);
var Task = this.sequelize.define('TaskXYZ',
{ title: Sequelize.STRING, userEmail: { type: Sequelize.STRING, field: 'user_email_xyz'} });
User.hasMany(Task, {foreignKey: 'userEmail', sourceKey: 'email', as: 'tasks'});
this.User = User;
this.Task = Task;
return this.sequelize.sync({ force: true });
});
it('should use sourceKey', function () {
var User = this.User, Task = this.Task;
return User.create({ username: 'John', email: 'john@example.com' }).then(function(user) {
return Task.create({title: 'Fix PR', userEmail: 'john@example.com'}).then(function(task) {
return user.getTasks().then(function(tasks) {
expect(tasks.length).to.equal(1);
expect(tasks[0].title).to.equal('Fix PR');
});
});
});
});
it('should count related records', function () {
var User = this.User, Task = this.Task;
return User.create({ username: 'John', email: 'john@example.com' }).then(function(user) {
return Task.create({title: 'Fix PR', userEmail: 'john@example.com'}).then(function(task) {
return user.countTasks().then(function(tasksCount) {
expect(tasksCount).to.equal(1);
});
});
});
});
it('should set right field when add relative', function () {
var User = this.User, Task = this.Task;
return User.create({ username: 'John', email: 'john@example.com' }).then(function(user) {
return Task.create({title: 'Fix PR'}).then(function(task) {
return user.addTask(task).then(function (updatedTask) {
return user.hasTask(task.id).then(function(hasTask, b) {
expect(hasTask).to.be.true;
});
});
});
});
});
});
});
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!