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

Commit 83e263bd by Alexander Mochalin Committed by Sushant

feat(associations): source and target key support for belongs-to-many (#11311)

1 parent 4f098998
...@@ -531,6 +531,70 @@ Person.belongsToMany(Person, { as: 'Children', through: 'PersonChildren' }) ...@@ -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: 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 ```js
......
...@@ -111,43 +111,6 @@ class BelongsToMany extends Association { ...@@ -111,43 +111,6 @@ class BelongsToMany extends Association {
} }
/* /*
* 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) * Find paired association (if exists)
*/ */
_.each(this.target.associations, association => { _.each(this.target.associations, association => {
...@@ -160,6 +123,23 @@ class BelongsToMany extends Association { ...@@ -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 (typeof this.through.model === 'string') {
if (!this.sequelize.isDefined(this.through.model)) { if (!this.sequelize.isDefined(this.through.model)) {
this.through.model = this.sequelize.define(this.through.model, {}, Object.assign(this.options, { this.through.model = this.sequelize.define(this.through.model, {}, Object.assign(this.options, {
...@@ -178,6 +158,25 @@ class BelongsToMany extends Association { ...@@ -178,6 +158,25 @@ class BelongsToMany extends Association {
])); ]));
if (this.paired) { 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) { if (this.otherKeyDefault) {
this.otherKey = this.paired.foreignKey; this.otherKey = this.paired.foreignKey;
} }
...@@ -187,9 +186,13 @@ class BelongsToMany extends Association { ...@@ -187,9 +186,13 @@ class BelongsToMany extends Association {
if (this.paired.otherKey !== this.foreignKey) { if (this.paired.otherKey !== this.foreignKey) {
delete this.through.model.rawAttributes[this.paired.otherKey]; delete this.through.model.rawAttributes[this.paired.otherKey];
this.paired.otherKey = this.foreignKey; this.paired.otherKey = this.foreignKey;
this.paired._injectAttributes(); needInjectPaired = true;
} }
} }
if (needInjectPaired) {
this.paired._injectAttributes();
}
} }
if (this.through) { if (this.through) {
...@@ -218,6 +221,41 @@ class BelongsToMany extends Association { ...@@ -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 // the id is in the target table
// or in an extra table which connects two tables // or in an extra table which connects two tables
_injectAttributes() { _injectAttributes() {
...@@ -240,12 +278,12 @@ class BelongsToMany extends Association { ...@@ -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 sourceKeyType = sourceKey.type;
const sourceKeyField = sourceKey.field || this.source.primaryKeyAttribute; const sourceKeyField = this.sourceKeyField;
const targetKey = this.target.rawAttributes[this.target.primaryKeyAttribute]; const targetKey = this.target.rawAttributes[this.targetKey];
const targetKeyType = targetKey.type; const targetKeyType = targetKey.type;
const targetKeyField = targetKey.field || this.target.primaryKeyAttribute; const targetKeyField = this.targetKeyField;
const sourceAttribute = _.defaults({}, this.foreignKeyAttribute, { type: sourceKeyType }); const sourceAttribute = _.defaults({}, this.foreignKeyAttribute, { type: sourceKeyType });
const targetAttribute = _.defaults({}, this.otherKeyAttribute, { type: targetKeyType }); const targetAttribute = _.defaults({}, this.otherKeyAttribute, { type: targetKeyType });
...@@ -393,7 +431,7 @@ class BelongsToMany extends Association { ...@@ -393,7 +431,7 @@ class BelongsToMany extends Association {
if (Object(through.model) === through.model) { if (Object(through.model) === through.model) {
throughWhere = {}; throughWhere = {};
throughWhere[this.foreignKey] = instance.get(this.source.primaryKeyAttribute); throughWhere[this.foreignKey] = instance.get(this.sourceKey);
if (through.scope) { if (through.scope) {
Object.assign(throughWhere, through.scope); Object.assign(throughWhere, through.scope);
...@@ -442,12 +480,11 @@ class BelongsToMany extends Association { ...@@ -442,12 +480,11 @@ class BelongsToMany extends Association {
* @returns {Promise<number>} * @returns {Promise<number>}
*/ */
count(instance, options) { count(instance, options) {
const model = this.target; const sequelize = this.target.sequelize;
const sequelize = model.sequelize;
options = Utils.cloneDeep(options); options = Utils.cloneDeep(options);
options.attributes = [ 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.joinTableAttributes = [];
options.raw = true; options.raw = true;
...@@ -474,7 +511,7 @@ class BelongsToMany extends Association { ...@@ -474,7 +511,7 @@ class BelongsToMany extends Association {
raw: true raw: true
}, options, { }, options, {
scope: false, scope: false,
attributes: [this.target.primaryKeyAttribute], attributes: [this.targetKey],
joinTableAttributes: [] joinTableAttributes: []
}); });
...@@ -483,7 +520,7 @@ class BelongsToMany extends Association { ...@@ -483,7 +520,7 @@ class BelongsToMany extends Association {
return instance.where(); return instance.where();
} }
return { return {
[this.target.primaryKeyAttribute]: instance [this.targetKey]: instance
}; };
}); });
...@@ -495,7 +532,7 @@ class BelongsToMany extends Association { ...@@ -495,7 +532,7 @@ class BelongsToMany extends Association {
}; };
return this.get(sourceInstance, options).then(associatedObjects => return this.get(sourceInstance, options).then(associatedObjects =>
_.differenceBy(instancePrimaryKeys, associatedObjects, this.target.primaryKeyAttribute).length === 0 _.differenceBy(instancePrimaryKeys, associatedObjects, this.targetKey).length === 0
); );
} }
...@@ -514,8 +551,8 @@ class BelongsToMany extends Association { ...@@ -514,8 +551,8 @@ class BelongsToMany extends Association {
set(sourceInstance, newAssociatedObjects, options) { set(sourceInstance, newAssociatedObjects, options) {
options = options || {}; options = options || {};
const sourceKey = this.source.primaryKeyAttribute; const sourceKey = this.sourceKey;
const targetKey = this.target.primaryKeyAttribute; const targetKey = this.targetKey;
const identifier = this.identifier; const identifier = this.identifier;
const foreignIdentifier = this.foreignIdentifier; const foreignIdentifier = this.foreignIdentifier;
let where = {}; let where = {};
...@@ -626,8 +663,8 @@ class BelongsToMany extends Association { ...@@ -626,8 +663,8 @@ class BelongsToMany extends Association {
options = _.clone(options) || {}; options = _.clone(options) || {};
const association = this; const association = this;
const sourceKey = association.source.primaryKeyAttribute; const sourceKey = association.sourceKey;
const targetKey = association.target.primaryKeyAttribute; const targetKey = association.targetKey;
const identifier = association.identifier; const identifier = association.identifier;
const foreignIdentifier = association.foreignIdentifier; const foreignIdentifier = association.foreignIdentifier;
const defaultAttributes = options.through || {}; const defaultAttributes = options.through || {};
...@@ -721,8 +758,8 @@ class BelongsToMany extends Association { ...@@ -721,8 +758,8 @@ class BelongsToMany extends Association {
oldAssociatedObjects = association.toInstanceArray(oldAssociatedObjects); oldAssociatedObjects = association.toInstanceArray(oldAssociatedObjects);
const where = { const where = {
[association.identifier]: sourceInstance.get(association.source.primaryKeyAttribute), [association.identifier]: sourceInstance.get(association.sourceKey),
[association.foreignIdentifier]: oldAssociatedObjects.map(newInstance => newInstance.get(association.target.primaryKeyAttribute)) [association.foreignIdentifier]: oldAssociatedObjects.map(newInstance => newInstance.get(association.targetKey))
}; };
return association.through.model.destroy(_.defaults({ where }, options)); return association.through.model.destroy(_.defaults({ where }, options));
......
...@@ -1666,13 +1666,11 @@ class QueryGenerator { ...@@ -1666,13 +1666,11 @@ class QueryGenerator {
); );
const association = include.association; const association = include.association;
const parentIsTop = !include.parent.association && include.parent.model.name === topLevelInfo.options.model.name; const parentIsTop = !include.parent.association && include.parent.model.name === topLevelInfo.options.model.name;
const primaryKeysSource = association.source.primaryKeyAttributes;
const tableSource = parentTableName; const tableSource = parentTableName;
const identSource = association.identifierField; const identSource = association.identifierField;
const primaryKeysTarget = association.target.primaryKeyAttributes;
const tableTarget = includeAs.internalAs; const tableTarget = includeAs.internalAs;
const identTarget = association.foreignIdentifierField; 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'; const joinType = include.required ? 'INNER JOIN' : 'LEFT OUTER JOIN';
let joinBody; let joinBody;
...@@ -1681,7 +1679,7 @@ class QueryGenerator { ...@@ -1681,7 +1679,7 @@ class QueryGenerator {
main: [], main: [],
subQuery: [] subQuery: []
}; };
let attrSource = primaryKeysSource[0]; let attrSource = association.sourceKey;
let sourceJoinOn; let sourceJoinOn;
let targetJoinOn; let targetJoinOn;
let throughWhere; let throughWhere;
...@@ -1696,10 +1694,10 @@ class QueryGenerator { ...@@ -1696,10 +1694,10 @@ class QueryGenerator {
// Figure out if we need to use field or attribute // Figure out if we need to use field or attribute
if (!topLevelInfo.subQuery) { 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) { 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 // Filter statement for left side of through
......
...@@ -164,13 +164,13 @@ if (dialect.match(/^postgres/)) { ...@@ -164,13 +164,13 @@ if (dialect.match(/^postgres/)) {
const second_body = 'return \'second\';'; const second_body = 'return \'second\';';
// create function // create function
return this.queryInterface.createFunction('my_func', [], 'varchar', 'plpgsql', first_body, null) return this.queryInterface.createFunction('create_job', [{ type: 'varchar', name: 'test' }], 'varchar', 'plpgsql', first_body, null)
// override // override
.then(() => this.queryInterface.createFunction('my_func', [], 'varchar', 'plpgsql', second_body, null, { force: true })) .then(() => this.queryInterface.createFunction('create_job', [{ type: 'varchar', name: 'test' }], 'varchar', 'plpgsql', second_body, null, { force: true }))
// validate // validate
.then(() => this.sequelize.query('select my_func();', { type: this.sequelize.QueryTypes.SELECT })) .then(() => this.sequelize.query("select create_job('abc');", { type: this.sequelize.QueryTypes.SELECT }))
.then(res => { .then(res => {
expect(res[0].my_func).to.be.eql('second'); expect(res[0].create_job).to.be.eql('second');
}); });
}); });
......
...@@ -224,7 +224,7 @@ describe(Support.getTestDialectTeaser('belongsToMany'), () => { ...@@ -224,7 +224,7 @@ describe(Support.getTestDialectTeaser('belongsToMany'), () => {
it('should infer otherKey from paired BTM relationship with a through model defined', function() { it('should infer otherKey from paired BTM relationship with a through model defined', function() {
const User = this.sequelize.define('User', {}); const User = this.sequelize.define('User', {});
const Place = this.sequelize.define('User', {}); const Place = this.sequelize.define('Place', {});
const UserPlace = this.sequelize.define('UserPlace', { const UserPlace = this.sequelize.define('UserPlace', {
id: { id: {
primaryKey: true, primaryKey: true,
...@@ -249,6 +249,51 @@ describe(Support.getTestDialectTeaser('belongsToMany'), () => { ...@@ -249,6 +249,51 @@ describe(Support.getTestDialectTeaser('belongsToMany'), () => {
}); });
}); });
describe('source/target keys', () => {
it('should infer targetKey from paired BTM relationship with a through string defined', function() {
const User = this.sequelize.define('User', { user_id: DataTypes.UUID });
const Place = this.sequelize.define('Place', { place_id: DataTypes.UUID });
const Places = User.belongsToMany(Place, { through: 'user_places', sourceKey: 'user_id' });
const Users = Place.belongsToMany(User, { through: 'user_places', sourceKey: 'place_id' });
expect(Places.paired).to.equal(Users);
expect(Users.paired).to.equal(Places);
expect(Places.sourceKey).to.equal('user_id');
expect(Users.sourceKey).to.equal('place_id');
expect(Places.targetKey).to.equal('place_id');
expect(Users.targetKey).to.equal('user_id');
});
it('should infer targetKey from paired BTM relationship with a through model defined', function() {
const User = this.sequelize.define('User', { user_id: DataTypes.UUID });
const Place = this.sequelize.define('Place', { place_id: DataTypes.UUID });
const UserPlace = this.sequelize.define('UserPlace', {
id: {
primaryKey: true,
type: DataTypes.INTEGER,
autoIncrement: true
}
}, { timestamps: false });
const Places = User.belongsToMany(Place, { through: UserPlace, sourceKey: 'user_id' });
const Users = Place.belongsToMany(User, { through: UserPlace, sourceKey: 'place_id' });
expect(Places.paired).to.equal(Users);
expect(Users.paired).to.equal(Places);
expect(Places.sourceKey).to.equal('user_id');
expect(Users.sourceKey).to.equal('place_id');
expect(Places.targetKey).to.equal('place_id');
expect(Users.targetKey).to.equal('user_id');
expect(Object.keys(UserPlace.rawAttributes).length).to.equal(3); // Defined primary key and two foreign keys
});
});
describe('pseudo associations', () => { describe('pseudo associations', () => {
it('should setup belongsTo relations to source and target from join model with defined foreign/other keys', function() { it('should setup belongsTo relations to source and target from join model with defined foreign/other keys', function() {
const Product = this.sequelize.define('Product', { const Product = this.sequelize.define('Product', {
......
...@@ -64,6 +64,18 @@ export interface BelongsToManyOptions extends ManyToManyOptions { ...@@ -64,6 +64,18 @@ export interface BelongsToManyOptions extends ManyToManyOptions {
otherKey?: string | ForeignKeyOptions; otherKey?: string | ForeignKeyOptions;
/** /**
* 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
*/
sourceKey?: string;
/**
* The name of the field to use as the key for the association in the target table. Defaults to the primary
* key of the target table
*/
targetKey?: string;
/**
* Should the join model have timestamps * Should the join model have timestamps
*/ */
timestamps?: boolean; timestamps?: boolean;
...@@ -76,6 +88,8 @@ export interface BelongsToManyOptions extends ManyToManyOptions { ...@@ -76,6 +88,8 @@ export interface BelongsToManyOptions extends ManyToManyOptions {
export class BelongsToMany<S extends Model = Model, T extends Model = Model> extends Association<S, T> { export class BelongsToMany<S extends Model = Model, T extends Model = Model> extends Association<S, T> {
public otherKey: string; public otherKey: string;
public sourceKey: string;
public targetKey: string;
public accessors: MultiAssociationAccessors; public accessors: MultiAssociationAccessors;
constructor(source: ModelCtor<S>, target: ModelCtor<T>, options: BelongsToManyOptions); constructor(source: ModelCtor<S>, target: ModelCtor<T>, options: BelongsToManyOptions);
} }
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!