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

Commit f767ed06 by Mick Hansen

feat(has-many): basic include.seperate support for hasMany (no limit/order support)

1 parent a009a1de
......@@ -1310,7 +1310,9 @@ var QueryGenerator = {
}
if (include.include) {
include.include.forEach(function(childInclude) {
include.include.filter(function (include) {
return !include.seperate;
}).forEach(function(childInclude) {
if (childInclude._pseudo) return;
var childJoinQueries = generateJoinQueries(childInclude, as);
......@@ -1328,7 +1330,9 @@ var QueryGenerator = {
};
// Loop through includes and generate subqueries
options.include.forEach(function(include) {
options.include.filter(function (include) {
return !include.seperate;
}).forEach(function(include) {
var joinQueries = generateJoinQueries(include, options.tableAs);
subJoinQueries = subJoinQueries.concat(joinQueries.subQuery);
......
......@@ -3,6 +3,7 @@
var Utils = require('./utils')
, Instance = require('./instance')
, Association = require('./associations/base')
, HasMany = require('./associations/has-many')
, DataTypes = require('./data-types')
, Util = require('util')
, Transaction = require('./transaction')
......@@ -583,6 +584,18 @@ validateIncludedElement = function(include, tableNames, options) {
include.where = include.where ? { $and: [include.where, include.association.scope] }: include.association.scope;
}
if (include.limit && include.seperate === undefined) {
include.seperate = true;
}
if (include.seperate === true && !(include.association instanceof HasMany)) {
throw new Error('Only HasMany associations support include.seperate');
}
if (include.seperate === true) {
include.duplicating = false;
}
// Validate child includes
if (include.hasOwnProperty('include')) {
validateIncludedElements.call(include.model, include, tableNames, options);
......@@ -1202,9 +1215,11 @@ Model.prototype.findAll = function(options) {
if (arguments.length > 1) {
throw new Error('Please note that find* was refactored and uses only one options object from now on.');
}
var tableNames = {};
var tableNames = {}
, originalOptions;
tableNames[this.getTableName(options)] = true;
originalOptions = optClone(options);
options = optClone(options);
_.defaults(options, { hooks: true });
......@@ -1228,6 +1243,7 @@ Model.prototype.findAll = function(options) {
options.hasJoin = true;
validateIncludedElements.call(this, options, tableNames);
validateIncludedElements.call(this, originalOptions, tableNames);
// If we're not raw, we have to make sure we include the primary key for deduplication
if (options.attributes && !options.raw) {
......@@ -1259,9 +1275,48 @@ Model.prototype.findAll = function(options) {
if (options.hooks) {
return this.runHooks('afterFind', results, options);
}
}).then(function (results) {
return Model.$findSeperate(results, originalOptions);
});
};
Model.$findSeperate = function(results, options) {
if (!options.include) return Promise.resolve(results);
var original = results;
if (!Array.isArray(results)) results = [results];
return Promise.map(options.include, function (include) {
if (!include.seperate) {
return Model.$findSeperate(
results.reduce(function (memo, result) {
var associations = result.get(include.association.as);
if (!Array.isArray(associations)) associations = [associations];
return memo.concat(associations);
}, []),
_.assign(
{},
_.omit(options, 'include', 'attributes', 'order', 'where', 'limit'),
{include: include.include || []}
)
);
}
return include.association.get(results, _.assign(
{},
_.omit(options, 'include', 'attributes', 'order', 'where', 'limit'),
include
)).then(function (map) {
results.forEach(function (result) {
result.set(
include.association.as,
map[result.get(include.association.source.primaryKeyAttribute)]
);
});
});
}).return(original);
};
/**
* Search for a single instance by its primary key. This applies LIMIT 1, so the listener will always be called with a single instance.
*
......
'use strict';
/* jshint -W030 */
var chai = require('chai')
, expect = chai.expect
, sinon = require('sinon')
, Support = require(__dirname + '/../support')
, Sequelize = require(__dirname + '/../../../index')
, Promise = Sequelize.Promise;
describe(Support.getTestDialectTeaser('Include'), function() {
describe('seperate', function () {
it('should run a hasMany association in a seperate 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, seperate: true}
],
order: [
['id', 'ASC']
],
logging: sqlSpy
});
}).then(function (users) {
expect(users[0].get('tasks')).to.be.ok;
expect(users[0].get('tasks').length).to.equal(3);
expect(users[1].get('tasks')).to.be.ok;
expect(users[1].get('tasks').length).to.equal(1);
expect(sqlSpy).to.have.been.calledTwice;
});
});
});
it('should run a nested (from a non-seperate include) hasMany association in a seperate query', function () {
var User = this.sequelize.define('User', {})
, Company = this.sequelize.define('Company')
, Task = this.sequelize.define('Task', {})
, sqlSpy = sinon.spy();
User.Company = User.belongsTo(Company, {as: 'company'});
Company.Tasks = Company.hasMany(Task, {as: 'tasks'});
return this.sequelize.sync({force: true}).then(function () {
return Promise.join(
User.create({
id: 1,
company: {
tasks: [
{},
{},
{}
]
}
}, {
include: [
{association: User.Company, include: [Company.Tasks]}
]
}),
User.create({
id: 2,
company: {
tasks: [
{}
]
}
}, {
include: [
{association: User.Company, include: [Company.Tasks]}
]
})
).then(function () {
return User.findAll({
include: [
{association: User.Company, include: [
{association: Company.Tasks, seperate: true}
]}
],
order: [
['id', 'ASC']
],
logging: sqlSpy
});
}).then(function (users) {
expect(users[0].get('company').get('tasks')).to.be.ok;
expect(users[0].get('company').get('tasks').length).to.equal(3);
expect(users[1].get('company').get('tasks')).to.be.ok;
expect(users[1].get('company').get('tasks').length).to.equal(1);
expect(sqlSpy).to.have.been.calledTwice;
});
});
});
it('should run two nested hasMany association in a seperate queries', function () {
var User = this.sequelize.define('User', {})
, Project = this.sequelize.define('Project', {})
, Task = this.sequelize.define('Task', {})
, sqlSpy = sinon.spy();
User.Projects = User.hasMany(Project, {as: 'projects'});
Project.Tasks = Project.hasMany(Task, {as: 'tasks'});
return this.sequelize.sync({force: true}).then(function () {
return Promise.join(
User.create({
id: 1,
projects: [
{
id: 1,
tasks: [
{},
{},
{}
]
},
{
id: 2,
tasks: [
{}
]
}
]
}, {
include: [
{association: User.Projects, include: [Project.Tasks]}
]
}),
User.create({
id: 2,
projects: [
{
id: 3,
tasks: [
{},
{}
]
}
]
}, {
include: [
{association: User.Projects, include: [Project.Tasks]}
]
})
).then(function () {
return User.findAll({
include: [
{association: User.Projects, seperate: true, include: [
{association: Project.Tasks, seperate: true}
]}
],
order: [
['id', 'ASC']
],
logging: sqlSpy
});
}).then(function (users) {
expect(users[0].get('projects')).to.be.ok;
expect(users[0].get('projects')[0].get('tasks')).to.be.ok;
expect(users[0].get('projects')[1].get('tasks')).to.be.ok;
expect(users[0].get('projects').length).to.equal(2);
expect(users[0].get('projects')[0].get('tasks').length).to.equal(3);
expect(users[0].get('projects')[1].get('tasks').length).to.equal(1);
expect(users[1].get('projects')).to.be.ok;
expect(users[1].get('projects')[0].get('tasks')).to.be.ok;
expect(users[1].get('projects').length).to.equal(1);
expect(users[1].get('projects')[0].get('tasks').length).to.equal(2);
expect(sqlSpy).to.have.been.calledThrice;
});
});
});
});
});
\ No newline at end of file
......@@ -4,26 +4,38 @@
var Support = require(__dirname + '/../support')
, DataTypes = require(__dirname + '/../../../lib/data-types')
, Model = require(__dirname + '/../../../lib/model')
, util = require('util')
, expectsql = Support.expectsql
, current = Support.sequelize
, sql = current.dialect.QueryGenerator;
// Notice: [] will be replaced by dialect specific tick/quote character when there is not dialect specific expectation but only a default expectation
describe(Support.getTestDialectTeaser('SQL'), function() {
describe('select', function () {
it('*', function () {
expectsql(sql.selectQuery('User'), {
default: 'SELECT * FROM [User];'
});
});
suite(Support.getTestDialectTeaser('SQL'), function() {
suite('select', function () {
var testsql = function (options, expectation) {
var model = options.model;
it('with attributes', function () {
expectsql(sql.selectQuery('User', {
attributes: ['name', 'age']
}), {
default: 'SELECT [name], [age] FROM [User];'
test(util.inspect(options, {depth: 2}), function () {
return expectsql(
sql.selectQuery(
options.table || option.model && option.model.getTableName(),
options,
options.model
),
expectation
);
});
};
testsql({
table: 'User',
attributes: [
'email',
['first_name', 'firstName']
]
}, {
default: 'SELECT [email], [first_name] AS [firstName] FROM [User];'
});
it('include (left outer join)', function () {
......@@ -59,22 +71,22 @@ describe(Support.getTestDialectTeaser('SQL'), function() {
});
});
describe('queryIdentifiersFalse', function () {
before(function () {
suite('queryIdentifiersFalse', function () {
suiteSetup(function () {
sql.options.quoteIdentifiers = false;
});
after(function () {
suiteTeardown(function () {
sql.options.quoteIdentifiers = true;
});
it('*', function () {
test('*', function () {
expectsql(sql.selectQuery('User'), {
default: 'SELECT * FROM [User];',
postgres: 'SELECT * FROM User;'
});
});
it('with attributes', function () {
test('with attributes', function () {
expectsql(sql.selectQuery('User', {
attributes: ['name', 'age']
}), {
......@@ -83,7 +95,7 @@ describe(Support.getTestDialectTeaser('SQL'), function() {
});
});
it('include (left outer join)', function () {
test('include (left outer join)', function () {
var User = Support.sequelize.define('User', {
name: DataTypes.STRING,
age: DataTypes.INTEGER
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!