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

Commit 3ced2aaa by Mick Hansen

Merge pull request #1747 from sequelize/setWithId

Add ability to set and add associations by model id instead of model instance
2 parents d14c24b0 85ab07cb
......@@ -5,6 +5,7 @@ Notice: All 1.7.x changes are present in 2.0.x aswell
- [FEATURE] There is now basic support for assigning a field name to an attribute `name: {type: DataTypes.STRING, field: 'full_name'}`
- [FEATURE] It's now possible to add multiple relations to a hasMany association, modelInstance.addRelations([otherInstanceA, otherInstanceB])
- [FEATURE] `define()` stores models in `sequelize.models` Object e.g. `sequelize.models.MyModel`
- [FEATURE] The `set` / `add` / `has` methods for associations now allow you to pass the value of a primary key, instead of a full Instance object, like so: `user.addTask(15);`.
- [BUG] An error is now thrown if an association would create a naming conflict between the association and the foreign key when doing eager loading. Closes [#1272](https://github.com/sequelize/sequelize/issues/1272)
- [INTERNALS] `bulkDeleteQuery` was removed from the MySQL / abstract query generator, since it was never used internally. Please use `deleteQuery` instead.
......
......@@ -88,7 +88,12 @@ module.exports = (function() {
var association = this
instancePrototype[this.accessors.set] = function(associatedInstance, options) {
this.set(association.identifier, associatedInstance ? associatedInstance[association.targetIdentifier] : null)
var value = associatedInstance
if (associatedInstance instanceof association.target.Instance) {
value = associatedInstance[association.targetIdentifier]
}
this.set(association.identifier, value)
options = Utils._.extend({
fields: [ association.identifier ],
......
......@@ -10,14 +10,11 @@ module.exports = (function() {
this.QueryInterface = instance.QueryInterface
}
HasManyDoubleLinked.prototype.injectGetter = function(options) {
HasManyDoubleLinked.prototype.injectGetter = function(options, queryOptions) {
var self = this
, through = self.association.through
, queryOptions = {}
, targetAssociation = self.association.targetAssociation
options = options || {}
//fully qualify
var instancePrimaryKey = self.instance.Model.primaryKeyAttribute
, foreignPrimaryKey = self.association.target.primaryKeyAttribute
......
......@@ -10,9 +10,7 @@ module.exports = (function() {
this.source = this.association.source
}
HasManySingleLinked.prototype.injectGetter = function(options) {
options = options || {}
HasManySingleLinked.prototype.injectGetter = function(options, queryOptions) {
options.where = new Utils.and([
new Utils.where(
this.target.rawAttributes[this.association.identifier],
......@@ -21,7 +19,7 @@ module.exports = (function() {
options.where
])
return this.association.target.all(options)
return this.association.target.all(options, queryOptions)
}
HasManySingleLinked.prototype.injectSetter = function(oldAssociations, newAssociations, defaultAttributes) {
......
......@@ -177,8 +177,8 @@ module.exports = (function() {
}
// remove any PKs previously defined by sequelize
Utils._.each(this.through.attributes, function(dataTypeString, attributeName) {
if (dataTypeString.toString().indexOf('PRIMARY KEY') !== -1 && self.through.rawAttributes[attributeName]._autoGenerated === true) {
Utils._.each(this.through.rawAttributes, function(attribute, attributeName) {
if (attribute.primaryKey === true && attribute._autoGenerated === true) {
delete self.through.rawAttributes[attributeName]
primaryKeyDeleted = true
}
......@@ -256,88 +256,153 @@ module.exports = (function() {
}
HasMany.prototype.injectGetter = function(obj) {
var self = this
var association = this
obj[this.accessors.get] = function(options) {
var Class = Object(self.through) === self.through ? HasManyDoubleLinked : HasManySingleLinked
return new Class(self, this).injectGetter(options)
obj[this.accessors.get] = function(options, queryOptions) {
options = options || {}
queryOptions = queryOptions || {}
var Class = Object(association.through) === association.through ? HasManyDoubleLinked : HasManySingleLinked
return new Class(association, this).injectGetter(options, queryOptions)
}
obj[this.accessors.hasAll] = function(objects, options) {
var instance = this;
return instance[self.accessors.get](options).then(function(associatedObjects) {
return Utils._.all(objects, function(o) {
return Utils._.any(associatedObjects, function(associatedObject) {
return Utils._.all(associatedObject.identifiers, function(key, identifier) {
return o[identifier] == associatedObject[identifier];
});
})
obj[this.accessors.hasAll] = function(instances, options) {
var instance = this
, where
options = options || {}
instances.forEach(function (instance) {
if (instance instanceof association.target.Instance) {
where = new Utils.or([where, instance.primaryKeyValues])
} else {
var _where = {}
_where[association.target.primaryKeyAttribute] = instance
where = new Utils.or([where, _where])
}
})
options.where = new Utils.and([
where,
options.where
])
return instance[association.accessors.get](
options,
{ raw: true }
).then(function(associatedObjects) {
return associatedObjects.length === instances.length
})
}
obj[this.accessors.hasSingle] = function(o, options) {
obj[this.accessors.hasSingle] = function(param, options) {
var instance = this
return instance[self.accessors.get](options).then(function(associatedObjects) {
return Utils._.any(associatedObjects, function(associatedObject) {
return Utils._.all(associatedObject.identifiers, function(key, identifier) {
return o[identifier] == associatedObject[identifier];
});
})
, where
options = options || {}
if (param instanceof association.target.Instance) {
where = param.primaryKeyValues
} else {
where = {}
where[association.target.primaryKeyAttribute] = param
}
options.where = new Utils.and([
where,
options.where
])
return instance[association.accessors.get](
options,
{ raw: true }
).then(function(associatedObjects) {
return associatedObjects.length !== 0
})
}
return this
}
HasMany.prototype.injectSetter = function(obj) {
var self = this
var association = this
, primaryKeyAttribute = association.target.primaryKeyAttribute
obj[this.accessors.set] = function (newAssociatedObjects, additionalAttributes) {
if (newAssociatedObjects === null) {
newAssociatedObjects = []
} else {
newAssociatedObjects = newAssociatedObjects.map(function (newAssociatedObject) {
if (!(newAssociatedObject instanceof association.target.Instance)) {
var tmpInstance = {}
tmpInstance[primaryKeyAttribute] = newAssociatedObject
return association.target.build(tmpInstance, {
isNewRecord: false
})
}
return newAssociatedObject
})
}
var instance = this
return instance[self.accessors.get]({
return instance[association.accessors.get]({
transaction: (additionalAttributes || {}).transaction
}).then(function(oldAssociatedObjects) {
var Class = Object(self.through) === self.through ? HasManyDoubleLinked : HasManySingleLinked
return new Class(self, instance).injectSetter(oldAssociatedObjects, newAssociatedObjects, additionalAttributes)
var Class = Object(association.through) === association.through ? HasManyDoubleLinked : HasManySingleLinked
return new Class(association, instance).injectSetter(oldAssociatedObjects, newAssociatedObjects, additionalAttributes)
})
}
obj[this.accessors.add] = function (newInstance, additionalAttributes) {
var instance = this
, primaryKey = newInstance.Model.primaryKeyAttribute
, where = new Utils.where(self.target.rawAttributes[primaryKey], newInstance[primaryKey])
, primaryKeyAttribute = association.target.primaryKeyAttribute
if (!(newInstance instanceof association.target.Instance)) {
var tmpInstance = {}
tmpInstance[primaryKeyAttribute] = newInstance
newInstance = association.target.build(tmpInstance, {
isNewRecord: false
})
}
if (Array.isArray(newInstance)) {
return obj[self.accessors.addMultiple](newInstance, additionalAttributes)
return obj[association.accessors.addMultiple](newInstance, additionalAttributes)
} else {
return instance[self.accessors.get]({
where: where,
return instance[association.accessors.get]({
where: newInstance.primaryKeyValues,
transaction: (additionalAttributes || {}).transaction
}).then(function(currentAssociatedObjects) {
if (currentAssociatedObjects.length === 0 || Object(self.through) === self.through) {
var Class = Object(self.through) === self.through ? HasManyDoubleLinked : HasManySingleLinked
return new Class(self, instance).injectAdder(newInstance, additionalAttributes, !!currentAssociatedObjects.length)
if (currentAssociatedObjects.length === 0 || Object(association.through) === association.through) {
var Class = Object(association.through) === association.through ? HasManyDoubleLinked : HasManySingleLinked
return new Class(association, instance).injectAdder(newInstance, additionalAttributes, !!currentAssociatedObjects.length)
} else {
return Utils.Promise.resolve(newInstance)
return Utils.Promise.resolve(currentAssociatedObjects[0])
}
})
}
}
obj[this.accessors.addMultiple] = function (newInstances, additionalAttributes) {
var Class = Object(self.through) === self.through ? HasManyDoubleLinked : HasManySingleLinked
return new Class(self, this).injectSetter([], newInstances, additionalAttributes)
var primaryKeyAttribute = association.target.primaryKeyAttribute
newInstances = newInstances.map(function (newInstance) {
if (!(newInstance instanceof association.target.Instance)) {
var tmpInstance = {}
tmpInstance[primaryKeyAttribute] = newInstance
return association.target.build(tmpInstance, {
isNewRecord: false
})
}
return newInstance
})
var Class = Object(association.through) === association.through ? HasManyDoubleLinked : HasManySingleLinked
return new Class(association, this).injectSetter([], newInstances, additionalAttributes)
}
obj[this.accessors.remove] = function (oldAssociatedObject, options) {
var instance = this
return instance[self.accessors.get]({
return instance[association.accessors.get]({
transaction: (options || {}).transaction
}).then(function(currentAssociatedObjects) {
var newAssociations = []
......@@ -348,7 +413,7 @@ module.exports = (function() {
}
})
return instance[self.accessors.set](newAssociations)
return instance[association.accessors.set](newAssociations)
})
}
......@@ -356,7 +421,7 @@ module.exports = (function() {
}
HasMany.prototype.injectCreator = function(obj) {
var self = this
var association = this
obj[this.accessors.create] = function(values, fieldsOrOptions) {
var instance = this
......@@ -366,14 +431,14 @@ module.exports = (function() {
options.transaction = fieldsOrOptions.transaction
}
if (Object(self.through) === self.through) {
if (Object(association.through) === association.through) {
// Create the related model instance
return self.target.create(values, fieldsOrOptions).then(function(newAssociatedObject) {
return instance[self.accessors.add](newAssociatedObject, options)
return association.target.create(values, fieldsOrOptions).then(function(newAssociatedObject) {
return instance[association.accessors.add](newAssociatedObject, options)
})
} else {
values[self.identifier] = instance.get(self.source.primaryKeyAttribute);
return self.target.create(values, fieldsOrOptions)
values[association.identifier] = instance.get(association.source.primaryKeyAttribute);
return association.target.create(values, fieldsOrOptions)
}
}
......@@ -385,7 +450,7 @@ module.exports = (function() {
* This is done because we need to keep the foreign key if another association
* is depending on it.
*
* @param {DaoFactory} daoFactory The source or target DaoFactory of this assocation
* @param {DaoFactory} daoFactory The source or target DaoFactory of this association
* @param {[type]} identifier The name of the foreign key identifier
* @return {Boolean} Whether or not the deletion of the foreign key is ok.
*/
......
......@@ -101,6 +101,13 @@ module.exports = (function() {
}
}).then(function () {
if (associatedInstance) {
if (!(associatedInstance instanceof association.target.Instance)) {
var tmpInstance = {}
tmpInstance[association.target.primaryKeyAttribute] = associatedInstance
associatedInstance = association.target.build(tmpInstance, {
isNewRecord: false
})
}
associatedInstance.set(association.identifier, instance.get(association.sourceIdentifier))
return associatedInstance.save(options)
}
......
......@@ -45,7 +45,20 @@ var Utils = require("./../utils")
* })
* ```
*
* In the above we specify that a user belongs to his profile picture. Conceptually, this might not make sense,
* There are several methods to update and add new assoications. Continuing with our example of users and pictures:
* ```js
* user.addPicture(p) // Add a single picture
* user.setPictures([p1, p2]) // Associate user with ONLY these two picture, all other associations will be deleted
* user.addPictures([p1, p2]) // Associate user with these two pictures, but don't touch any current associations
* ```
*
* You don't have to pass in a complete object to the association functions, if your associated model has a single primary key:
*
* ```js
* user.addPicture(req.query.pid) // Here pid is just an integer, representing the primary key of the picture
* ```
*
* In the example above we have specified that a user belongs to his profile picture. Conceptually, this might not make sense,
* but since we want to add the foreign key to the user model this is the way to do it.
* Note how we also specified `constraints: false` for profile picture. This is because we add a foreign key from
* user to picture (profilePictureId), and from picture to user (userId). If we were to add foreign keys to both, it would
......
......@@ -184,6 +184,25 @@ describe(Support.getTestDialectTeaser("BelongsTo"), function() {
})
})
it('supports passing the primary key instead of an object', function () {
var User = this.sequelize.define('UserXYZ', { username: DataTypes.STRING })
, Task = this.sequelize.define('TaskXYZ', { title: DataTypes.STRING })
Task.belongsTo(User)
return this.sequelize.sync({ force :true }).then(function () {
return User.create({ id: 15, username: 'jansemand' }).then(function (user) {
return Task.create({}).then(function (task) {
return task.setUserXYZ(user.id).then(function () {
return task.getUserXYZ().then(function (user) {
expect(user.username).to.equal('jansemand')
})
})
})
})
})
})
it('should not clobber atributes', function (done) {
var Comment = this.sequelize.define('comment', {
text: DataTypes.STRING
......
......@@ -113,6 +113,25 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
})
})
})
it('answers true if the label has been assigned when passing a primary key instead of an object', function() {
var self = this
return this.sequelize.Promise.all([
this.Article.create({ title: 'Article' }),
this.Label.create({ text: 'Awesomeness' }),
this.Label.create({ text: 'Epicness' })
]).spread(function (article, label1, label2) {
return article.addLabel(label1).then(function () {
return self.sequelize.Promise.all([
article.hasLabel(label1.id),
article.hasLabel(label2.id),
]).spread(function (hasLabel1, hasLabel2) {
expect(hasLabel1).to.be.true
expect(hasLabel2).to.be.false
})
})
})
})
})
describe('hasAll', function() {
......@@ -181,6 +200,20 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
})
})
it('answers false if only some labels have been assigned when passing a primary key instead of an object', function() {
return this.sequelize.Promise.all([
this.Article.create({ title: 'Article' }),
this.Label.create({ text: 'Awesomeness' }),
this.Label.create({ text: 'Epicness' })
]).spread(function (article, label1, label2) {
return article.addLabel(label1).then(function() {
return article.hasLabels([label1.id, label2.id]).then(function(result) {
expect(result).to.be.false
})
})
})
})
it('answers true if all label have been assigned', function(done) {
var chainer = new Sequelize.Utils.QueryChainer([
this.Article.create({ title: 'Article' }),
......@@ -197,6 +230,20 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
})
})
})
it('answers true if all label have been assigned when passing a primary key instead of an object', function() {
return this.sequelize.Promise.all([
this.Article.create({ title: 'Article' }),
this.Label.create({ text: 'Awesomeness' }),
this.Label.create({ text: 'Epicness' })
]).spread(function (article, label1, label2) {
return article.setLabels([label1, label2]).then(function() {
return article.hasLabels([label1.id, label2.id]).then(function(result) {
expect(result).to.be.true
})
})
})
})
})
describe('setAssociations', function() {
......@@ -257,6 +304,30 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
})
})
})
it('supports passing the primary key instead of an object', function () {
var Article = this.sequelize.define('Article', { title: DataTypes.STRING })
, Label = this.sequelize.define('Label', { text: DataTypes.STRING })
Article.hasMany(Label)
return this.sequelize.sync({ force :true }).then(function () {
return Article.create({}).then(function (article) {
return Label.create({ text: 'label one' }).then(function (label1) {
return Label.create({ text: 'label two' }).then(function (label2) {
return article.addLabel(label1.id).then(function () {
return article.setLabels([label2.id]).then(function () {
return article.getLabels().then(function (labels) {
expect(labels).to.have.length(1)
expect(labels[0].text).to.equal('label two')
})
})
})
})
})
})
})
})
})
describe('addAssociations', function() {
......@@ -291,6 +362,25 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
})
})
})
it('supports passing the primary key instead of an object', function () {
var Article = this.sequelize.define('Article', { 'title': DataTypes.STRING })
, Label = this.sequelize.define('Label', { 'text': DataTypes.STRING })
Article.hasMany(Label)
return this.sequelize.sync({ force :true }).then(function () {
return Article.create({}).then(function (article) {
return Label.create({ text: 'label one' }).then(function (label) {
return article.addLabel(label.id).then(function () {
return article.getLabels().then(function (labels) {
expect(labels[0].text).to.equal('label one') // Make sure that we didn't modify one of the other attributes while building / saving a new instance
})
})
})
})
})
})
})
describe('addMultipleAssociations', function () {
......@@ -871,6 +961,31 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
})
})
})
it('supports passing the primary key instead of an object', function () {
var User = this.sequelize.define('User', { username: DataTypes.STRING })
, Task = this.sequelize.define('Task', { title: DataTypes.STRING })
User.hasMany(Task)
Task.hasMany(User)
return this.sequelize.sync({ force :true }).then(function () {
return User.create({ id: 12 }).then(function (user) {
return Task.create({ id: 50, title: 'get started' }).then(function (task1) {
return Task.create({ id: 5, title: 'wat' }).then(function (task2) {
return user.addTask(task1.id).then(function () {
return user.setTasks([task2.id]).then(function () {
return user.getTasks().then(function (tasks) {
expect(tasks).to.have.length(1)
expect(tasks[0].title).to.equal('wat')
})
})
})
})
})
})
})
})
})
describe('createAssociations', function() {
......@@ -950,6 +1065,26 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
})
})
})
it('supports passing the primary key instead of an object', function () {
var User = this.sequelize.define('User', { username: DataTypes.STRING })
, Task = this.sequelize.define('Task', { title: DataTypes.STRING })
User.hasMany(Task)
Task.hasMany(User)
return this.sequelize.sync({ force :true }).then(function () {
return User.create({ id: 12 }).then(function (user) {
return Task.create({ id: 50, title: 'get started' }).then(function (task) {
return user.addTask(task.id).then(function () {
return user.getTasks().then(function (tasks) {
expect(tasks[0].title).to.equal('get started')
})
})
})
})
})
})
})
describe('addMultipleAssociations', function () {
......
......@@ -166,6 +166,25 @@ describe(Support.getTestDialectTeaser("HasOne"), function() {
})
})
})
it('supports passing the primary key instead of an object', function () {
var User = this.sequelize.define('UserXYZ', { username: Sequelize.STRING })
, Task = this.sequelize.define('TaskXYZ', { title: Sequelize.STRING })
User.hasOne(Task)
return this.sequelize.sync({ force :true }).then(function () {
return User.create({}).then(function (user) {
return Task.create({ id: 19, title: 'task it!' }).then(function (task) {
return user.setTaskXYZ(task.id).then(function () {
return user.getTaskXYZ().then(function (task) {
expect(task.title).to.equal('task it!')
})
})
})
})
})
})
})
describe('createAssociation', function() {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!