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

Commit 6ba2b674 by Mick Hansen

feat(has-many): integrate groupedLimit into hasMany.get to support limit with include.seperate

1 parent e440f263
...@@ -259,7 +259,8 @@ HasMany.prototype.get = function(instances, options) { ...@@ -259,7 +259,8 @@ HasMany.prototype.get = function(instances, options) {
var association = this var association = this
, where = {} , where = {}
, Model = association.target , Model = association.target
, instance; , instance
, values;
if (!Array.isArray(instances)) { if (!Array.isArray(instances)) {
instance = instances; instance = instances;
...@@ -273,15 +274,28 @@ HasMany.prototype.get = function(instances, options) { ...@@ -273,15 +274,28 @@ HasMany.prototype.get = function(instances, options) {
} }
if (instances) { if (instances) {
where[association.foreignKey] = { values = instances.map(function (instance) {
$in: instances.map(function (instance) {
return instance.get(association.source.primaryKeyAttribute, {raw: true}); return instance.get(association.source.primaryKeyAttribute, {raw: true});
}) });
if (options.limit) {
options.groupedLimit = {
limit: options.limit,
on: association.foreignKey,
values: values
};
delete options.limit;
} else {
where[association.foreignKey] = {
$in: values
}; };
}
} else { } else {
where[association.foreignKey] = instance.get(association.source.primaryKeyAttribute, {raw: true}); where[association.foreignKey] = instance.get(association.source.primaryKeyAttribute, {raw: true});
} }
options.where = options.where ? options.where = options.where ?
{$and: [where, options.where]} : {$and: [where, options.where]} :
where; where;
......
...@@ -11,7 +11,8 @@ AbstractDialect.prototype.supports = { ...@@ -11,7 +11,8 @@ AbstractDialect.prototype.supports = {
'LIMIT ON UPDATE': false, 'LIMIT ON UPDATE': false,
'ON DUPLICATE KEY': true, 'ON DUPLICATE KEY': true,
'ORDER NULLS': false, 'ORDER NULLS': false,
'UNION': true,
'UNION ALL': true,
/* What is the dialect's keyword for INSERT IGNORE */ /* What is the dialect's keyword for INSERT IGNORE */
'IGNORE': '', 'IGNORE': '',
...@@ -46,6 +47,7 @@ AbstractDialect.prototype.supports = { ...@@ -46,6 +47,7 @@ AbstractDialect.prototype.supports = {
using: true, using: true,
}, },
joinTableDependent: true, joinTableDependent: true,
groupedLimit: true,
indexViaAlter: false, indexViaAlter: false,
JSON: false, JSON: false,
deferrableConstraints: false deferrableConstraints: false
......
...@@ -1025,10 +1025,10 @@ var QueryGenerator = { ...@@ -1025,10 +1025,10 @@ var QueryGenerator = {
mainAttributes = mainAttributes || (options.include ? [mainTableAs + '.*'] : ['*']); mainAttributes = mainAttributes || (options.include ? [mainTableAs + '.*'] : ['*']);
// If subquery, we ad the mainAttributes to the subQuery and set the mainAttributes to select * from subquery // If subquery, we ad the mainAttributes to the subQuery and set the mainAttributes to select * from subquery
if (subQuery) { if (subQuery || options.groupedLimit) {
// We need primary keys // We need primary keys
subQueryAttributes = mainAttributes; subQueryAttributes = mainAttributes;
mainAttributes = [mainTableAs + '.*']; mainAttributes = [(mainTableAs || table) + '.*'];
} }
if (options.include) { if (options.include) {
...@@ -1314,7 +1314,7 @@ var QueryGenerator = { ...@@ -1314,7 +1314,7 @@ var QueryGenerator = {
if (include.include) { if (include.include) {
include.include.filter(function (include) { include.include.filter(function (include) {
return !include.seperate; return !include.separate;
}).forEach(function(childInclude) { }).forEach(function(childInclude) {
if (childInclude._pseudo) return; if (childInclude._pseudo) return;
var childJoinQueries = generateJoinQueries(childInclude, as); var childJoinQueries = generateJoinQueries(childInclude, as);
...@@ -1334,7 +1334,7 @@ var QueryGenerator = { ...@@ -1334,7 +1334,7 @@ var QueryGenerator = {
// Loop through includes and generate subqueries // Loop through includes and generate subqueries
options.include.filter(function (include) { options.include.filter(function (include) {
return !include.seperate; return !include.separate;
}).forEach(function(include) { }).forEach(function(include) {
var joinQueries = generateJoinQueries(include, mainTableAs); var joinQueries = generateJoinQueries(include, mainTableAs);
...@@ -1359,21 +1359,24 @@ var QueryGenerator = { ...@@ -1359,21 +1359,24 @@ var QueryGenerator = {
mainTableAs = table; mainTableAs = table;
} }
mainQueryItems.push('SELECT * FROM ('+ mainQueryItems.push('SELECT '+mainAttributes.join(', ')+' FROM ('+
options.groupedLimit.values.map(function (value) { options.groupedLimit.values.map(function (value) {
var where = _.assign({}, options.where); var where = _.assign({}, options.where);
where[options.groupedLimit.on] = value; where[options.groupedLimit.on] = value;
return '('+self.selectQuery( return '('+self.selectQuery(
options.table, table,
{ {
attributes: options.attributes, attributes: options.attributes,
limit: options.groupedLimit.limit, limit: options.groupedLimit.limit,
order: options.order, order: options.order,
where: where where: where
} },
model
).replace(/;$/, '')+')'; ).replace(/;$/, '')+')';
}).join(' UNION ALL ') }).join(
self._dialect.supports['UNION ALL'] ?' UNION ALL ' : ' UNION '
)
+')'); +')');
} else { } else {
mainQueryItems.push('SELECT ' + mainAttributes.join(', ') + ' FROM ' + table); mainQueryItems.push('SELECT ' + mainAttributes.join(', ') + ' FROM ' + table);
......
...@@ -20,11 +20,13 @@ var SqliteDialect = function(sequelize) { ...@@ -20,11 +20,13 @@ var SqliteDialect = function(sequelize) {
SqliteDialect.prototype.supports = _.merge(_.cloneDeep(Abstract.prototype.supports), { SqliteDialect.prototype.supports = _.merge(_.cloneDeep(Abstract.prototype.supports), {
'DEFAULT': false, 'DEFAULT': false,
'DEFAULT VALUES': true, 'DEFAULT VALUES': true,
'UNION ALL': false,
'IGNORE': ' OR IGNORE', 'IGNORE': ' OR IGNORE',
index: { index: {
using: false using: false
}, },
joinTableDependent: false joinTableDependent: false,
groupedLimit: false
}); });
SqliteDialect.prototype.Query = Query; SqliteDialect.prototype.Query = Query;
......
...@@ -584,15 +584,15 @@ validateIncludedElement = function(include, tableNames, options) { ...@@ -584,15 +584,15 @@ validateIncludedElement = function(include, tableNames, options) {
include.where = include.where ? { $and: [include.where, include.association.scope] }: include.association.scope; include.where = include.where ? { $and: [include.where, include.association.scope] }: include.association.scope;
} }
if (include.limit && include.seperate === undefined) { if (include.limit && include.separate === undefined) {
include.seperate = true; include.separate = true;
} }
if (include.seperate === true && !(include.association instanceof HasMany)) { if (include.separate === true && !(include.association instanceof HasMany)) {
throw new Error('Only HasMany associations support include.seperate'); throw new Error('Only HasMany associations support include.separate');
} }
if (include.seperate === true) { if (include.separate === true) {
include.duplicating = false; include.duplicating = false;
} }
...@@ -1191,6 +1191,8 @@ Model.prototype.all = function(options) { ...@@ -1191,6 +1191,8 @@ Model.prototype.all = function(options) {
* @param {Object} [options.include[].where] Where clauses to apply to the child models. Note that this converts the eager load to an inner join, unless you explicitly set `required: false` * @param {Object} [options.include[].where] Where clauses to apply to the child models. Note that this converts the eager load to an inner join, unless you explicitly set `required: false`
* @param {Array<String>} [options.include[].attributes] A list of attributes to select from the child model * @param {Array<String>} [options.include[].attributes] A list of attributes to select from the child model
* @param {Boolean} [options.include[].required] If true, converts to an inner join, which means that the parent model will only be loaded if it has any matching children. True if `include.where` is set, false otherwise. * @param {Boolean} [options.include[].required] If true, converts to an inner join, which means that the parent model will only be loaded if it has any matching children. True if `include.where` is set, false otherwise.
* @param {Boolean} [options.include[].separate] If true, runs a separate query to fetch the associated instances, only supported for hasMany associations
* @param {Number} [options.include[].limit] Limit the joined rows, only supported with include.separate=true
* @param {Object} [options.include[].through.where] Filter on the join model for belongsToMany relations * @param {Object} [options.include[].through.where] Filter on the join model for belongsToMany relations
* @param {Array} [options.include[].through.attributes] A list of attributes to select from the join model for belongsToMany relations * @param {Array} [options.include[].through.attributes] A list of attributes to select from the join model for belongsToMany relations
* @param {Array<Object|Model>} [options.include[].include] Load further nested related models * @param {Array<Object|Model>} [options.include[].include] Load further nested related models
...@@ -1269,6 +1271,7 @@ Model.prototype.findAll = function(options) { ...@@ -1269,6 +1271,7 @@ Model.prototype.findAll = function(options) {
}).then(function() { }).then(function() {
originalOptions = optClone(options); originalOptions = optClone(options);
options.tableNames = Object.keys(tableNames); options.tableNames = Object.keys(tableNames);
return this.QueryInterface.select(this, this.getTableName(options), options); return this.QueryInterface.select(this, this.getTableName(options), options);
}).tap(function(results) { }).tap(function(results) {
if (options.hooks) { if (options.hooks) {
...@@ -1286,7 +1289,7 @@ Model.$findSeperate = function(results, options) { ...@@ -1286,7 +1289,7 @@ Model.$findSeperate = function(results, options) {
if (options.plain) results = [results]; if (options.plain) results = [results];
return Promise.map(options.include, function (include) { return Promise.map(options.include, function (include) {
if (!include.seperate) { if (!include.separate) {
return Model.$findSeperate( return Model.$findSeperate(
results.reduce(function (memo, result) { results.reduce(function (memo, result) {
var associations = result.get(include.association.as); var associations = result.get(include.association.as);
......
...@@ -678,6 +678,7 @@ QueryInterface.prototype.select = function(model, tableName, options) { ...@@ -678,6 +678,7 @@ QueryInterface.prototype.select = function(model, tableName, options) {
options = options || {}; options = options || {};
options.type = QueryTypes.SELECT; options.type = QueryTypes.SELECT;
options.model = model; options.model = model;
return this.sequelize.query( return this.sequelize.query(
this.QueryGenerator.selectQuery(tableName, options, model), this.QueryGenerator.selectQuery(tableName, options, model),
options options
......
...@@ -26,8 +26,8 @@ describe(Support.getTestDialectTeaser('HasMany'), function() { ...@@ -26,8 +26,8 @@ describe(Support.getTestDialectTeaser('HasMany'), function() {
}); });
}); });
describe('(1:N)', function() {
describe('get', function () { describe('get', function () {
if (current.dialect.supports.groupedLimit) {
describe('multiple', function () { describe('multiple', function () {
it('should fetch associations for multiple instances', function () { it('should fetch associations for multiple instances', function () {
var User = this.sequelize.define('User', {}) var User = this.sequelize.define('User', {})
...@@ -67,9 +67,119 @@ describe(Support.getTestDialectTeaser('HasMany'), function() { ...@@ -67,9 +67,119 @@ describe(Support.getTestDialectTeaser('HasMany'), function() {
}); });
}); });
}); });
it('should fetch associations for multiple instances with limit and order', function () {
var User = this.sequelize.define('User', {})
, Task = this.sequelize.define('Task', {
title: DataTypes.STRING
});
User.Tasks = User.hasMany(Task, {as: 'tasks'});
return this.sequelize.sync({force: true}).then(function () {
return Promise.join(
User.create({
tasks: [
{title: 'b'},
{title: 'd'},
{title: 'c'},
{title: 'a'}
]
}, {
include: [User.Tasks]
}),
User.create({
tasks: [
{title: 'a'},
{title: 'c'},
{title: 'b'}
]
}, {
include: [User.Tasks]
})
);
}).then(function (users) {
return User.Tasks.get(users, {
limit: 2,
order: [
['title', 'ASC']
]
}).then(function (result) {
expect(result[users[0].id].length).to.equal(2);
expect(result[users[0].id][0].title).to.equal('a');
expect(result[users[0].id][1].title).to.equal('b');
expect(result[users[1].id].length).to.equal(2);
expect(result[users[1].id][0].title).to.equal('a');
expect(result[users[1].id][1].title).to.equal('b');
});
}); });
}); });
it('should fetch associations for multiple instances with limit and order and a belongsTo relation', function () {
var User = this.sequelize.define('User', {})
, Task = this.sequelize.define('Task', {
title: DataTypes.STRING,
categoryId: {
type: DataTypes.INTEGER,
field: 'category_id'
}
})
, Category = this.sequelize.define('Category', {});
User.Tasks = User.hasMany(Task, {as: 'tasks'});
Task.Category = Task.belongsTo(Category, {as: 'category', foreignKey: 'categoryId'});
return this.sequelize.sync({force: true}).then(function () {
return Promise.join(
User.create({
tasks: [
{title: 'b', category: {}},
{title: 'd', category: {}},
{title: 'c', category: {}},
{title: 'a', category: {}}
]
}, {
include: [{association: User.Tasks, include: [Task.Category]}]
}),
User.create({
tasks: [
{title: 'a', category: {}},
{title: 'c', category: {}},
{title: 'b', category: {}}
]
}, {
include: [{association: User.Tasks, include: [Task.Category]}]
})
);
}).then(function (users) {
return User.Tasks.get(users, {
limit: 2,
order: [
['title', 'ASC']
],
include: [Task.Category],
}).then(function (result) {
expect(result[users[0].id].length).to.equal(2);
expect(result[users[0].id][0].title).to.equal('a');
expect(result[users[0].id][0].category).to.be.ok;
expect(result[users[0].id][1].title).to.equal('b');
expect(result[users[0].id][1].category).to.be.ok;
expect(result[users[1].id].length).to.equal(2);
expect(result[users[1].id][0].title).to.equal('a');
expect(result[users[1].id][0].category).to.be.ok;
expect(result[users[1].id][1].title).to.equal('b');
expect(result[users[1].id][1].category).to.be.ok;
});
});
});
});
}
});
describe('(1:N)', function() {
describe('hasSingle', function() { describe('hasSingle', function() {
beforeEach(function() { beforeEach(function() {
this.Article = this.sequelize.define('Article', { 'title': DataTypes.STRING }); this.Article = this.sequelize.define('Article', { 'title': DataTypes.STRING });
......
...@@ -6,11 +6,13 @@ var chai = require('chai') ...@@ -6,11 +6,13 @@ var chai = require('chai')
, sinon = require('sinon') , sinon = require('sinon')
, Support = require(__dirname + '/../support') , Support = require(__dirname + '/../support')
, Sequelize = require(__dirname + '/../../../index') , Sequelize = require(__dirname + '/../../../index')
, current = Support.sequelize
, Promise = Sequelize.Promise; , Promise = Sequelize.Promise;
describe(Support.getTestDialectTeaser('Include'), function() { if (current.dialect.supports.groupedLimit) {
describe('seperate', function () { describe(Support.getTestDialectTeaser('Include'), function() {
it('should run a hasMany association in a seperate query', function () { describe('separate', function () {
it('should run a hasMany association in a separate query', function () {
var User = this.sequelize.define('User', {}) var User = this.sequelize.define('User', {})
, Task = this.sequelize.define('Task', {}) , Task = this.sequelize.define('Task', {})
, sqlSpy = sinon.spy(); , sqlSpy = sinon.spy();
...@@ -40,7 +42,7 @@ describe(Support.getTestDialectTeaser('Include'), function() { ...@@ -40,7 +42,7 @@ describe(Support.getTestDialectTeaser('Include'), function() {
).then(function () { ).then(function () {
return User.findAll({ return User.findAll({
include: [ include: [
{association: User.Tasks, seperate: true} {association: User.Tasks, separate: true}
], ],
order: [ order: [
['id', 'ASC'] ['id', 'ASC']
...@@ -57,7 +59,57 @@ describe(Support.getTestDialectTeaser('Include'), function() { ...@@ -57,7 +59,57 @@ describe(Support.getTestDialectTeaser('Include'), function() {
}); });
}); });
it('should run a nested (from a non-seperate include) hasMany association in a seperate query', function () { it('should run a hasMany association with limit in a separate query', function () {
var User = this.sequelize.define('User', {})
, Task = this.sequelize.define('Task', {})
, sqlSpy = sinon.spy();
User.Tasks = User.hasMany(Task, {as: 'tasks'});
return this.sequelize.sync({force: true}).then(function () {
return Promise.join(
User.create({
id: 1,
tasks: [
{},
{},
{}
]
}, {
include: [User.Tasks]
}),
User.create({
id: 2,
tasks: [
{},
{},
{},
{}
]
}, {
include: [User.Tasks]
})
).then(function () {
return User.findAll({
include: [
{association: User.Tasks, limit: 2}
],
order: [
['id', 'ASC']
],
logging: sqlSpy
});
}).then(function (users) {
expect(users[0].get('tasks')).to.be.ok;
expect(users[0].get('tasks').length).to.equal(2);
expect(users[1].get('tasks')).to.be.ok;
expect(users[1].get('tasks').length).to.equal(2);
expect(sqlSpy).to.have.been.calledTwice;
});
});
});
it('should run a nested (from a non-separate include) hasMany association in a separate query', function () {
var User = this.sequelize.define('User', {}) var User = this.sequelize.define('User', {})
, Company = this.sequelize.define('Company') , Company = this.sequelize.define('Company')
, Task = this.sequelize.define('Task', {}) , Task = this.sequelize.define('Task', {})
...@@ -98,7 +150,7 @@ describe(Support.getTestDialectTeaser('Include'), function() { ...@@ -98,7 +150,7 @@ describe(Support.getTestDialectTeaser('Include'), function() {
return User.findAll({ return User.findAll({
include: [ include: [
{association: User.Company, include: [ {association: User.Company, include: [
{association: Company.Tasks, seperate: true} {association: Company.Tasks, separate: true}
]} ]}
], ],
order: [ order: [
...@@ -116,7 +168,7 @@ describe(Support.getTestDialectTeaser('Include'), function() { ...@@ -116,7 +168,7 @@ describe(Support.getTestDialectTeaser('Include'), function() {
}); });
}); });
it('should run two nested hasMany association in a seperate queries', function () { it('should run two nested hasMany association in a separate queries', function () {
var User = this.sequelize.define('User', {}) var User = this.sequelize.define('User', {})
, Project = this.sequelize.define('Project', {}) , Project = this.sequelize.define('Project', {})
, Task = this.sequelize.define('Task', {}) , Task = this.sequelize.define('Task', {})
...@@ -169,8 +221,8 @@ describe(Support.getTestDialectTeaser('Include'), function() { ...@@ -169,8 +221,8 @@ describe(Support.getTestDialectTeaser('Include'), function() {
).then(function () { ).then(function () {
return User.findAll({ return User.findAll({
include: [ include: [
{association: User.Projects, seperate: true, include: [ {association: User.Projects, separate: true, include: [
{association: Project.Tasks, seperate: true} {association: Project.Tasks, separate: true}
]} ]}
], ],
order: [ order: [
...@@ -196,4 +248,5 @@ describe(Support.getTestDialectTeaser('Include'), function() { ...@@ -196,4 +248,5 @@ describe(Support.getTestDialectTeaser('Include'), function() {
}); });
}); });
}); });
}); });
\ No newline at end of file }
\ No newline at end of file
...@@ -60,11 +60,11 @@ suite(Support.getTestDialectTeaser('SQL'), function() { ...@@ -60,11 +60,11 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
] ]
} }
}, { }, {
default: 'SELECT * FROM ('+ default: 'SELECT [User].* FROM ('+
[ [
'(SELECT [email], [first_name] AS [firstName], [last_name] AS [lastName] FROM [User] WHERE [User].[companyId] = 1 ORDER BY [last_name] ASC LIMIT 3)', '(SELECT [email], [first_name] AS [firstName], [last_name] AS [lastName] FROM [User] WHERE [User].[companyId] = 1 ORDER BY [last_name] ASC'+sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })+')',
'(SELECT [email], [first_name] AS [firstName], [last_name] AS [lastName] FROM [User] WHERE [User].[companyId] = 5 ORDER BY [last_name] ASC LIMIT 3)' '(SELECT [email], [first_name] AS [firstName], [last_name] AS [lastName] FROM [User] WHERE [User].[companyId] = 5 ORDER BY [last_name] ASC'+sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })+')'
].join(' UNION ALL ') ].join(current.dialect.supports['UNION ALL'] ?' UNION ALL ' : ' UNION ')
+') AS [User];' +') AS [User];'
}); });
...@@ -132,11 +132,11 @@ suite(Support.getTestDialectTeaser('SQL'), function() { ...@@ -132,11 +132,11 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
] ]
} }
}, { }, {
default: 'SELECT * FROM ('+ default: 'SELECT [user].*, [POSTS].[id] AS [POSTS.id], [POSTS].[title] AS [POSTS.title] FROM ('+
[ [
'(SELECT [id_user] AS [id], [email], [first_name] AS [firstName], [last_name] AS [lastName] FROM [users] WHERE [users].[companyId] = 1 ORDER BY [last_name] ASC LIMIT 3)', '(SELECT [id_user] AS [id], [email], [first_name] AS [firstName], [last_name] AS [lastName] FROM [users] AS [user] WHERE [user].[companyId] = 1 ORDER BY [user].[last_name] ASC'+sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })+')',
'(SELECT [id_user] AS [id], [email], [first_name] AS [firstName], [last_name] AS [lastName] FROM [users] WHERE [users].[companyId] = 5 ORDER BY [last_name] ASC LIMIT 3)' '(SELECT [id_user] AS [id], [email], [first_name] AS [firstName], [last_name] AS [lastName] FROM [users] AS [user] WHERE [user].[companyId] = 5 ORDER BY [user].[last_name] ASC'+sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })+')'
].join(' UNION ALL ') ].join(current.dialect.supports['UNION ALL'] ?' UNION ALL ' : ' UNION ')
+') AS [user] LEFT OUTER JOIN [post] AS [POSTS] ON [user].[id] = [POSTS].[user_id];' +') AS [user] LEFT OUTER JOIN [post] AS [POSTS] ON [user].[id] = [POSTS].[user_id];'
}); });
})(); })();
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!