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

Commit ffbaf77f by Mick Hansen

refactor(has-many): move association methods to the association

To prepare for being able to run a association method on multiple source instances i've moved the association methods to the association prototype
instead of having them on the instance prototype, the instance prototype now only has proxy methods to the methods on the association.
1 parent 7e5b7c5a
...@@ -221,247 +221,269 @@ HasMany.prototype.injectAttributes = function() { ...@@ -221,247 +221,269 @@ HasMany.prototype.injectAttributes = function() {
return this; return this;
}; };
HasMany.prototype.injectGetter = function(obj) { HasMany.prototype.mixin = function(obj) {
var association = this; var association = this;
obj[this.accessors.get] = function(options) { obj[this.accessors.get] = function(options) {
var scopeWhere = association.scope ? {} : null return association.get(this, options);
, Model = association.target;
options = association.target.__optClone(options) || {};
if (association.scope) {
_.assign(scopeWhere, association.scope);
}
options.where = {
$and: [
new Utils.where(
association.target.rawAttributes[association.foreignKey],
this.get(association.source.primaryKeyAttribute, {raw: true})
),
scopeWhere,
options.where
]
};
if (options.hasOwnProperty('scope')) {
if (!options.scope) {
Model = Model.unscoped();
} else {
Model = Model.scope(options.scope);
}
}
return Model.all(options);
}; };
if (this.accessors.count) { if (this.accessors.count) {
obj[this.accessors.count] = function(options) { obj[this.accessors.count] = function(options) {
var model = association.target return association.count(this, options);
, sequelize = model.sequelize;
options = association.target.__optClone(options) || {};
options.attributes = [
[sequelize.fn('COUNT', sequelize.col(model.primaryKeyAttribute)), 'count']
];
options.raw = true;
options.plain = true;
return obj[association.accessors.get].call(this, options).then(function (result) {
return parseInt(result.count, 10);
});
}; };
} }
obj[this.accessors.hasSingle] = obj[this.accessors.hasAll] = function(instances, options) { obj[this.accessors.hasSingle] = obj[this.accessors.hasAll] = function(instances, options) {
var where = {}; return association.has(this, instances, options);
};
if (!Array.isArray(instances)) {
instances = [instances];
}
options = options || {}; obj[this.accessors.set] = function(instances, options) {
options.scope = false; return association.set(this, instances, options);
};
where.$or = instances.map(function (instance) {
if (instance instanceof association.target.Instance) {
return instance.where();
} else {
var _where = {};
_where[association.target.primaryKeyAttribute] = instance;
return _where;
}
});
options.where = { obj[this.accessors.add] = obj[this.accessors.addMultiple] = function(instances, options) {
$and: [ return association.add(this, instances, options);
where, };
options.where
]
};
return this[association.accessors.get]( obj[this.accessors.remove] = obj[this.accessors.removeMultiple] = function(instances, options) {
options, return association.add(this, instances, options);
{ raw: true }
).then(function(associatedObjects) {
return associatedObjects.length === instances.length;
});
}; };
return this; obj[this.accessors.create] = function(values, options) {
return association.create(this, values, options);
};
}; };
HasMany.prototype.injectSetter = function(obj) { HasMany.prototype.get = function(instance, options) {
var association = this; var association = this
, scopeWhere = association.scope ? {} : null
, Model = association.target;
obj[this.accessors.set] = function(newAssociatedObjects, additionalAttributes) { options = association.target.__optClone(options) || {};
var options = additionalAttributes || {};
additionalAttributes = additionalAttributes || {}; if (association.scope) {
_.assign(scopeWhere, association.scope);
}
if (newAssociatedObjects === null) { options.where = {
newAssociatedObjects = []; $and: [
new Utils.where(
association.target.rawAttributes[association.foreignKey],
instance.get(association.source.primaryKeyAttribute, {raw: true})
),
scopeWhere,
options.where
]
};
if (options.hasOwnProperty('scope')) {
if (!options.scope) {
Model = Model.unscoped();
} else { } else {
newAssociatedObjects = association.toInstanceArray(newAssociatedObjects); Model = Model.scope(options.scope);
} }
}
var instance = this; return Model.all(options);
};
return instance[association.accessors.get](_.defaults({
scope: false,
raw: true
}, options)).then(function(oldAssociations) {
var promises = []
, obsoleteAssociations = oldAssociations.filter(function(old) {
return !_.find(newAssociatedObjects, function(obj) {
return obj[association.target.primaryKeyAttribute] === old[association.target.primaryKeyAttribute];
});
})
, unassociatedObjects = newAssociatedObjects.filter(function(obj) {
return !_.find(oldAssociations, function(old) {
return obj[association.target.primaryKeyAttribute] === old[association.target.primaryKeyAttribute];
});
})
, updateWhere
, update;
if (obsoleteAssociations.length > 0) {
update = {};
update[association.foreignKey] = null;
updateWhere = {};
updateWhere[association.target.primaryKeyAttribute] = obsoleteAssociations.map(function(associatedObject) {
return associatedObject[association.target.primaryKeyAttribute];
});
promises.push(association.target.unscoped().update(
update,
_.defaults({
where: updateWhere
}, options)
));
}
if (unassociatedObjects.length > 0) {
updateWhere = {};
update = {};
update[association.foreignKey] = instance.get(association.source.primaryKeyAttribute);
_.assign(update, association.scope);
updateWhere[association.target.primaryKeyAttribute] = unassociatedObjects.map(function(unassociatedObject) {
return unassociatedObject[association.target.primaryKeyAttribute];
});
promises.push(association.target.unscoped().update(
update,
_.defaults({
where: updateWhere
}, options)
));
}
return Utils.Promise.all(promises).return(instance);
});
};
obj[this.accessors.addMultiple] = obj[this.accessors.add] = function(newInstances, options) { HasMany.prototype.count = function(instance, options) {
// If newInstance is null or undefined, no-op var association = this
if (!newInstances) return Utils.Promise.resolve(); , model = association.target
options = options || {}; , sequelize = model.sequelize;
options = association.target.__optClone(options) || {};
options.attributes = [
[sequelize.fn('COUNT', sequelize.col(model.primaryKeyAttribute)), 'count']
];
options.raw = true;
options.plain = true;
return this.get(instance, options).then(function (result) {
return parseInt(result.count, 10);
});
};
var instance = this, update = {}, where = {}; HasMany.prototype.has = function(sourceInstance, targetInstances, options) {
var association = this
, where = {};
newInstances = association.toInstanceArray(newInstances); if (!Array.isArray(targetInstances)) {
targetInstances = [targetInstances];
}
update[association.foreignKey] = instance.get(association.source.primaryKeyAttribute); options = options || {};
_.assign(update, association.scope); options.scope = false;
options.raw = true;
where[association.target.primaryKeyAttribute] = newInstances.map(function (unassociatedObject) { where.$or = targetInstances.map(function (instance) {
return unassociatedObject.get(association.target.primaryKeyAttribute); if (instance instanceof association.target.Instance) {
}); return instance.where();
} else {
var _where = {};
_where[association.target.primaryKeyAttribute] = instance;
return _where;
}
});
return association.target.unscoped().update( options.where = {
update, $and: [
_.defaults({ where,
where: where options.where
}, options) ]
).return(instance);
};
obj[this.accessors.removeMultiple] = obj[this.accessors.remove] = function(oldAssociatedObjects, options) {
options = options || {};
oldAssociatedObjects = association.toInstanceArray(oldAssociatedObjects);
var update = {};
update[association.foreignKey] = null;
var where = {};
where[association.foreignKey] = this.get(association.source.primaryKeyAttribute);
where[association.target.primaryKeyAttribute] = oldAssociatedObjects.map(function (oldAssociatedObject) { return oldAssociatedObject.get(association.target.primaryKeyAttribute); });
return association.target.unscoped().update(
update,
_.defaults({
where: where
}, options)
).return(this);
}; };
return this; return this.get(
sourceInstance,
options
).then(function(associatedObjects) {
return associatedObjects.length === targetInstances.length;
});
}; };
HasMany.prototype.injectCreator = function(obj) { HasMany.prototype.set = function(sourceInstance, targetInstances, options) {
var association = this; var association = this;
obj[this.accessors.create] = function(values, options) { if (targetInstances === null) {
var instance = this; targetInstances = [];
options = options || {}; } else {
targetInstances = association.toInstanceArray(targetInstances);
}
if (Array.isArray(options)) { return association.get(sourceInstance, _.defaults({
options = { scope: false,
fields: options raw: true
}; }, options)).then(function(oldAssociations) {
} var promises = []
, obsoleteAssociations = oldAssociations.filter(function(old) {
return !_.find(targetInstances, function(obj) {
return obj[association.target.primaryKeyAttribute] === old[association.target.primaryKeyAttribute];
});
})
, unassociatedObjects = targetInstances.filter(function(obj) {
return !_.find(oldAssociations, function(old) {
return obj[association.target.primaryKeyAttribute] === old[association.target.primaryKeyAttribute];
});
})
, updateWhere
, update;
if (obsoleteAssociations.length > 0) {
update = {};
update[association.foreignKey] = null;
updateWhere = {};
updateWhere[association.target.primaryKeyAttribute] = obsoleteAssociations.map(function(associatedObject) {
return associatedObject[association.target.primaryKeyAttribute];
});
if (values === undefined) { promises.push(association.target.unscoped().update(
values = {}; update,
_.defaults({
where: updateWhere
}, options)
));
} }
if (association.scope) { if (unassociatedObjects.length > 0) {
Object.keys(association.scope).forEach(function (attribute) { updateWhere = {};
values[attribute] = association.scope[attribute];
if (options.fields) options.fields.push(attribute); update = {};
update[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute);
_.assign(update, association.scope);
updateWhere[association.target.primaryKeyAttribute] = unassociatedObjects.map(function(unassociatedObject) {
return unassociatedObject[association.target.primaryKeyAttribute];
}); });
promises.push(association.target.unscoped().update(
update,
_.defaults({
where: updateWhere
}, options)
));
} }
values[association.foreignKey] = instance.get(association.source.primaryKeyAttribute); return Utils.Promise.all(promises).return(sourceInstance);
if (options.fields) options.fields.push(association.foreignKey); });
return association.target.create(values, options); };
};
return this; HasMany.prototype.add = function(sourceInstance, targetInstances, options) {
if (!targetInstances) return Utils.Promise.resolve();
var association = this
, update = {}
, where = {};
options = options || {};
targetInstances = association.toInstanceArray(targetInstances);
update[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute);
_.assign(update, association.scope);
where[association.target.primaryKeyAttribute] = targetInstances.map(function (unassociatedObject) {
return unassociatedObject.get(association.target.primaryKeyAttribute);
});
return association.target.unscoped().update(
update,
_.defaults({
where: where
}, options)
).return(sourceInstance);
};
HasMany.prototype.remove = function(sourceInstance, targetInstances, options) {
var association = this
, update = {}
, where = {};
options = options || {};
targetInstances = association.toInstanceArray(targetInstances);
update[association.foreignKey] = null;
where[association.foreignKey] = this.get(association.source.primaryKeyAttribute);
where[association.target.primaryKeyAttribute] = targetInstances.map(function (targetInstance) {
return targetInstance.get(association.target.primaryKeyAttribute);
});
return association.target.unscoped().update(
update,
_.defaults({
where: where
}, options)
).return(this);
};
HasMany.prototype.create = function(sourceInstance, values, options) {
var association = this;
options = options || {};
if (Array.isArray(options)) {
options = {
fields: options
};
}
if (values === undefined) {
values = {};
}
if (association.scope) {
Object.keys(association.scope).forEach(function (attribute) {
values[attribute] = association.scope[attribute];
if (options.fields) options.fields.push(attribute);
});
}
values[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute);
if (options.fields) options.fields.push(association.foreignKey);
return association.target.create(values, options);
}; };
module.exports = HasMany; module.exports = HasMany;
...@@ -205,27 +205,26 @@ Mixin.belongsTo = singleLinked(BelongsTo); ...@@ -205,27 +205,26 @@ Mixin.belongsTo = singleLinked(BelongsTo);
* @param {string} [options.onUpdate='CASCADE'] * @param {string} [options.onUpdate='CASCADE']
* @param {boolean} [options.constraints=true] Should on update and on delete constraints be enabled on the foreign key. * @param {boolean} [options.constraints=true] Should on update and on delete constraints be enabled on the foreign key.
*/ */
Mixin.hasMany = function(targetModel, options) { // testhint options:none Mixin.hasMany = function(target, options) { // testhint options:none
if (!(targetModel instanceof this.sequelize.Model)) { if (!(target instanceof this.sequelize.Model)) {
throw new Error(this.name + '.hasMany called with something that\'s not an instance of Sequelize.Model'); throw new Error(this.name + '.hasMany called with something that\'s not an instance of Sequelize.Model');
} }
var sourceModel = this; var source = this;
// Since this is a mixin, we'll need a unique variable name for hooks (since Model will override our hooks option) // Since this is a mixin, we'll need a unique variable name for hooks (since Model will override our hooks option)
options = options || {}; options = options || {};
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks); options.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = options.hooks; options.useHooks = options.hooks;
options = _.extend(options, _.omit(sourceModel.options, ['hooks'])); options = _.extend(options, _.omit(source.options, ['hooks']));
// the id is in the foreign table or in a connecting table // the id is in the foreign table or in a connecting table
var association = new HasMany(sourceModel, targetModel, options); var association = new HasMany(source, target, options);
sourceModel.associations[association.associationAccessor] = association.injectAttributes(); source.associations[association.associationAccessor] = association;
association.injectGetter(sourceModel.Instance.prototype); association.injectAttributes();
association.injectSetter(sourceModel.Instance.prototype); association.mixin(source.Instance.prototype);
association.injectCreator(sourceModel.Instance.prototype);
return association; return association;
}; };
......
...@@ -7,11 +7,12 @@ var chai = require('chai') ...@@ -7,11 +7,12 @@ var chai = require('chai')
, stub = sinon.stub , stub = sinon.stub
, Support = require(__dirname + '/../support') , Support = require(__dirname + '/../support')
, DataTypes = require(__dirname + '/../../../lib/data-types') , DataTypes = require(__dirname + '/../../../lib/data-types')
, HasMany = require(__dirname + '/../../../lib/associations/has-many')
, current = Support.sequelize , current = Support.sequelize
, Promise = current.Promise; , Promise = current.Promise;
describe(Support.getTestDialectTeaser('hasMany'), function() { describe(Support.getTestDialectTeaser('hasMany'), function() {
describe('optimizations using bulk create, destroy and update', function() { describe('optimizations using bulk create, destroy and update', function() {
var User = current.define('User', { username: DataTypes.STRING }) var User = current.define('User', { username: DataTypes.STRING })
, Task = current.define('Task', { title: DataTypes.STRING }); , Task = current.define('Task', { title: DataTypes.STRING });
...@@ -61,4 +62,27 @@ describe(Support.getTestDialectTeaser('hasMany'), function() { ...@@ -61,4 +62,27 @@ describe(Support.getTestDialectTeaser('hasMany'), function() {
}); });
}); });
}); });
describe('mixin', function () {
var User = current.define('User')
, Task = current.define('Task');
it('should mixin association methods', function () {
var as = Math.random().toString()
, association = new HasMany(User, Task, {as: as})
, obj = {};
association.mixin(obj);
expect(obj[association.accessors.get]).to.be.an('function');
expect(obj[association.accessors.set]).to.be.an('function');
expect(obj[association.accessors.addMultiple]).to.be.an('function');
expect(obj[association.accessors.add]).to.be.an('function');
expect(obj[association.accessors.remove]).to.be.an('function');
expect(obj[association.accessors.removeMultiple]).to.be.an('function');
expect(obj[association.accessors.hasSingle]).to.be.an('function');
expect(obj[association.accessors.hasAll]).to.be.an('function');
expect(obj[association.accessors.count]).to.be.an('function');
});
});
}); });
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!