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

Commit ee8194e5 by Todd Wolfson Committed by Sushant

V4 Fix sourceKey for hasMany, issue: #4258 (#7113)

1 parent 2f8930f8
...@@ -91,6 +91,7 @@ ...@@ -91,6 +91,7 @@
- [FIXED] Nested query return correct result when quoteIdentifiers is false. (Postgres) [#6363](https://github.com/sequelize/sequelize/issues/6363) - [FIXED] Nested query return correct result when quoteIdentifiers is false. (Postgres) [#6363](https://github.com/sequelize/sequelize/issues/6363)
- [FIXED] Fixed an issue where changing multiple ENUM columns in PostgreSQL could break. [#6203] (https://github.com/sequelize/sequelize/issues/6203) - [FIXED] Fixed an issue where changing multiple ENUM columns in PostgreSQL could break. [#6203] (https://github.com/sequelize/sequelize/issues/6203)
- [FIXED] Add `parent`, `original` and `sql` properties to `UniqueConstraintError` - [FIXED] Add `parent`, `original` and `sql` properties to `UniqueConstraintError`
- [ADDED] Support `sourceKey` for `hasMany` relationships [#4258](https://github.com/sequelize/sequelize/issues/4258)
## BC breaks: ## BC breaks:
- Range type bounds now default to [postgres default](https://www.postgresql.org/docs/9.5/static/rangetypes.html#RANGETYPES-CONSTRUCT) `[)` (inclusive, exclusive), previously was `()` (exclusive, exclusive) - Range type bounds now default to [postgres default](https://www.postgresql.org/docs/9.5/static/rangetypes.html#RANGETYPES-CONSTRUCT) `[)` (inclusive, exclusive), previously was `()` (exclusive, exclusive)
......
...@@ -192,6 +192,18 @@ Project.hasMany(User, {as: 'Workers'}) ...@@ -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. 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: 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
Belongs-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.
......
...@@ -79,6 +79,14 @@ class HasMany extends Association { ...@@ -79,6 +79,14 @@ class HasMany extends Association {
this.foreignKeyField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey; 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; this.associationAccessor = this.as;
// Get singular and plural names, trying to uppercase the first letter, unless the model forbids it // Get singular and plural names, trying to uppercase the first letter, unless the model forbids it
...@@ -202,7 +210,7 @@ class HasMany extends Association { ...@@ -202,7 +210,7 @@ class HasMany extends Association {
const newAttributes = {}; 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 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
newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, { 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 allowNull : true
}); });
...@@ -211,7 +219,7 @@ class HasMany extends Association { ...@@ -211,7 +219,7 @@ class HasMany extends Association {
constraintOptions.onDelete = constraintOptions.onDelete || (target.allowNull ? 'SET NULL' : 'CASCADE'); constraintOptions.onDelete = constraintOptions.onDelete || (target.allowNull ? 'SET NULL' : 'CASCADE');
constraintOptions.onUpdate = constraintOptions.onUpdate || '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); Utils.mergeDefaults(this.target.rawAttributes, newAttributes);
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey; this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
...@@ -256,7 +264,7 @@ class HasMany extends Association { ...@@ -256,7 +264,7 @@ class HasMany extends Association {
} }
if (instances) { if (instances) {
values = instances.map(instance => instance.get(association.source.primaryKeyAttribute, {raw: true})); values = instances.map(instance => instance.get(association.sourceKey, {raw: true}));
if (options.limit && instances.length > 1) { if (options.limit && instances.length > 1) {
options.groupedLimit = { options.groupedLimit = {
...@@ -273,7 +281,7 @@ class HasMany extends Association { ...@@ -273,7 +281,7 @@ class HasMany extends Association {
delete options.groupedLimit; delete options.groupedLimit;
} }
} else { } else {
where[association.foreignKey] = instance.get(association.source.primaryKeyAttribute, {raw: true}); where[association.foreignKey] = instance.get(association.sourceKey, {raw: true});
} }
...@@ -299,7 +307,7 @@ class HasMany extends Association { ...@@ -299,7 +307,7 @@ class HasMany extends Association {
const result = {}; const result = {};
for (const instance of instances) { for (const instance of instances) {
result[instance.get(association.source.primaryKeyAttribute, {raw: true})] = []; result[instance.get(association.sourceKey, {raw: true})] = [];
} }
for (const instance of results) { for (const instance of results) {
...@@ -404,7 +412,7 @@ class HasMany extends Association { ...@@ -404,7 +412,7 @@ class HasMany extends Association {
updateWhere = {}; updateWhere = {};
update = {}; update = {};
update[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute); update[association.foreignKey] = sourceInstance.get(association.sourceKey);
_.assign(update, association.scope); _.assign(update, association.scope);
updateWhere[association.target.primaryKeyAttribute] = unassociatedObjects.map(unassociatedObject => updateWhere[association.target.primaryKeyAttribute] = unassociatedObjects.map(unassociatedObject =>
...@@ -434,7 +442,7 @@ class HasMany extends Association { ...@@ -434,7 +442,7 @@ class HasMany extends Association {
targetInstances = association.toInstanceArray(targetInstances); targetInstances = association.toInstanceArray(targetInstances);
update[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute); update[association.foreignKey] = sourceInstance.get(association.sourceKey);
_.assign(update, association.scope); _.assign(update, association.scope);
where[association.target.primaryKeyAttribute] = targetInstances.map(unassociatedObject => where[association.target.primaryKeyAttribute] = targetInstances.map(unassociatedObject =>
...@@ -454,7 +462,7 @@ class HasMany extends Association { ...@@ -454,7 +462,7 @@ class HasMany extends Association {
update[association.foreignKey] = null; 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(targetInstance => where[association.target.primaryKeyAttribute] = targetInstances.map(targetInstance =>
targetInstance.get(association.target.primaryKeyAttribute) targetInstance.get(association.target.primaryKeyAttribute)
); );
...@@ -484,7 +492,7 @@ class HasMany extends Association { ...@@ -484,7 +492,7 @@ class HasMany extends Association {
} }
} }
values[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute); values[association.foreignKey] = sourceInstance.get(association.sourceKey);
if (options.fields) options.fields.push(association.foreignKey); if (options.fields) options.fields.push(association.foreignKey);
return association.target.create(values, options); return association.target.create(values, options);
} }
......
...@@ -97,6 +97,7 @@ const Mixin = { ...@@ -97,6 +97,7 @@ const Mixin = {
* @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 {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.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|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 {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.onDelete='SET NULL|CASCADE'] SET NULL if foreignKey allows nulls, CASCADE if otherwise
* @param {string} [options.onUpdate='CASCADE'] * @param {string} [options.onUpdate='CASCADE']
......
...@@ -1250,7 +1250,7 @@ const QueryGenerator = { ...@@ -1250,7 +1250,7 @@ const QueryGenerator = {
left.primaryKeyAttribute; left.primaryKeyAttribute;
const fieldLeft = association instanceof BelongsTo ? const fieldLeft = association instanceof BelongsTo ?
association.identifierField : association.identifierField :
left.rawAttributes[left.primaryKeyAttribute].field; left.rawAttributes[association.sourceIdentifier || left.primaryKeyAttribute].field;
let asLeft; let asLeft;
/* Attributes for the right side */ /* Attributes for the right side */
const right = include.model; const right = include.model;
......
...@@ -1245,4 +1245,63 @@ describe(Support.getTestDialectTeaser('HasMany'), function() { ...@@ -1245,4 +1245,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'); .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!