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

Commit df95670d by Daniel Durante

Merge pull request #690 from janmeier/hasmanyOptimization

hasMany setter uses bulk create and destroy to create and remove associations
2 parents 0c591847 4de13581
......@@ -23,6 +23,7 @@
- [FEATURE] `findOrCreate` now returns an additional flag (`created`), that is true if a model was created, and false if it was found [#648](https://github.com/sequelize/sequelize/pull/648). janmeier
- [FEATURE] Field and table comments for MySQL and PG. [#523](https://github.com/sequelize/sequelize/pull/523). MySQL by iamjochen. PG by janmeier
- [FEATURE] BigInts can now be used for autoincrement/serial columns. [#673](https://github.com/sequelize/sequelize/pull/673). thanks to sevastos
- [REFACTORING] hasMany now uses a single SQL statement when creating and destroying associations, instead of removing each association seperately [690](https://github.com/sequelize/sequelize/pull/690). Inspired by [#104](https://github.com/sequelize/sequelize/issues/104). janmeier
# v1.6.0 #
- [DEPENDENCIES] upgrade mysql to alpha7. You *MUST* use this version or newer for DATETIMEs to work
......
......@@ -42,35 +42,52 @@ module.exports = (function() {
HasManyDoubleLinked.prototype.injectSetter = function(emitterProxy, oldAssociations, newAssociations) {
var self = this
destroyObsoleteAssociations
.call(this, oldAssociations, newAssociations)
.on('sql', function(sql) { emitterProxy.emit('sql', sql) })
.error(function(err) { emitterProxy.emit('error', err) })
.success(function() {
var chainer = new Utils.QueryChainer
, chainer = new Utils.QueryChainer()
, association = self.__factory.target.associations[self.__factory.associationAccessor]
, foreignIdentifier = association.isSelfAssociation ? association.foreignIdentifier : association.identifier
, obsoleteAssociations = oldAssociations.filter(function (old) {
// Return only those old associations that are not found in new
return !Utils._.find(newAssociations, function (obj) {
return obj.id === old.id
})
})
, unassociatedObjects = newAssociations.filter(function (obj) {
return !Utils._.find(oldAssociations, function (old) {
return obj.id === old.id
})
})
unassociatedObjects.forEach(function(unassociatedObject) {
if (obsoleteAssociations.length > 0) {
var foreignIds = obsoleteAssociations.map(function (associatedObject) {
return associatedObject.id
})
, primaryKeys = Object.keys(self.__factory.connectorDAO.rawAttributes)
, foreignKey = primaryKeys.filter(function(pk) { return pk != self.__factory.identifier })[0]
var where = {}
where[self.__factory.identifier] = self.instance.id
where[foreignKey] = foreignIds
chainer.add(self.__factory.connectorDAO.destroy(where))
}
if (unassociatedObjects.length > 0) {
var bulk = unassociatedObjects.map(function(unassociatedObject) {
var attributes = {}
attributes[self.__factory.identifier] = self.instance.id
attributes[foreignIdentifier] = unassociatedObject.id
chainer.add(self.__factory.connectorDAO.create(attributes))
return attributes
})
chainer.add(self.__factory.connectorDAO.bulkCreate(bulk))
}
chainer
.run()
.success(function() { emitterProxy.emit('success', newAssociations) })
.error(function(err) { emitterProxy.emit('error', err) })
.on('sql', function(sql) { emitterProxy.emit('sql', sql) })
})
}
HasManyDoubleLinked.prototype.injectAdder = function(emitterProxy, newAssociation) {
......@@ -87,57 +104,5 @@ module.exports = (function() {
.on('sql', function(sql) { emitterProxy.emit('sql', sql) })
}
// private
var destroyObsoleteAssociations = function(oldAssociations, newAssociations) {
var self = this
return new Utils.CustomEventEmitter(function(emitter) {
var chainer = new Utils.QueryChainer()
var foreignIdentifier = self.__factory.target.associations[self.__factory.associationAccessor].identifier
var obsoleteAssociations = oldAssociations.filter(function (old) {
// Return only those old associations that are not found in new
return !Utils._.find(newAssociations, function (obj) {
return obj.id === old.id
})
})
if (obsoleteAssociations.length === 0) {
return emitter.emit('success', null)
}
obsoleteAssociations.forEach(function(associatedObject) {
var where = {}
, primaryKeys = Object.keys(self.__factory.connectorDAO.rawAttributes)
, foreignKey = primaryKeys.filter(function(pk) { return pk != self.__factory.identifier })[0]
, notFoundEmitters = []
where[self.__factory.identifier] = self.instance.id
where[foreignKey] = associatedObject.id
self.__factory.connectorDAO
.find({ where: where })
.success(function(connector) {
if (connector === null) {
notFoundEmitters.push(null)
} else {
chainer.add(connector.destroy())
}
if ((chainer.emitters.length + notFoundEmitters.length) === obsoleteAssociations.length) {
// found all obsolete connectors and will delete them now
chainer
.run()
.success(function() { emitter.emit('success', null) })
.error(function(err) { emitter.emit('error', err) })
.on('sql', function(sql) { emitter.emit('sql', sql) })
}
})
.error(function(err) { emitter.emit('error', err) })
.on('sql', function(sql) { emitter.emit('sql', sql) })
})
}).run()
}
return HasManyDoubleLinked
})()
......@@ -7,7 +7,8 @@ module.exports = (function() {
}
HasManySingleLinked.prototype.injectGetter = function(options) {
var where = {}, options = options || {}
var where = {}
options = options || {}
where[this.__factory.identifier] = this.instance.id
......@@ -17,7 +18,6 @@ module.exports = (function() {
HasManySingleLinked.prototype.injectSetter = function(emitter, oldAssociations, newAssociations) {
var self = this
, options = this.__factory.options
, chainer = new Utils.QueryChainer()
, obsoleteAssociations = oldAssociations.filter(function (old) {
return !Utils._.find(newAssociations, function (obj) {
......@@ -29,23 +29,37 @@ module.exports = (function() {
return obj.id === old.id
})
})
, update
if (obsoleteAssociations.length > 0) {
// clear the old associations
obsoleteAssociations.forEach(function(associatedObject) {
var obsoleteIds = obsoleteAssociations.map(function(associatedObject) {
associatedObject[self.__factory.identifier] = null
chainer.add(associatedObject.save())
return associatedObject.id
})
update = {}
update[self.__factory.identifier] = null
chainer.add(this.__factory.target.update(update, { id: obsoleteIds }))
}
if (unassociatedObjects.length > 0) {
// set the new associations
unassociatedObjects.forEach(function(associatedObject) {
var unassociatedIds = unassociatedObjects.map(function(associatedObject) {
associatedObject[self.__factory.identifier] = self.instance.id
chainer.add(associatedObject.save())
return associatedObject.id
})
update = {}
update[self.__factory.identifier] = self.instance.id
chainer.add(this.__factory.target.update(update, { id: unassociatedIds }))
}
chainer
.run()
.success(function() { emitter.emit('success', newAssociations) })
.error(function(err) { emitter.emit('error', err) })
.on('sql', function(sql) { emitter.emit('sql', sql) })
}
HasManySingleLinked.prototype.injectAdder = function(emitterProxy, newAssociation) {
......
......@@ -3,6 +3,7 @@ if (typeof require === 'function') {
, Helpers = require('../buster-helpers')
, Sequelize = require('../../index')
, dialect = Helpers.getTestDialect()
, _ = require('lodash')
}
buster.spec.expose()
......@@ -32,8 +33,6 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() {
})
it('does not have any labels assigned to it initially', function(done) {
var self = this
var chainer = new Sequelize.Utils.QueryChainer([
this.Article.create({ title: 'Article' }),
this.Label.create({ text: 'Awesomeness' }),
......@@ -55,8 +54,6 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() {
})
it('answers true if the label has been assigned', function(done) {
var self = this
var chainer = new Sequelize.Utils.QueryChainer([
this.Article.create({ title: 'Article' }),
this.Label.create({ text: 'Awesomeness' }),
......@@ -90,8 +87,6 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() {
})
it('answers false if only some labels have been assigned', function(done) {
var self = this
var chainer = new Sequelize.Utils.QueryChainer([
this.Article.create({ title: 'Article' }),
this.Label.create({ text: 'Awesomeness' }),
......@@ -109,8 +104,6 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() {
})
it('answers true if all label have been assigned', function(done) {
var self = this
var chainer = new Sequelize.Utils.QueryChainer([
this.Article.create({ title: 'Article' }),
this.Label.create({ text: 'Awesomeness' }),
......@@ -186,7 +179,7 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() {
describe("getting assocations with options", function() {
before(function(done) {
var self = this;
var self = this
this.User = this.sequelize.define('User', { username: Sequelize.STRING })
this.Task = this.sequelize.define('Task', { title: Sequelize.STRING, active: Sequelize.BOOLEAN })
......@@ -210,7 +203,7 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() {
this.User.find({where: {username: 'John'}}).success(function (john) {
john.getTasks().success(function (tasks) {
expect(tasks.length).toEqual(2)
done();
done()
})
})
})
......@@ -219,17 +212,66 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() {
this.User.find({ where: { username: 'John' } }).success(function (john) {
john.getTasks({ where: { active: true }, limit: 10, order: 'id DESC' }).success(function (tasks) {
expect(tasks.length).toEqual(1)
done();
done()
})
})
})
})
describe('optimizations using bulk create, destroy and update', function () {
before(function (done) {
this.User = this.sequelize.define('User', { username: Sequelize.STRING }, {timestamps: false})
this.Task = this.sequelize.define('Task', { title: Sequelize.STRING }, {timestamps: false})
this.User.hasMany(this.Task)
this.sequelize.sync({force: true}).success(done)
})
it('uses one UPDATE statement', function (done) {
var spy = this.spy()
this.User.create({ username: 'foo' }).success(function(user) {
this.Task.create({ title: 'task1' }).success(function(task1) {
this.Task.create({ title: 'task2' }).success(function(task2) {
user.setTasks([task1, task2]).on('sql', spy).on('sql', _.after(2, function (sql) { // We don't care about SELECt, only UPDAET
expect(sql).toMatch("UPDATE")
expect(sql).toMatch("IN (1,2)")
})).success(function () {
expect(spy).toHaveBeenCalledTwice() // Once for SELECT, once for UPDATE
done()
})
}.bind(this))
}.bind(this))
}.bind(this))
})
it('uses one UPDATE statement', function (done) {
var spy = this.spy()
this.User.create({ username: 'foo' }).success(function (user) {
this.Task.create({ title: 'task1' }).success(function (task1) {
this.Task.create({ title: 'task2' }).success(function (task2) {
user.setTasks([task1, task2]).success(function () {
user.setTasks(null).on('sql', spy).on('sql', _.after(2, function (sql) { // We don't care about SELECT, only UPDATE
expect(sql).toMatch("UPDATE")
expect(sql).toMatch("IN (1,2)")
})).success(function () {
expect(spy).toHaveBeenCalledTwice() // Once for SELECT, once for UPDATE
done()
})
})
}.bind(this))
}.bind(this))
}.bind(this))
})
}) // end optimization using bulk create, destroy and update
})
describe('(N:M)', function() {
describe("getting assocations with options", function() {
before(function(done) {
var self = this;
var self = this
this.User = this.sequelize.define('User', { username: Sequelize.STRING })
this.Task = this.sequelize.define('Task', { title: Sequelize.STRING, active: Sequelize.BOOLEAN })
......@@ -254,7 +296,7 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() {
this.User.find({where: {username: 'John'}}).success(function (john) {
john.getTasks().success(function (tasks) {
expect(tasks.length).toEqual(2)
done();
done()
})
})
})
......@@ -263,7 +305,7 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() {
this.User.find({where: {username: 'John'}}).success(function (john) {
john.getTasks({where: {active: true}}).success(function (tasks) {
expect(tasks.length).toEqual(1)
done();
done()
})
})
})
......@@ -323,6 +365,56 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() {
})
})
})
describe('optimizations using bulk create, destroy and update', function () {
before(function (done) {
this.User = this.sequelize.define('User', { username: Sequelize.STRING }, {timestamps: false})
this.Task = this.sequelize.define('Task', { title: Sequelize.STRING }, {timestamps: false})
this.User.hasMany(this.Task)
this.Task.hasMany(this.User)
this.sequelize.sync({force: true}).success(done)
})
it('uses one insert into statement', function (done) {
var spy = this.spy()
this.User.create({ username: 'foo' }).success(function(user) {
this.Task.create({ title: 'task1' }).success(function(task1) {
this.Task.create({ title: 'task2' }).success(function(task2) {
user.setTasks([task1, task2]).on('sql', spy).on('sql', _.after(2, function (sql) {
expect(sql).toMatch("INSERT INTO")
expect(sql).toMatch("VALUES (1,1),(2,1)")
})).success(function () {
expect(spy).toHaveBeenCalledTwice() // Once for SELECT, once for INSERT into
done()
})
}.bind(this))
}.bind(this))
}.bind(this))
})
it('uses one delete from statement', function (done) {
var spy = this.spy()
this.User.create({ username: 'foo' }).success(function (user) {
this.Task.create({ title: 'task1' }).success(function (task1) {
this.Task.create({ title: 'task2' }).success(function (task2) {
user.setTasks([task1, task2]).success(function () {
user.setTasks(null).on('sql', spy).on('sql', _.after(2, function (sql) {
expect(sql).toMatch("DELETE FROM")
expect(sql).toMatch("IN (1,2)")
})).success(function () {
expect(spy).toHaveBeenCalledTwice() // Once for SELECT, once for DELETE
done()
})
})
}.bind(this))
}.bind(this))
}.bind(this))
})
}) // end optimization using bulk create, destroy and update
})
describe("Foreign key constraints", function() {
......@@ -480,5 +572,4 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() {
})
})
})
})
\ No newline at end of file
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!