Skip to content

Commit

Permalink
feat(associations): source and target key support for belongs-to-many (
Browse files Browse the repository at this point in the history
  • Loading branch information
swarthy authored and sushantdhiman committed Aug 14, 2019
1 parent 4f09899 commit 83e263b
Show file tree
Hide file tree
Showing 7 changed files with 850 additions and 70 deletions.
64 changes: 64 additions & 0 deletions docs/associations.md
Expand Up @@ -531,6 +531,70 @@ Person.belongsToMany(Person, { as: 'Children', through: 'PersonChildren' })

```

#### Source and target keys

If you want to create a belongs to many relationship that does not use the default primary key some setup work is required.
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:

```js
const User = this.sequelize.define('User', {
id: {
type: DataTypes.UUID,
allowNull: false,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
field: 'user_id'
},
userSecondId: {
type: DataTypes.UUID,
allowNull: false,
defaultValue: DataTypes.UUIDV4,
field: 'user_second_id'
}
}, {
tableName: 'tbl_user',
indexes: [
{
unique: true,
fields: ['user_second_id']
}
]
});

const Group = this.sequelize.define('Group', {
id: {
type: DataTypes.UUID,
allowNull: false,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
field: 'group_id'
},
groupSecondId: {
type: DataTypes.UUID,
allowNull: false,
defaultValue: DataTypes.UUIDV4,
field: 'group_second_id'
}
}, {
tableName: 'tbl_group',
indexes: [
{
unique: true,
fields: ['group_second_id']
}
]
});

User.belongsToMany(Group, {
through: 'usergroups',
sourceKey: 'userSecondId'
});
Group.belongsToMany(User, {
through: 'usergroups',
sourceKey: 'groupSecondId'
});
```

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:

```js
Expand Down
147 changes: 92 additions & 55 deletions lib/associations/belongs-to-many.js
Expand Up @@ -110,43 +110,6 @@ class BelongsToMany extends Association {
this.targetAssociation = this;
}

/*
* Default/generated foreign/other keys
*/
if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else {
if (!this.options.foreignKey) {
this.foreignKeyDefault = true;
}

this.foreignKeyAttribute = {};
this.foreignKey = this.options.foreignKey || Utils.camelize(
[
this.source.options.name.singular,
this.source.primaryKeyAttribute
].join('_')
);
}

if (_.isObject(this.options.otherKey)) {
this.otherKeyAttribute = this.options.otherKey;
this.otherKey = this.otherKeyAttribute.name || this.otherKeyAttribute.fieldName;
} else {
if (!this.options.otherKey) {
this.otherKeyDefault = true;
}

this.otherKeyAttribute = {};
this.otherKey = this.options.otherKey || Utils.camelize(
[
this.isSelfAssociation ? Utils.singularize(this.as) : this.target.options.name.singular,
this.target.primaryKeyAttribute
].join('_')
);
}

/*
* Find paired association (if exists)
*/
Expand All @@ -160,6 +123,23 @@ class BelongsToMany extends Association {
}
});

/*
* Default/generated source/target keys
*/
this.sourceKey = this.options.sourceKey || this.source.primaryKeyAttribute;
this.sourceKeyField = this.source.rawAttributes[this.sourceKey].field || this.sourceKey;

if (this.options.targetKey) {
this.targetKey = this.options.targetKey;
this.targetKeyField = this.target.rawAttributes[this.targetKey].field || this.targetKey;
} else {
this.targetKeyDefault = true;
this.targetKey = this.target.primaryKeyAttribute;
this.targetKeyField = this.target.rawAttributes[this.targetKey].field || this.targetKey;
}

this._createForeignAndOtherKeys();

