Skip to content

Commit 83e263b

Browse files
swarthysushantdhiman
authored andcommitted
feat(associations): source and target key support for belongs-to-many (#11311)
1 parent 4f09899 commit 83e263b

File tree

7 files changed

+850
-70
lines changed

7 files changed

+850
-70
lines changed

docs/associations.md

+64
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,70 @@ Person.belongsToMany(Person, { as: 'Children', through: 'PersonChildren' })
531531

532532
```
533533

534+
#### Source and target keys
535+
536+
If you want to create a belongs to many relationship that does not use the default primary key some setup work is required.
537+
You must set the `sourceKey` (optionally `targetKey`) appropriately for the two ends of the belongs to many. Further you must also ensure you have appropriate indexes created on your relationships. For example:
538+
539+
```js
540+
const User = this.sequelize.define('User', {
541+
id: {
542+
type: DataTypes.UUID,
543+
allowNull: false,
544+
primaryKey: true,
545+
defaultValue: DataTypes.UUIDV4,
546+
field: 'user_id'
547+
},
548+
userSecondId: {
549+
type: DataTypes.UUID,
550+
allowNull: false,
551+
defaultValue: DataTypes.UUIDV4,
552+
field: 'user_second_id'
553+
}
554+
}, {
555+
tableName: 'tbl_user',
556+
indexes: [
557+
{
558+
unique: true,
559+
fields: ['user_second_id']
560+
}
561+
]
562+
});
563+
564+
const Group = this.sequelize.define('Group', {
565+
id: {
566+
type: DataTypes.UUID,
567+
allowNull: false,
568+
primaryKey: true,
569+
defaultValue: DataTypes.UUIDV4,
570+
field: 'group_id'
571+
},
572+
groupSecondId: {
573+
type: DataTypes.UUID,
574+
allowNull: false,
575+
defaultValue: DataTypes.UUIDV4,
576+
field: 'group_second_id'
577+
}
578+
}, {
579+
tableName: 'tbl_group',
580+
indexes: [
581+
{
582+
unique: true,
583+
fields: ['group_second_id']
584+
}
585+
]
586+
});
587+
588+
User.belongsToMany(Group, {
589+
through: 'usergroups',
590+
sourceKey: 'userSecondId'
591+
});
592+
Group.belongsToMany(User, {
593+
through: 'usergroups',
594+
sourceKey: 'groupSecondId'
595+
});
596+
```
597+
534598
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:
535599

536600
```js

lib/associations/belongs-to-many.js

+92-55
Original file line numberDiff line numberDiff line change
@@ -110,43 +110,6 @@ class BelongsToMany extends Association {
110110
this.targetAssociation = this;
111111
}
112112

113-
/*
114-
* Default/generated foreign/other keys
115-
*/
116-
if (_.isObject(this.options.foreignKey)) {
117-
this.foreignKeyAttribute = this.options.foreignKey;
118-
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
119-
} else {
120-
if (!this.options.foreignKey) {
121-
this.foreignKeyDefault = true;
122-
}
123-
124-
this.foreignKeyAttribute = {};
125-
this.foreignKey = this.options.foreignKey || Utils.camelize(
126-
[
127-
this.source.options.name.singular,
128-
this.source.primaryKeyAttribute
129-
].join('_')
130-
);
131-
}
132-
133-
if (_.isObject(this.options.otherKey)) {
134-
this.otherKeyAttribute = this.options.otherKey;
135-
this.otherKey = this.otherKeyAttribute.name || this.otherKeyAttribute.fieldName;
136-
} else {
137-
if (!this.options.otherKey) {
138-
this.otherKeyDefault = true;
139-
}
140-
141-
this.otherKeyAttribute = {};
142-
this.otherKey = this.options.otherKey || Utils.camelize(
143-
[
144-
this.isSelfAssociation ? Utils.singularize(this.as) : this.target.options.name.singular,
145-
this.target.primaryKeyAttribute
146-
].join('_')
147-
);
148-
}
149-
150113
/*
151114
* Find paired association (if exists)
152115
*/
@@ -160,6 +123,23 @@ class BelongsToMany extends Association {
160123
}
161124
});
162125

