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

Commit 088c2dbd by Mick Hansen

refactor(include): refactor N:M subquery filtering to use the query generator re…

…cursively by extracting the needed include tree
1 parent 1e9d02bb
...@@ -723,7 +723,9 @@ module.exports = (function() { ...@@ -723,7 +723,9 @@ module.exports = (function() {
, mainAttributes = options.attributes && options.attributes.slice(0) , mainAttributes = options.attributes && options.attributes.slice(0)
, mainJoinQueries = [] , mainJoinQueries = []
// We'll use a subquery if we have hasMany associations and a limit and a filtered/required association // We'll use a subquery if we have hasMany associations and a limit and a filtered/required association
, subQuery = limit && (options.hasIncludeWhere || options.hasIncludeRequired || options.hasMultiAssociation) && options.subQuery !== false , subQuery = options.subQuery === undefined ?
limit && (options.hasIncludeWhere || options.hasIncludeRequired || options.hasMultiAssociation) && options.subQuery !== false :
options.subquery
, subQueryItems = [] , subQueryItems = []
, subQueryAttributes = null , subQueryAttributes = null
, subJoinQueries = [] , subJoinQueries = []
...@@ -920,17 +922,74 @@ module.exports = (function() { ...@@ -920,17 +922,74 @@ module.exports = (function() {
joinQueryItem += ' AND ' + targetWhere; joinQueryItem += ' AND ' + targetWhere;
if (subQuery && include.required) { if (subQuery && include.required) {
if (!options.where) options.where = {}; if (!options.where) options.where = {};
(function (include) {
// Creating the as-is where for the subQuery, checks that the required association exists // Closure to use sane local variables
options.where['__' + throughAs] = self.sequelize.asIs(['(',
var parent = include
'SELECT ' + self.quoteIdentifier(throughAs) + '.' + self.quoteIdentifier(identSource) + ' FROM ' + self.quoteTable(throughTable, throughAs), , child = include
! include.required && joinType + self.quoteTable(association.source.tableName, tableSource) + ' ON ' + sourceJoinOn || '', , nestedIncludes = []
joinType + self.quoteTable(table, as) + ' ON ' + targetJoinOn, , topParent
'WHERE ' + (! include.required && targetWhere || sourceJoinOn + ' AND ' + targetWhere), , topInclude
'LIMIT 1', , $query;
')', 'IS NOT NULL'].join(' ')); while (parent = parent.parent) {
nestedIncludes = [_.extend({}, child, {include: nestedIncludes})];
child = parent;
}
topInclude = nestedIncludes[0];
topParent = topInclude.parent;
if (topInclude.through && Object(topInclude.through.model) === topInclude.through.model) {
$query = self.selectQuery(topInclude.through.model.getTableName(), {
attributes: [topInclude.through.model.primaryKeyAttributes[0]],
include: [{
model: topInclude.model,
as: topInclude.model.name,
attributes: [],
association: {
associationType: 'BelongsTo',
isSingleAssociation: true,
source: topInclude.association.target,
target: topInclude.association.source,
identifier: topInclude.association.foreignIdentifier,
identifierField: topInclude.association.foreignIdentifierField
},
required: true,
include: topInclude.include,
_pseudo: true
}],
where: {
$join: self.sequelize.asIs([
self.quoteTable(topParent.model.name) + '.' + self.quoteIdentifier(topParent.model.primaryKeyAttributes[0]),
self.quoteIdentifier(topInclude.through.model.name) + '.' + self.quoteIdentifier(topInclude.association.identifierField)
].join(" = "))
},
limit: 1,
includeIgnoreAttributes: false
}, topInclude.through.model);
} else {
$query = self.selectQuery(topInclude.model.tableName, {
attributes: [topInclude.model.primaryKeyAttributes[0]],
include: topInclude.include,
where: {
$join: self.sequelize.asIs([
self.quoteTable(topParent.model.name) + '.' + self.quoteIdentifier(topParent.model.primaryKeyAttributes[0]),
self.quoteIdentifier(topInclude.model.name) + '.' + self.quoteIdentifier(topInclude.association.identifierField)
].join(" = "))
},
limit: 1,
includeIgnoreAttributes: false
}, topInclude.model);
}
options.where['__' + throughAs] = self.sequelize.asIs([
'(',
$query.replace(/\;$/, ""),
')',
'IS NOT NULL'
].join(' '));
})(include);
} }
} }
} else { } else {
......
...@@ -752,7 +752,7 @@ module.exports = (function() { ...@@ -752,7 +752,7 @@ module.exports = (function() {
type: QueryTypes.SELECT, type: QueryTypes.SELECT,
hasJoin: hasJoin, hasJoin: hasJoin,
tableNames: Object.keys(tableNames) tableNames: Object.keys(tableNames)
}, queryOptions, { transaction: (options || {}).transaction })); }, queryOptions, { transaction: options.transaction }));
}).tap(function(results) { }).tap(function(results) {
if (options.hooks) { if (options.hooks) {
return this.runHooks('afterFind', results, options); return this.runHooks('afterFind', results, options);
...@@ -1816,6 +1816,7 @@ module.exports = (function() { ...@@ -1816,6 +1816,7 @@ module.exports = (function() {
options.hasIncludeRequired = options.hasIncludeRequired || include.hasIncludeRequired || !!include.required; options.hasIncludeRequired = options.hasIncludeRequired || include.hasIncludeRequired || !!include.required;
} }
}; };
Model.$validateIncludedElements = validateIncludedElements;
var validateIncludedElement = function(include, tableNames) { var validateIncludedElement = function(include, tableNames) {
if (!include.hasOwnProperty('model') && !include.hasOwnProperty('association')) { if (!include.hasOwnProperty('model') && !include.hasOwnProperty('association')) {
......
...@@ -7,6 +7,7 @@ var chai = require('chai') ...@@ -7,6 +7,7 @@ var chai = require('chai')
, DataTypes = require(__dirname + "/../../lib/data-types") , DataTypes = require(__dirname + "/../../lib/data-types")
, datetime = require('chai-datetime') , datetime = require('chai-datetime')
, async = require('async') , async = require('async')
, Promise = Sequelize.Promise;
chai.use(datetime) chai.use(datetime)
chai.config.includeStack = true chai.config.includeStack = true
...@@ -235,7 +236,7 @@ describe(Support.getTestDialectTeaser("Include"), function () { ...@@ -235,7 +236,7 @@ describe(Support.getTestDialectTeaser("Include"), function () {
} }
}) })
it('should accept nested `where` and `limit` at the same time', function (done) { it('should accept nested `where` and `limit` at the same time', function () {
var Product = this.sequelize.define('Product', { var Product = this.sequelize.define('Product', {
title: DataTypes.STRING title: DataTypes.STRING
}) })
...@@ -247,76 +248,53 @@ describe(Support.getTestDialectTeaser("Include"), function () { ...@@ -247,76 +248,53 @@ describe(Support.getTestDialectTeaser("Include"), function () {
}) })
, Set = this.sequelize.define('Set', { , Set = this.sequelize.define('Set', {
title: DataTypes.STRING title: DataTypes.STRING
}) });
Set.hasMany(Product)
Product.belongsTo(Set)
Product.hasMany(Tag, {through: ProductTag})
Tag.hasMany(Product, {through: ProductTag})
this.sequelize.sync({force: true}).then(function () { Set.hasMany(Product);
Product.belongsTo(Set);
Product.hasMany(Tag, {through: ProductTag});
Tag.hasMany(Product, {through: ProductTag});
return Set.bulkCreate([ return this.sequelize.sync({force: true}).then(function () {
return Promise.join(
Set.bulkCreate([
{title: 'office'} {title: 'office'}
]) ]),
.then( function() { Product.bulkCreate([
return Product.bulkCreate([
{title: 'Chair'}, {title: 'Chair'},
{title: 'Desk'}, {title: 'Desk'},
{title: 'Dress'} {title: 'Dress'}
]) ]),
}) Tag.bulkCreate([
.then( function() {
return Tag.bulkCreate([
{name: 'A'}, {name: 'A'},
{name: 'B'}, {name: 'B'},
{name: 'C'} {name: 'C'}
]).done(function () { ])
return Tag.findAll() ).then(function () {
}) return Promise.join(
}) Set.findAll(),
.then(function() { Product.findAll(),
return Set.findAll() Tag.findAll()
}) );
.then(function(sets) { }).spread(function (sets, products, tags) {
return [sets, Product.findAll()] return Promise.join(
}) sets[0].addProducts([products[0], products[1]]),
.spread( function(sets, products) { products[0].addTag(tags[0], {priority: 1}).then(function() {
return sets[0].addProduct(products[0]) return products[0].addTag(tags[1], {priority:2});
.then( function() { }).then(function() {
return sets[0].addProduct(products[1]) return products[0].addTag(tags[2], {priority:1});
}),
products[1].addTag(tags[1], {priority:2}).then(function() {
return products[2].addTag(tags[1], {priority:3});
}).then( function() { }).then( function() {
return [sets, products] return products[2].addTag(tags[2], {priority:0});
})
})
.spread( function(sets, products) {
return [sets, products, Tag.findAll()]
})
.spread( function(sets, products, tags) {
return products[0].addTag(tags[0], {priority:1})
.then( function() {
return products[0].addTag(tags[1], {priority:2})
})
.then( function() {
return products[0].addTag(tags[2], {priority:1})
})
.then( function() {
return products[1].addTag(tags[1], {priority:2})
}) })
.then( function() { );
return products[2].addTag(tags[1], {priority:3}) }).then(function() {
})
.then( function() {
return products[2].addTag(tags[2], {priority:0})
})
})
.then( function() {
return Set.findAll({ return Set.findAll({
include: [ include: [{
{
model: Product, model: Product,
include: [ include: [{
{
model: Tag, model: Tag,
where: { where: {
name: 'A' name: 'A'
...@@ -324,17 +302,12 @@ describe(Support.getTestDialectTeaser("Include"), function () { ...@@ -324,17 +302,12 @@ describe(Support.getTestDialectTeaser("Include"), function () {
}] }]
}], }],
limit: 1 limit: 1
}) }).on('sql', function (sql) {
}) console.log(sql);
.then( function() { });
done() });
}) });
.catch(function (err) { });
expect(err).not.to.be.ok
done()
})
})
})
it('should support an include with multiple different association types', function (done) { it('should support an include with multiple different association types', function (done) {
var User = this.sequelize.define('User', {}) var User = this.sequelize.define('User', {})
, Product = this.sequelize.define('Product', { , Product = this.sequelize.define('Product', {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!