if (typeof this.through.model === 'string') {
if (!this.sequelize.isDefined(this.through.model)) {
this.through.model = this.sequelize.define(this.through.model, {}, Object.assign(this.options, {
Expand All @@ -178,6 +158,25 @@ class BelongsToMany extends Association {
]));

if (this.paired) {
let needInjectPaired = false;

if (this.targetKeyDefault) {
this.targetKey = this.paired.sourceKey;
this.targetKeyField = this.paired.sourceKeyField;
this._createForeignAndOtherKeys();
}
if (this.paired.targetKeyDefault) {
// in this case paired.otherKey depends on paired.targetKey,
// so cleanup previously wrong generated otherKey
if (this.paired.targetKey !== this.sourceKey) {
delete this.through.model.rawAttributes[this.paired.otherKey];
this.paired.targetKey = this.sourceKey;
this.paired.targetKeyField = this.sourceKeyField;
this.paired._createForeignAndOtherKeys();
needInjectPaired = true;
}
}

if (this.otherKeyDefault) {
this.otherKey = this.paired.foreignKey;
}
Expand All @@ -187,9 +186,13 @@ class BelongsToMany extends Association {
if (this.paired.otherKey !== this.foreignKey) {
delete this.through.model.rawAttributes[this.paired.otherKey];
this.paired.otherKey = this.foreignKey;
this.paired._injectAttributes();
needInjectPaired = true;
}
}

if (needInjectPaired) {
this.paired._injectAttributes();
}
}

if (this.through) {
Expand Down Expand Up @@ -218,6 +221,41 @@ class BelongsToMany extends Association {
};
}

_createForeignAndOtherKeys() {
/*
* Default/generated foreign/other keys
*/
if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else {
this.foreignKeyAttribute = {};
this.foreignKey = this.options.foreignKey || Utils.camelize(
[
this.source.options.name.singular,
this.sourceKey
].join('_')
);
}

if (_.isObject(this.options.otherKey)) {
this.otherKeyAttribute = this.options.otherKey;
this.otherKey = this.otherKeyAttribute.name || this.otherKeyAttribute.fieldName;
} else {
if (!this.options.otherKey) {
this.otherKeyDefault = true;
}

this.otherKeyAttribute = {};
this.otherKey = this.options.otherKey || Utils.camelize(
[
this.isSelfAssociation ? Utils.singularize(this.as) : this.target.options.name.singular,
this.targetKey
].join('_')
);
}
}

// the id is in the target table
// or in an extra table which connects two tables
_injectAttributes() {
Expand All @@ -240,12 +278,12 @@ class BelongsToMany extends Association {
}
});

const sourceKey = this.source.rawAttributes[this.source.primaryKeyAttribute];
const sourceKey = this.source.rawAttributes[this.sourceKey];
const sourceKeyType = sourceKey.type;
const sourceKeyField = sourceKey.field || this.source.primaryKeyAttribute;
const targetKey = this.target.rawAttributes[this.target.primaryKeyAttribute];
const sourceKeyField = this.sourceKeyField;
const targetKey = this.target.rawAttributes[this.targetKey];
const targetKeyType = targetKey.type;
const targetKeyField = targetKey.field || this.target.primaryKeyAttribute;
const targetKeyField = this.targetKeyField;
const sourceAttribute = _.defaults({}, this.foreignKeyAttribute, { type: sourceKeyType });
const targetAttribute = _.defaults({}, this.otherKeyAttribute, { type: targetKeyType });

Expand Down Expand Up @@ -393,7 +431,7 @@ class BelongsToMany extends Association {

if (Object(through.model) === through.model) {
throughWhere = {};
throughWhere[this.foreignKey] = instance.get(this.source.primaryKeyAttribute);
throughWhere[this.foreignKey] = instance.get(this.sourceKey);

if (through.scope) {
Object.assign(throughWhere, through.scope);
Expand Down Expand Up @@ -442,12 +480,11 @@ class BelongsToMany extends Association {
* @returns {Promise<number>}
*/
count(instance, options) {
const model = this.target;
const sequelize = model.sequelize;
const sequelize = this.target.sequelize;

options = Utils.cloneDeep(options);
options.attributes = [
[sequelize.fn('COUNT', sequelize.col([this.target.name, model.primaryKeyField].join('.'))), 'count']
[sequelize.fn('COUNT', sequelize.col([this.target.name, this.targetKeyField].join('.'))), 'count']
];
options.joinTableAttributes = [];
options.raw = true;
Expand All @@ -474,7 +511,7 @@ class BelongsToMany extends Association {
raw: true
}, options, {
scope: false,
attributes: [this.target.primaryKeyAttribute],
attributes: [this.targetKey],
joinTableAttributes: []
});

Expand All @@ -483,7 +520,7 @@ class BelongsToMany extends Association {
return instance.where();
}
return {
[this.target.primaryKeyAttribute]: instance
[this.targetKey]: instance
};
});

Expand All @@ -495,7 +532,7 @@ class BelongsToMany extends Association {
};

return this.get(sourceInstance, options).then(associatedObjects =>
_.differenceBy(instancePrimaryKeys, associatedObjects, this.target.primaryKeyAttribute).length === 0
_.differenceBy(instancePrimaryKeys, associatedObjects, this.targetKey).length === 0
);
}

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

const sourceKey = this.source.primaryKeyAttribute;
const targetKey = this.target.primaryKeyAttribute;
const sourceKey = this.sourceKey;
const targetKey = this.targetKey;
const identifier = this.identifier;
const foreignIdentifier = this.foreignIdentifier;
let where = {};
Expand Down Expand Up @@ -626,8 +663,8 @@ class BelongsToMany extends Association {
options = _.clone(options) || {};

const association = this;
const sourceKey = association.source.primaryKeyAttribute;
const targetKey = association.target.primaryKeyAttribute;
const sourceKey = association.sourceKey;
const targetKey = association.targetKey;
const identifier = association.identifier;
const foreignIdentifier = association.foreignIdentifier;
const defaultAttributes = options.through || {};
Expand Down Expand Up @@ -721,8 +758,8 @@ class BelongsToMany extends Association {
oldAssociatedObjects = association.toInstanceArray(oldAssociatedObjects);

const where = {
[association.identifier]: sourceInstance.get(association.source.primaryKeyAttribute),
[association.foreignIdentifier]: oldAssociatedObjects.map(newInstance => newInstance.get(association.target.primaryKeyAttribute))
[association.identifier]: sourceInstance.get(association.sourceKey),
[association.foreignIdentifier]: oldAssociatedObjects.map(newInstance => newInstance.get(association.targetKey))
};

return association.through.model.destroy(_.defaults({ where }, options));
Expand Down
10 changes: 4 additions & 6 deletions lib/dialects/abstract/query-generator.js
Expand Up @@ -1666,13 +1666,11 @@ class QueryGenerator {
);
const association = include.association;
const parentIsTop = !include.parent.association && include.parent.model.name === topLevelInfo.options.model.name;
const primaryKeysSource = association.source.primaryKeyAttributes;
const tableSource = parentTableName;
const identSource = association.identifierField;
const primaryKeysTarget = association.target.primaryKeyAttributes;
const tableTarget = includeAs.internalAs;
const identTarget = association.foreignIdentifierField;
const attrTarget = association.target.rawAttributes[primaryKeysTarget[0]].field || primaryKeysTarget[0];
const attrTarget = association.targetKeyField;

const joinType = include.required ? 'INNER JOIN' : 'LEFT OUTER JOIN';
let joinBody;
Expand All @@ -1681,7 +1679,7 @@ class QueryGenerator {
main: [],
subQuery: []
};
let attrSource = primaryKeysSource[0];
let attrSource = association.sourceKey;
let sourceJoinOn;
let targetJoinOn;
let throughWhere;
Expand All @@ -1696,10 +1694,10 @@ class QueryGenerator {

// Figure out if we need to use field or attribute
if (!topLevelInfo.subQuery) {
attrSource = association.source.rawAttributes[primaryKeysSource[0]].field;
attrSource = association.sourceKeyField;
}
if (topLevelInfo.subQuery && !include.subQuery && !include.parent.subQuery && include.parent.model !== topLevelInfo.options.mainModel) {
attrSource = association.source.rawAttributes[primaryKeysSource[0]].field;
attrSource = association.sourceKeyField;
}

// Filter statement for left side of through
Expand Down

0 comments on commit 83e263b

Please sign in to comment.