126+
/*
127+
* Default/generated source/target keys
128+
*/
129+
this.sourceKey = this.options.sourceKey || this.source.primaryKeyAttribute;
130+
this.sourceKeyField = this.source.rawAttributes[this.sourceKey].field || this.sourceKey;
131+
132+
if (this.options.targetKey) {
133+
this.targetKey = this.options.targetKey;
134+
this.targetKeyField = this.target.rawAttributes[this.targetKey].field || this.targetKey;
135+
} else {
136+
this.targetKeyDefault = true;
137+
this.targetKey = this.target.primaryKeyAttribute;
138+
this.targetKeyField = this.target.rawAttributes[this.targetKey].field || this.targetKey;
139+
}
140+
141+
this._createForeignAndOtherKeys();
142+
163143
if (typeof this.through.model === 'string') {
164144
if (!this.sequelize.isDefined(this.through.model)) {
165145
this.through.model = this.sequelize.define(this.through.model, {}, Object.assign(this.options, {
@@ -178,6 +158,25 @@ class BelongsToMany extends Association {
178158
]));
179159

180160
if (this.paired) {
161+
let needInjectPaired = false;
162+
163+
if (this.targetKeyDefault) {
164+
this.targetKey = this.paired.sourceKey;
165+
this.targetKeyField = this.paired.sourceKeyField;
166+
this._createForeignAndOtherKeys();
167+
}
168+
if (this.paired.targetKeyDefault) {
169+
// in this case paired.otherKey depends on paired.targetKey,
170+
// so cleanup previously wrong generated otherKey
171+
if (this.paired.targetKey !== this.sourceKey) {
172+
delete this.through.model.rawAttributes[this.paired.otherKey];
173+
this.paired.targetKey = this.sourceKey;
174+
this.paired.targetKeyField = this.sourceKeyField;
175+
this.paired._createForeignAndOtherKeys();
176+
needInjectPaired = true;
177+
}
178+
}
179+
181180
if (this.otherKeyDefault) {
182181
this.otherKey = this.paired.foreignKey;
183182
}
@@ -187,9 +186,13 @@ class BelongsToMany extends Association {
187186
if (this.paired.otherKey !== this.foreignKey) {
188187
delete this.through.model.rawAttributes[this.paired.otherKey];
189188
this.paired.otherKey = this.foreignKey;
190-
this.paired._injectAttributes();
189+
needInjectPaired = true;
191190
}
192191
}
192+
193+
if (needInjectPaired) {
194+
this.paired._injectAttributes();
195+
}
193196
}
194197

195198
if (this.through) {
@@ -218,6 +221,41 @@ class BelongsToMany extends Association {
218221
};
219222
}
220223

224+
_createForeignAndOtherKeys() {
225+
/*
226+
* Default/generated foreign/other keys
227+
*/
228+
if (_.isObject(this.options.foreignKey)) {
229+
this.foreignKeyAttribute = this.options.foreignKey;
230+
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
231+
} else {
232+
this.foreignKeyAttribute = {};
233+
this.foreignKey = this.options.foreignKey || Utils.camelize(
234+
[
235+
this.source.options.name.singular,
236+
this.sourceKey
237+
].join('_')
238+
);
239+
}
240+
241+
if (_.isObject(this.options.otherKey)) {
242+
this.otherKeyAttribute = this.options.otherKey;
243+
this.otherKey = this.otherKeyAttribute.name || this.otherKeyAttribute.fieldName;
244+
} else {
245+
if (!this.options.otherKey) {
246+
this.otherKeyDefault = true;
247+
}
248+
249+
this.otherKeyAttribute = {};
250+
this.otherKey = this.options.otherKey || Utils.camelize(
251+
[
252+
this.isSelfAssociation ? Utils.singularize(this.as) : this.target.options.name.singular,
253+
this.targetKey
254+
].join('_')
255+
);
256+
}
257+
}
258+
221259
// the id is in the target table
222260
// or in an extra table which connects two tables
223261
_injectAttributes() {
@@ -240,12 +278,12 @@ class BelongsToMany extends Association {
240278
}
241279
});
242280

243-
const sourceKey = this.source.rawAttributes[this.source.primaryKeyAttribute];
281+
const sourceKey = this.source.rawAttributes[this.sourceKey];
244282
const sourceKeyType = sourceKey.type;
245-
const sourceKeyField = sourceKey.field || this.source.primaryKeyAttribute;
246-
const targetKey = this.target.rawAttributes[this.target.primaryKeyAttribute];
283+
const sourceKeyField = this.sourceKeyField;
284+
const targetKey = this.target.rawAttributes[this.targetKey];
247285
const targetKeyType = targetKey.type;
248-
const targetKeyField = targetKey.field || this.target.primaryKeyAttribute;
286+
const targetKeyField = this.targetKeyField;
249287
const sourceAttribute = _.defaults({}, this.foreignKeyAttribute, { type: sourceKeyType });
250288
const targetAttribute = _.defaults({}, this.otherKeyAttribute, { type: targetKeyType });
251289

@@ -393,7 +431,7 @@ class BelongsToMany extends Association {
393431

394432
if (Object(through.model) === through.model) {
395433
throughWhere = {};
396-
throughWhere[this.foreignKey] = instance.get(this.source.primaryKeyAttribute);
434+
throughWhere[this.foreignKey] = instance.get(this.sourceKey);
397435

398436
if (through.scope) {
399437
Object.assign(throughWhere, through.scope);
@@ -442,12 +480,11 @@ class BelongsToMany extends Association {
442480
* @returns {Promise<number>}
443481
*/
444482
count(instance, options) {
445-
const model = this.target;
446-
const sequelize = model.sequelize;
483+
const sequelize = this.target.sequelize;
447484

448485
options = Utils.cloneDeep(options);
449486
options.attributes = [
450-
[sequelize.fn('COUNT', sequelize.col([this.target.name, model.primaryKeyField].join('.'))), 'count']
487+
[sequelize.fn('COUNT', sequelize.col([this.target.name, this.targetKeyField].join('.'))), 'count']
451488
];
452489
options.joinTableAttributes = [];
453490
options.raw = true;
@@ -474,7 +511,7 @@ class BelongsToMany extends Association {
474511
raw: true
475512
}, options, {
476513
scope: false,
477-
attributes: [this.target.primaryKeyAttribute],
514+
attributes: [this.targetKey],
478515
joinTableAttributes: []
479516
});
480517

@@ -483,7 +520,7 @@ class BelongsToMany extends Association {
483520
return instance.where();
484521
}
485522
return {
486-
[this.target.primaryKeyAttribute]: instance
523+
[this.targetKey]: instance
487524
};
488525
});
489526

@@ -495,7 +532,7 @@ class BelongsToMany extends Association {
495532
};
496533

497534
return this.get(sourceInstance, options).then(associatedObjects =>
498-
_.differenceBy(instancePrimaryKeys, associatedObjects, this.target.primaryKeyAttribute).length === 0
535+
_.differenceBy(instancePrimaryKeys, associatedObjects, this.targetKey).length === 0
499536
);
500537
}
501538

@@ -514,8 +551,8 @@ class BelongsToMany extends Association {
514551
set(sourceInstance, newAssociatedObjects, options) {
515552
options = options || {};
516553

517-
const sourceKey = this.source.primaryKeyAttribute;
518-
const targetKey = this.target.primaryKeyAttribute;
554+
const sourceKey = this.sourceKey;
555+
const targetKey = this.targetKey;
519556
const identifier = this.identifier;
520557
const foreignIdentifier = this.foreignIdentifier;
521558
let where = {};
@@ -626,8 +663,8 @@ class BelongsToMany extends Association {
626663
options = _.clone(options) || {};
627664

628665
const association = this;
629-
const sourceKey = association.source.primaryKeyAttribute;
630-
const targetKey = association.target.primaryKeyAttribute;
666+
const sourceKey = association.sourceKey;
667+
const targetKey = association.targetKey;
631668
const identifier = association.identifier;
632669
const foreignIdentifier = association.foreignIdentifier;
633670
const defaultAttributes = options.through || {};
@@ -721,8 +758,8 @@ class BelongsToMany extends Association {
721758
oldAssociatedObjects = association.toInstanceArray(oldAssociatedObjects);
722759

723760
const where = {
724-
[association.identifier]: sourceInstance.get(association.source.primaryKeyAttribute),
725-
[association.foreignIdentifier]: oldAssociatedObjects.map(newInstance => newInstance.get(association.target.primaryKeyAttribute))
761+
[association.identifier]: sourceInstance.get(association.sourceKey),
762+
[association.foreignIdentifier]: oldAssociatedObjects.map(newInstance => newInstance.get(association.targetKey))
726763
};
727764

728765
return association.through.model.destroy(_.defaults({ where }, options));

lib/dialects/abstract/query-generator.js

+4-6
Original file line numberDiff line numberDiff line change
@@ -1666,13 +1666,11 @@ class QueryGenerator {
16661666
);
16671667
const association = include.association;
16681668
const parentIsTop = !include.parent.association && include.parent.model.name === topLevelInfo.options.model.name;
1669-
const primaryKeysSource = association.source.primaryKeyAttributes;
16701669
const tableSource = parentTableName;
16711670
const identSource = association.identifierField;
1672-
const primaryKeysTarget = association.target.primaryKeyAttributes;
16731671
const tableTarget = includeAs.internalAs;
16741672
const identTarget = association.foreignIdentifierField;
1675-
const attrTarget = association.target.rawAttributes[primaryKeysTarget[0]].field || primaryKeysTarget[0];
1673+
const attrTarget = association.targetKeyField;
16761674

16771675
const joinType = include.required ? 'INNER JOIN' : 'LEFT OUTER JOIN';
16781676
let joinBody;
@@ -1681,7 +1679,7 @@ class QueryGenerator {
16811679
main: [],
16821680
subQuery: []
16831681
};
1684-
let attrSource = primaryKeysSource[0];
1682+
let attrSource = association.sourceKey;
16851683
let sourceJoinOn;
16861684
let targetJoinOn;
16871685
let throughWhere;
@@ -1696,10 +1694,10 @@ class QueryGenerator {
16961694

16971695
// Figure out if we need to use field or attribute
16981696
if (!topLevelInfo.subQuery) {
1699-
attrSource = association.source.rawAttributes[primaryKeysSource[0]].field;
1697+
attrSource = association.sourceKeyField;
17001698
}
17011699
if (topLevelInfo.subQuery && !include.subQuery && !include.parent.subQuery && include.parent.model !== topLevelInfo.options.mainModel) {
1702-
attrSource = association.source.rawAttributes[primaryKeysSource[0]].field;
1700+
attrSource = association.sourceKeyField;
17031701
}
17041702

17051703
// Filter statement for left side of through

0 commit comments

Comments
 (0)