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

Commit f365f72f by Sushant Committed by GitHub

feat(hasOne): sourceKey support with key validation (#9382)

1 parent 0393eb36
......@@ -296,7 +296,6 @@ const Company = this.sequelize.define('company', {/* attributes */});
User.belongsTo(Company, {foreignKey: 'fk_companyname', targetKey: 'name'}); // Adds fk_companyname to User
```
### HasOne
HasOne associations are associations where the foreign key for the one-to-one relation exists on the **target model**.
......@@ -353,6 +352,19 @@ Game.belongsTo(Team);
Even though it is called a HasOne association, for most 1:1 relations you usually want the BelongsTo association since BelongsTo will add the foreignKey on the source where hasOne will add on the target.
#### Source keys
The source key is the attribute on the source model that the foreign key attribute on the target model points to. By default the source key for a `hasOne` relation will be the source model's primary attribute. To use a custom attribute, use the `sourceKey` option.
```js
const User = this.sequelize.define('user', {/* attributes */})
const Company = this.sequelize.define('company', {/* attributes */});
// Adds companyName attribute to User
// Use name attribute from Company as source attribute
Company.hasOne(User, {foreignKey: 'companyName', sourceKey: 'name'});
```
### Difference between HasOne and BelongsTo
In Sequelize 1:1 relationship can be set using HasOne and BelongsTo. They are suitable for different scenarios. Lets study this difference using an example.
......
......@@ -54,11 +54,18 @@ class BelongsTo extends Association {
this.identifierField = this.source.rawAttributes[this.identifier].field || this.identifier;
}
if (
this.options.targetKey
&& !this.target.rawAttributes[this.options.targetKey]
) {
throw new Error(`Unknown attribute "${this.options.targetKey}" passed as targetKey, define this attribute on model "${this.target.name}" first`);
}
this.targetKey = this.options.targetKey || this.target.primaryKeyAttribute;
this.targetKeyField = this.target.rawAttributes[this.targetKey].field || this.targetKey;
this.targetKeyIsPrimary = this.targetKey === this.target.primaryKeyAttribute;
this.targetIdentifier = this.targetKey;
this.associationAccessor = this.as;
this.options.useHooks = options.useHooks;
......
......@@ -48,9 +48,17 @@ class HasOne extends Association {
);
}
this.sourceIdentifier = this.source.primaryKeyAttribute;
this.sourceKey = this.source.primaryKeyAttribute;
if (
this.options.sourceKey
&& !this.source.rawAttributes[this.options.sourceKey]
) {
throw new Error(`Unknown attribute "${this.options.sourceKey}" passed as sourceKey, define this attribute on model "${this.source.name}" first`);
}
this.sourceKey = this.options.sourceKey || this.source.primaryKeyAttribute;
this.sourceKeyField = this.source.rawAttributes[this.sourceKey].field || this.sourceKey;
this.sourceKeyIsPrimary = this.sourceKey === this.source.primaryKeyAttribute;
this.sourceIdentifier = this.sourceKey;
this.associationAccessor = this.as;
this.options.useHooks = options.useHooks;
......@@ -72,10 +80,9 @@ class HasOne extends Association {
// the id is in the target table
_injectAttributes() {
const newAttributes = {};
const keyType = this.source.rawAttributes[this.source.primaryKeyAttribute].type;
newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, {
type: this.options.keyType || keyType,
type: this.options.keyType || this.source.rawAttributes[this.sourceKey].type,
allowNull: true
});
......@@ -85,7 +92,7 @@ class HasOne extends Association {
this.options.onUpdate = this.options.onUpdate || 'CASCADE';
}
Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.source, this.target, this.options);
Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.source, this.target, this.options, this.sourceKeyField);
Utils.mergeDefaults(this.target.rawAttributes, newAttributes);
this.target.refreshAttributes();
......
......@@ -4173,7 +4173,8 @@ class Model {
* @param {object} [options]
* @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} [options.as] The alias of this model, in singular form. 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 singularized 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 attribute in the target model 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 attribute to use as the key for the association in the source table. Defaults to the primary key of the source table
* @param {string} [options.onDelete='SET NULL|CASCADE'] SET NULL if foreignKey allows nulls, CASCADE if otherwise
* @param {string} [options.onUpdate='CASCADE']
* @param {boolean} [options.constraints=true] Should on update and on delete constraints be enabled on the foreign key.
......@@ -4191,8 +4192,8 @@ class Model {
* @param {object} [options]
* @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} [options.as] The alias of this model, in singular form. 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 singularized name of target
* @param {string|object} [options.foreignKey] The name of the foreign key in the source 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 target + primary key of target
* @param {string} [options.targetKey] 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
* @param {string|object} [options.foreignKey] The name of the foreign key attribute in the source 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 target + primary key of target
* @param {string} [options.targetKey] The name of the attribute to use as the key for the association in the target table. Defaults to the primary key of the target table
* @param {string} [options.onDelete='SET NULL|NO ACTION'] SET NULL if foreignKey allows nulls, NO ACTION if otherwise
* @param {string} [options.onUpdate='CASCADE']
* @param {boolean} [options.constraints=true] Should on update and on delete constraints be enabled on the foreign key.
......
......@@ -643,7 +643,7 @@ describe(Support.getTestDialectTeaser('HasOne'), () => {
});
describe('Association column', () => {
describe('association column', () => {
it('has correct type for non-id primary keys with non-integer type', function() {
const User = this.sequelize.define('UserPKBT', {
username: {
......@@ -664,6 +664,97 @@ describe(Support.getTestDialectTeaser('HasOne'), () => {
expect(User.rawAttributes.GroupPKBTName.type).to.an.instanceof(Sequelize.STRING);
});
});
it('should support a non-primary key as the association column on a target with custom primary key', function() {
const User = this.sequelize.define('User', {
user_name: {
type: Sequelize.STRING,
primaryKey: true
}
});
const Task = this.sequelize.define('Task', {
title: Sequelize.STRING,
username: Sequelize.STRING
});
User.hasOne(Task, { foreignKey: 'username', sourceKey: 'user_name' });
return this.sequelize.sync({ force: true }).then(() => {
return User.create({ user_name: 'bob' }).then(newUser => {
return Task.create({ title: 'some task' }).then(newTask => {
return newUser.setTask(newTask).then(() => {
return User.findOne({ where: { user_name: 'bob' } }).then(foundUser => {
return foundUser.getTask().then(foundTask => {
expect(foundTask.title).to.equal('some task');
});
});
});
});
});
});
});
it('should support a non-primary unique key as the association column', function() {
const User = this.sequelize.define('User', {
username: {
type: Sequelize.STRING,
unique: true
}
});
const Task = this.sequelize.define('Task', {
title: Sequelize.STRING,
username: Sequelize.STRING
});
User.hasOne(Task, { foreignKey: 'username', sourceKey: 'username' });
return this.sequelize.sync({ force: true }).then(() => {
return User.create({ username: 'bob' }).then(newUser => {
return Task.create({ title: 'some task' }).then(newTask => {
return newUser.setTask(newTask).then(() => {
return User.findOne({ where: { username: 'bob' } }).then(foundUser => {
return foundUser.getTask().then(foundTask => {
expect(foundTask.title).to.equal('some task');
});
});
});
});
});
});
});
it('should support a non-primary unique key as the association column with a field option', function() {
const User = this.sequelize.define('User', {
username: {
type: Sequelize.STRING,
unique: true,
field: 'the_user_name_field'
}
});
const Task = this.sequelize.define('Task', {
title: Sequelize.STRING,
username: Sequelize.STRING
});
User.hasOne(Task, { foreignKey: 'username', sourceKey: 'username' });
return this.sequelize.sync({ force: true }).then(() => {
return User.create({ username: 'bob' }).then(newUser => {
return Task.create({ title: 'some task' }).then(newTask => {
return newUser.setTask(newTask).then(() => {
return User.findOne({ where: { username: 'bob' } }).then(foundUser => {
return foundUser.getTask().then(foundTask => {
expect(foundTask.title).to.equal('some task');
});
});
});
});
});
});
});
});
describe('Association options', () => {
......
......@@ -15,6 +15,15 @@ describe(Support.getTestDialectTeaser('belongsTo'), () => {
}).to.throw('User.belongsTo called with something that\'s not a subclass of Sequelize.Model');
});
it('warn on invalid options', () => {
const User = current.define('User', {});
const Task = current.define('Task', {});
expect(() => {
User.belongsTo(Task, { targetKey: 'wowow' });
}).to.throw('Unknown attribute "wowow" passed as targetKey, define this attribute on model "Task" first');
});
it('should not override custom methods with association mixin', () => {
const methods = {
getTask: 'get',
......
......@@ -16,6 +16,15 @@ describe(Support.getTestDialectTeaser('hasOne'), () => {
}).to.throw('User.hasOne called with something that\'s not a subclass of Sequelize.Model');
});
it('warn on invalid options', () => {
const User = current.define('User', {});
const Task = current.define('Task', {});
expect(() => {
User.hasOne(Task, { sourceKey: 'wowow' });
}).to.throw('Unknown attribute "wowow" passed as sourceKey, define this attribute on model "User" first');
});
it('properly use the `as` key to generate foreign key name', () => {
const User = current.define('User', { username: DataTypes.STRING }),
Task = current.define('Task', { title: DataTypes.STRING });
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!