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

Commit 4f098998 by javiertury Committed by Sushant

feat: support include option in bulkInsert (#11307)

1 parent de06ac3f
...@@ -2532,163 +2532,313 @@ class Model { ...@@ -2532,163 +2532,313 @@ class Model {
return Promise.resolve([]); return Promise.resolve([]);
} }
options = Object.assign({
validate: false,
hooks: true,
individualHooks: false,
ignoreDuplicates: false
}, options);
options.fields = options.fields || Object.keys(this.rawAttributes);
const dialect = this.sequelize.options.dialect; const dialect = this.sequelize.options.dialect;
const now = Utils.now(this.sequelize.options.dialect);
if (options.ignoreDuplicates && ['mssql'].includes(dialect)) { options.model = this;
return Promise.reject(new Error(`${dialect} does not support the ignoreDuplicates option.`));
}
if (options.updateOnDuplicate && (dialect !== 'mysql' && dialect !== 'mariadb' && dialect !== 'postgres')) {
return Promise.reject(new Error(`${dialect} does not support the updateOnDuplicate option.`));
}
if (options.updateOnDuplicate !== undefined) { if (!options.includeValidated) {
if (Array.isArray(options.updateOnDuplicate) && options.updateOnDuplicate.length) { this._conformIncludes(options, this);
options.updateOnDuplicate = _.intersection( if (options.include) {
_.without(Object.keys(this.tableAttributes), this._timestampAttributes.createdAt), this._expandIncludeAll(options);
options.updateOnDuplicate this._validateIncludedElements(options);
);
} else {
return Promise.reject(new Error('updateOnDuplicate option only supports non-empty array.'));
} }
} }
options.model = this; const instances = records.map(values => this.build(values, { isNewRecord: true, include: options.include }));
const createdAtAttr = this._timestampAttributes.createdAt; const recursiveBulkCreate = (instances, options) => {
const updatedAtAttr = this._timestampAttributes.updatedAt; options = Object.assign({
const now = Utils.now(this.sequelize.options.dialect); validate: false,
hooks: true,
individualHooks: false,
ignoreDuplicates: false
}, options);
let instances = records.map(values => this.build(values, { isNewRecord: true })); if (options.returning === undefined) {
if (options.association) {
options.returning = false;
} else {
options.returning = true;
}
}
return Promise.try(() => { if (options.ignoreDuplicates && ['mssql'].includes(dialect)) {
// Run before hook return Promise.reject(new Error(`${dialect} does not support the ignoreDuplicates option.`));
if (options.hooks) {
return this.runHooks('beforeBulkCreate', instances, options);
} }
}).then(() => { if (options.updateOnDuplicate && (dialect !== 'mysql' && dialect !== 'mariadb' && dialect !== 'postgres')) {
// Validate return Promise.reject(new Error(`${dialect} does not support the updateOnDuplicate option.`));
if (options.validate) {
const errors = new Promise.AggregateError();
const validateOptions = _.clone(options);
validateOptions.hooks = options.individualHooks;
return Promise.map(instances, instance =>
instance.validate(validateOptions).catch(err => {
errors.push(new sequelizeErrors.BulkRecordError(err, instance));
})
).then(() => {
delete options.skip;
if (errors.length) {
throw errors;
}
});
} }
}).then(() => {
if (options.individualHooks) { const model = options.model;
// Create each instance individually
return Promise.map(instances, instance => { options.fields = options.fields || Object.keys(model.rawAttributes);
const individualOptions = _.clone(options); const createdAtAttr = model._timestampAttributes.createdAt;
delete individualOptions.fields; const updatedAtAttr = model._timestampAttributes.updatedAt;
delete individualOptions.individualHooks;
delete individualOptions.ignoreDuplicates; if (options.updateOnDuplicate !== undefined) {
individualOptions.validate = false; if (Array.isArray(options.updateOnDuplicate) && options.updateOnDuplicate.length) {
individualOptions.hooks = true; options.updateOnDuplicate = _.intersection(
_.without(Object.keys(model.tableAttributes), createdAtAttr),
return instance.save(individualOptions); options.updateOnDuplicate
}).then(_instances => { );
instances = _instances; } else {
}); return Promise.reject(new Error('updateOnDuplicate option only supports non-empty array.'));
}
} }
// Create all in one query
// Recreate records from instances to represent any changes made in hooks or validation
records = instances.map(instance => {
const values = instance.dataValues;
// set createdAt/updatedAt attributes return Promise.try(() => {
if (createdAtAttr && !values[createdAtAttr]) { // Run before hook
values[createdAtAttr] = now; if (options.hooks) {
if (!options.fields.includes(createdAtAttr)) { return model.runHooks('beforeBulkCreate', instances, options);
options.fields.push(createdAtAttr);
}
} }
if (updatedAtAttr && !values[updatedAtAttr]) { }).then(() => {
values[updatedAtAttr] = now; // Validate
if (!options.fields.includes(updatedAtAttr)) { if (options.validate) {
options.fields.push(updatedAtAttr); const errors = new Promise.AggregateError();
} const validateOptions = _.clone(options);
validateOptions.hooks = options.individualHooks;
return Promise.map(instances, instance =>
instance.validate(validateOptions).catch(err => {
errors.push(new sequelizeErrors.BulkRecordError(err, instance));
})
).then(() => {
delete options.skip;
if (errors.length) {
throw errors;
}
});
}
}).then(() => {
if (options.individualHooks) {
// Create each instance individually
return Promise.map(instances, instance => {
const individualOptions = _.clone(options);
delete individualOptions.fields;
delete individualOptions.individualHooks;
delete individualOptions.ignoreDuplicates;
individualOptions.validate = false;
individualOptions.hooks = true;
return instance.save(individualOptions);
});
} }
instance.dataValues = Utils.mapValueFieldNames(values, options.fields, this); return Promise.resolve().then(() => {
if (!options.include || !options.include.length) return;
const out = Object.assign({}, instance.dataValues); // Nested creation for BelongsTo relations
for (const key of this._virtualAttributes) { return Promise.map(options.include.filter(include => include.association instanceof BelongsTo), include => {
delete out[key]; const associationInstances = [];
} const associationInstanceIndexToInstanceMap = [];
return out;
});
// Map attributes to fields for serial identification for (const instance of instances) {
const fieldMappedAttributes = {}; const associationInstance = instance.get(include.as);
for (const attr in this.tableAttributes) { if (associationInstance) {
fieldMappedAttributes[this.rawAttributes[attr].field || attr] = this.rawAttributes[attr]; associationInstances.push(associationInstance);
} associationInstanceIndexToInstanceMap.push(instance);
}
}
// Map updateOnDuplicate attributes to fields if (!associationInstances.length) {
if (options.updateOnDuplicate) { return;
options.updateOnDuplicate = options.updateOnDuplicate.map(attr => this.rawAttributes[attr].field || attr); }
// Get primary keys for postgres to enable updateOnDuplicate
options.upsertKeys = _.chain(this.primaryKeys).values().map('fieldName').value();
if (Object.keys(this.uniqueKeys).length > 0) {
options.upsertKeys = _.chain(this.uniqueKeys).values().filter(c => c.fields.length === 1).map('column').value();
}
}
// Map returning attributes to fields const includeOptions = _(Utils.cloneDeep(include))
if (options.returning && Array.isArray(options.returning)) { .omit(['association'])
options.returning = options.returning.map(attr => this.rawAttributes[attr].field || attr); .defaults({
} transaction: options.transaction,
logging: options.logging
}).value();
return this.QueryInterface.bulkInsert(this.getTableName(options), records, options, fieldMappedAttributes).then(results => { return recursiveBulkCreate(associationInstances, includeOptions).then(associationInstances => {
if (Array.isArray(results)) { for (const idx in associationInstances) {
results.forEach((result, i) => { const associationInstance = associationInstances[idx];
if (instances[i] && !instances[i].get(this.primaryKeyAttribute)) { const instance = associationInstanceIndexToInstanceMap[idx];
instances[i].dataValues[this.primaryKeyField] = result[this.primaryKeyField];
instance[include.association.accessors.set](associationInstance, { save: false, logging: options.logging });
}
});
});
}).then(() => {
// Create all in one query
// Recreate records from instances to represent any changes made in hooks or validation
records = instances.map(instance => {
const values = instance.dataValues;
// set createdAt/updatedAt attributes
if (createdAtAttr && !values[createdAtAttr]) {
values[createdAtAttr] = now;
if (!options.fields.includes(createdAtAttr)) {
options.fields.push(createdAtAttr);
}
}
if (updatedAtAttr && !values[updatedAtAttr]) {
values[updatedAtAttr] = now;
if (!options.fields.includes(updatedAtAttr)) {
options.fields.push(updatedAtAttr);
}
} }
const out = Object.assign({}, Utils.mapValueFieldNames(values, options.fields, model));
for (const key of model._virtualAttributes) {
delete out[key];
}
return out;
}); });
}
return results; // Map attributes to fields for serial identification
}); const fieldMappedAttributes = {};
}).then(() => { for (const attr in model.tableAttributes) {
// map fields back to attributes fieldMappedAttributes[model.rawAttributes[attr].field || attr] = model.rawAttributes[attr];
instances.forEach(instance => { }
for (const attr in this.rawAttributes) {
if (this.rawAttributes[attr].field && // Map updateOnDuplicate attributes to fields
instance.dataValues[this.rawAttributes[attr].field] !== undefined && if (options.updateOnDuplicate) {
this.rawAttributes[attr].field !== attr options.updateOnDuplicate = options.updateOnDuplicate.map(attr => model.rawAttributes[attr].field || attr);
) { // Get primary keys for postgres to enable updateOnDuplicate
instance.dataValues[attr] = instance.dataValues[this.rawAttributes[attr].field]; options.upsertKeys = _.chain(model.primaryKeys).values().map('fieldName').value();
delete instance.dataValues[this.rawAttributes[attr].field]; if (Object.keys(model.uniqueKeys).length > 0) {
options.upsertKeys = _.chain(model.uniqueKeys).values().filter(c => c.fields.length === 1).map('column').value();
}
}
// Map returning attributes to fields
if (options.returning && Array.isArray(options.returning)) {
options.returning = options.returning.map(attr => model.rawAttributes[attr].field || attr);
} }
instance._previousDataValues[attr] = instance.dataValues[attr];
instance.changed(attr, false); return model.QueryInterface.bulkInsert(model.getTableName(options), records, options, fieldMappedAttributes).then(results => {
if (Array.isArray(results)) {
results.forEach((result, i) => {
const instance = instances[i];
for (const key in result) {
if (!instance || key === model.primaryKeyAttribute &&
instance.get(model.primaryKeyAttribute) &&
['mysql', 'mariadb', 'sqlite'].includes(dialect)) {
// The query.js for these DBs is blind, it autoincrements the
// primarykey value, even if it was set manually. Also, it can
// return more results than instances, bug?.
continue;
}
if (Object.prototype.hasOwnProperty.call(result, key)) {
const record = result[key];
const attr = _.find(model.rawAttributes, attribute => attribute.fieldName === key || attribute.field === key);
instance.dataValues[attr && attr.fieldName || key] = record;
}
}
});
}
return results;
});
});
}).then(() => {
if (!options.include || !options.include.length) return;
// Nested creation for HasOne/HasMany/BelongsToMany relations
return Promise.map(options.include.filter(include => !(include.association instanceof BelongsTo ||
include.parent && include.parent.association instanceof BelongsToMany)), include => {
const associationInstances = [];
const associationInstanceIndexToInstanceMap = [];
for (const instance of instances) {
let associated = instance.get(include.as);
if (!Array.isArray(associated)) associated = [associated];
for (const associationInstance of associated) {
if (associationInstance) {
if (!(include.association instanceof BelongsToMany)) {
associationInstance.set(include.association.foreignKey, instance.get(include.association.sourceKey || instance.constructor.primaryKeyAttribute, { raw: true }), { raw: true });
Object.assign(associationInstance, include.association.scope);
}
associationInstances.push(associationInstance);
associationInstanceIndexToInstanceMap.push(instance);
}
}
}
if (!associationInstances.length) {
return;
}
const includeOptions = _(Utils.cloneDeep(include))
.omit(['association'])
.defaults({
transaction: options.transaction,
logging: options.logging
}).value();
return recursiveBulkCreate(associationInstances, includeOptions).then(associationInstances => {
if (include.association instanceof BelongsToMany) {
const valueSets = [];
for (const idx in associationInstances) {
const associationInstance = associationInstances[idx];
const instance = associationInstanceIndexToInstanceMap[idx];
const values = {};
values[include.association.foreignKey] = instance.get(instance.constructor.primaryKeyAttribute, { raw: true });
values[include.association.otherKey] = associationInstance.get(associationInstance.constructor.primaryKeyAttribute, { raw: true });
// Include values defined in the association
Object.assign(values, include.association.through.scope);
if (associationInstance[include.association.through.model.name]) {
for (const attr of Object.keys(include.association.through.model.rawAttributes)) {
if (include.association.through.model.rawAttributes[attr]._autoGenerated ||
attr === include.association.foreignKey ||
attr === include.association.otherKey ||
typeof associationInstance[include.association.through.model.name][attr] === undefined) {
continue;
}
values[attr] = associationInstance[include.association.through.model.name][attr];
}
}
valueSets.push(values);
}
const throughOptions = _(Utils.cloneDeep(include))
.omit(['association', 'attributes'])
.defaults({
transaction: options.transaction,
logging: options.logging
}).value();
throughOptions.model = include.association.throughModel;
const throughInstances = include.association.throughModel.bulkBuild(valueSets, throughOptions);
return recursiveBulkCreate(throughInstances, throughOptions);
}
});
});
}).then(() => {
// map fields back to attributes
instances.forEach(instance => {
for (const attr in model.rawAttributes) {
if (model.rawAttributes[attr].field &&
instance.dataValues[model.rawAttributes[attr].field] !== undefined &&
model.rawAttributes[attr].field !== attr
) {
instance.dataValues[attr] = instance.dataValues[model.rawAttributes[attr].field];
delete instance.dataValues[model.rawAttributes[attr].field];
}
instance._previousDataValues[attr] = instance.dataValues[attr];
instance.changed(attr, false);
}
instance.isNewRecord = false;
});
// Run after hook
if (options.hooks) {
return model.runHooks('afterBulkCreate', instances, options);
} }
instance.isNewRecord = false; }).then(() => instances);
}); };
// Run after hook return recursiveBulkCreate(instances, options);
if (options.hooks) {
return this.runHooks('afterBulkCreate', instances, options);
}
}).then(() => instances);
} }
/** /**
......
...@@ -152,7 +152,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { ...@@ -152,7 +152,7 @@ describe(Support.getTestDialectTeaser('Model'), () => {
if (dialect === 'postgres') { if (dialect === 'postgres') {
expect(sql).to.include('INSERT INTO "Beers" ("id","style","createdAt","updatedAt") VALUES (DEFAULT'); expect(sql).to.include('INSERT INTO "Beers" ("id","style","createdAt","updatedAt") VALUES (DEFAULT');
} else if (dialect === 'mssql') { } else if (dialect === 'mssql') {
expect(sql).to.include('INSERT INTO [Beers] ([style],[createdAt],[updatedAt]) VALUES'); expect(sql).to.include('INSERT INTO [Beers] ([style],[createdAt],[updatedAt]) ');
} else { // mysql, sqlite } else { // mysql, sqlite
expect(sql).to.include('INSERT INTO `Beers` (`id`,`style`,`createdAt`,`updatedAt`) VALUES (NULL'); expect(sql).to.include('INSERT INTO `Beers` (`id`,`style`,`createdAt`,`updatedAt`) VALUES (NULL');
} }
......
'use strict';
const chai = require('chai'),
Sequelize = require('../../../../index'),
expect = chai.expect,
Support = require('../../support'),
DataTypes = require('../../../../lib/data-types');
describe(Support.getTestDialectTeaser('Model'), () => {
describe('bulkCreate', () => {
describe('include', () => {
it('should bulkCreate data for BelongsTo relations', function() {
const Product = this.sequelize.define('Product', {
title: Sequelize.STRING
}, {
hooks: {
afterBulkCreate(products) {
products.forEach(product => {
product.isIncludeCreatedOnAfterCreate = !!(product.User && product.User.id);
});
}
}
});
const User = this.sequelize.define('User', {
first_name: Sequelize.STRING,
last_name: Sequelize.STRING
}, {
hooks: {
beforeBulkCreate(users, options) {
users.forEach(user => {
user.createOptions = options;
});
}
}
});
Product.belongsTo(User);
return this.sequelize.sync({ force: true }).then(() => {
return Product.bulkCreate([{
title: 'Chair',
User: {
first_name: 'Mick',
last_name: 'Broadstone'
}
}, {
title: 'Table',
User: {
first_name: 'John',
last_name: 'Johnson'
}
}], {
include: [{
model: User,
myOption: 'option'
}]
}).then(savedProducts => {
expect(savedProducts[0].isIncludeCreatedOnAfterCreate).to.be.true;
expect(savedProducts[0].User.createOptions.myOption).to.be.equal('option');
expect(savedProducts[1].isIncludeCreatedOnAfterCreate).to.be.true;
expect(savedProducts[1].User.createOptions.myOption).to.be.equal('option');
return Promise.all([
Product.findOne({
where: { id: savedProducts[0].id },
include: [User]
}),
Product.findOne({
where: { id: savedProducts[1].id },
include: [User]
})
]).then(persistedProducts => {
expect(persistedProducts[0].User).to.be.ok;
expect(persistedProducts[0].User.first_name).to.be.equal('Mick');
expect(persistedProducts[0].User.last_name).to.be.equal('Broadstone');
expect(persistedProducts[1].User).to.be.ok;
expect(persistedProducts[1].User.first_name).to.be.equal('John');
expect(persistedProducts[1].User.last_name).to.be.equal('Johnson');
});
});
});
});
it('should bulkCreate data for BelongsTo relations with no nullable FK', function() {
const Product = this.sequelize.define('Product', {
title: Sequelize.STRING
});
const User = this.sequelize.define('User', {
first_name: Sequelize.STRING
});
Product.belongsTo(User, {
foreignKey: {
allowNull: false
}
});
return this.sequelize.sync({ force: true }).then(() => {
return Product.bulkCreate([{
title: 'Chair',
User: {
first_name: 'Mick'
}
}, {
title: 'Table',
User: {
first_name: 'John'
}
}], {
include: [{
model: User
}]
}).then(savedProducts => {
expect(savedProducts[0]).to.exist;
expect(savedProducts[0].title).to.be.equal('Chair');
expect(savedProducts[0].User).to.exist;
expect(savedProducts[0].User.first_name).to.be.equal('Mick');
expect(savedProducts[1]).to.exist;
expect(savedProducts[1].title).to.be.equal('Table');
expect(savedProducts[1].User).to.exist;
expect(savedProducts[1].User.first_name).to.be.equal('John');
});
});
});
it('should bulkCreate data for BelongsTo relations with alias', function() {
const Product = this.sequelize.define('Product', {
title: Sequelize.STRING
});
const User = this.sequelize.define('User', {
first_name: Sequelize.STRING,
last_name: Sequelize.STRING
});
const Creator = Product.belongsTo(User, { as: 'creator' });
return this.sequelize.sync({ force: true }).then(() => {
return Product.bulkCreate([{
title: 'Chair',
creator: {
first_name: 'Matt',
last_name: 'Hansen'
}
}, {
title: 'Table',
creator: {
first_name: 'John',
last_name: 'Johnson'
}
}], {
include: [Creator]
}).then(savedProducts => {
return Promise.all([
Product.findOne({
where: { id: savedProducts[0].id },
include: [Creator]
}),
Product.findOne({
where: { id: savedProducts[1].id },
include: [Creator]
})
]).then(persistedProducts => {
expect(persistedProducts[0].creator).to.be.ok;
expect(persistedProducts[0].creator.first_name).to.be.equal('Matt');
expect(persistedProducts[0].creator.last_name).to.be.equal('Hansen');
expect(persistedProducts[1].creator).to.be.ok;
expect(persistedProducts[1].creator.first_name).to.be.equal('John');
expect(persistedProducts[1].creator.last_name).to.be.equal('Johnson');
});
});
});
});
it('should bulkCreate data for HasMany relations', function() {
const Product = this.sequelize.define('Product', {
title: Sequelize.STRING
}, {
hooks: {
afterBulkCreate(products) {
products.forEach(product => {
product.areIncludesCreatedOnAfterCreate = product.Tags &&
product.Tags.every(tag => {
return !!tag.id;
});
});
}
}
});
const Tag = this.sequelize.define('Tag', {
name: Sequelize.STRING
}, {
hooks: {
afterBulkCreate(tags, options) {
tags.forEach(tag => tag.createOptions = options);
}
}
});
Product.hasMany(Tag);
return this.sequelize.sync({ force: true }).then(() => {
return Product.bulkCreate([{
id: 1,
title: 'Chair',
Tags: [
{ id: 1, name: 'Alpha' },
{ id: 2, name: 'Beta' }
]
}, {
id: 2,
title: 'Table',
Tags: [
{ id: 3, name: 'Gamma' },
{ id: 4, name: 'Delta' }
]
}], {
include: [{
model: Tag,
myOption: 'option'
}]
}).then(savedProducts => {
expect(savedProducts[0].areIncludesCreatedOnAfterCreate).to.be.true;
expect(savedProducts[0].Tags[0].createOptions.myOption).to.be.equal('option');
expect(savedProducts[0].Tags[1].createOptions.myOption).to.be.equal('option');
expect(savedProducts[1].areIncludesCreatedOnAfterCreate).to.be.true;
expect(savedProducts[1].Tags[0].createOptions.myOption).to.be.equal('option');
expect(savedProducts[1].Tags[1].createOptions.myOption).to.be.equal('option');
return Promise.all([
Product.findOne({
where: { id: savedProducts[0].id },
include: [Tag]
}),
Product.findOne({
where: { id: savedProducts[1].id },
include: [Tag]
})
]).then(persistedProducts => {
expect(persistedProducts[0].Tags).to.be.ok;
expect(persistedProducts[0].Tags.length).to.equal(2);
expect(persistedProducts[1].Tags).to.be.ok;
expect(persistedProducts[1].Tags.length).to.equal(2);
});
});
});
});
it('should bulkCreate data for HasMany relations with alias', function() {
const Product = this.sequelize.define('Product', {
title: Sequelize.STRING
});
const Tag = this.sequelize.define('Tag', {
name: Sequelize.STRING
});
const Categories = Product.hasMany(Tag, { as: 'categories' });
return this.sequelize.sync({ force: true }).then(() => {
return Product.bulkCreate([{
id: 1,
title: 'Chair',
categories: [
{ id: 1, name: 'Alpha' },
{ id: 2, name: 'Beta' }
]
}, {
id: 2,
title: 'Table',
categories: [
{ id: 3, name: 'Gamma' },
{ id: 4, name: 'Delta' }
]
}], {
include: [Categories]
}).then(savedProducts => {
return Promise.all([
Product.findOne({
where: { id: savedProducts[0].id },
include: [Categories]
}),
Product.findOne({
where: { id: savedProducts[1].id },
include: [Categories]
})
]).then(persistedProducts => {
expect(persistedProducts[0].categories).to.be.ok;
expect(persistedProducts[0].categories.length).to.equal(2);
expect(persistedProducts[1].categories).to.be.ok;
expect(persistedProducts[1].categories.length).to.equal(2);
});
});
});
});
it('should bulkCreate data for HasOne relations', function() {
const User = this.sequelize.define('User', {
username: Sequelize.STRING
});
const Task = this.sequelize.define('Task', {
title: Sequelize.STRING
});
User.hasOne(Task);
return this.sequelize.sync({ force: true }).then(() => {
return User.bulkCreate([{
username: 'Muzzy',
Task: {
title: 'Eat Clocks'
}
}, {
username: 'Walker',
Task: {
title: 'Walk'
}
}], {
include: [Task]
}).then(savedUsers => {
return Promise.all([
User.findOne({
where: { id: savedUsers[0].id },
include: [Task]
}),
User.findOne({
where: { id: savedUsers[1].id },
include: [Task]
})
]).then(persistedUsers => {
expect(persistedUsers[0].Task).to.be.ok;
expect(persistedUsers[1].Task).to.be.ok;
});
});
});
});
it('should bulkCreate data for HasOne relations with alias', function() {
const User = this.sequelize.define('User', {
username: Sequelize.STRING
});
const Task = this.sequelize.define('Task', {
title: Sequelize.STRING
});
const Job = User.hasOne(Task, { as: 'job' });
return this.sequelize.sync({ force: true }).then(() => {
return User.bulkCreate([{
username: 'Muzzy',
job: {
title: 'Eat Clocks'
}
}, {
username: 'Walker',
job: {
title: 'Walk'
}
}], {
include: [Job]
}).then(savedUsers => {
return Promise.all([
User.findOne({
where: { id: savedUsers[0].id },
include: [Job]
}),
User.findOne({
where: { id: savedUsers[1].id },
include: [Job]
})
]).then(persistedUsers => {
expect(persistedUsers[0].job).to.be.ok;
expect(persistedUsers[1].job).to.be.ok;
});
});
});
});
it('should bulkCreate data for BelongsToMany relations', function() {
const User = this.sequelize.define('User', {
username: DataTypes.STRING
}, {
hooks: {
afterBulkCreate(users) {
users.forEach(user => {
user.areIncludesCreatedOnAfterCreate = user.Tasks &&
user.Tasks.every(task => {
return !!task.id;
});
});
}
}
});
const Task = this.sequelize.define('Task', {
title: DataTypes.STRING,
active: DataTypes.BOOLEAN
}, {
hooks: {
afterBulkCreate(tasks, options) {
tasks.forEach(task => {
task.createOptions = options;
});
}
}
});
User.belongsToMany(Task, { through: 'user_task' });
Task.belongsToMany(User, { through: 'user_task' });
return this.sequelize.sync({ force: true }).then(() => {
return User.bulkCreate([{
username: 'John',
Tasks: [
{ title: 'Get rich', active: true },
{ title: 'Die trying', active: false }
]
}, {
username: 'Jack',
Tasks: [
{ title: 'Prepare sandwich', active: true },
{ title: 'Each sandwich', active: false }
]
}], {
include: [{
model: Task,
myOption: 'option'
}]
}).then(savedUsers => {
expect(savedUsers[0].areIncludesCreatedOnAfterCreate).to.be.true;
expect(savedUsers[0].Tasks[0].createOptions.myOption).to.be.equal('option');
expect(savedUsers[0].Tasks[1].createOptions.myOption).to.be.equal('option');
expect(savedUsers[1].areIncludesCreatedOnAfterCreate).to.be.true;
expect(savedUsers[1].Tasks[0].createOptions.myOption).to.be.equal('option');
expect(savedUsers[1].Tasks[1].createOptions.myOption).to.be.equal('option');
return Promise.all([
User.findOne({
where: { id: savedUsers[0].id },
include: [Task]
}),
User.findOne({
where: { id: savedUsers[1].id },
include: [Task]
})
]).then(persistedUsers => {
expect(persistedUsers[0].Tasks).to.be.ok;
expect(persistedUsers[0].Tasks.length).to.equal(2);
expect(persistedUsers[1].Tasks).to.be.ok;
expect(persistedUsers[1].Tasks.length).to.equal(2);
});
});
});
});
it('should bulkCreate data for polymorphic BelongsToMany relations', function() {
const Post = this.sequelize.define('Post', {
title: DataTypes.STRING
}, {
tableName: 'posts',
underscored: true
});
const Tag = this.sequelize.define('Tag', {
name: DataTypes.STRING
}, {
tableName: 'tags',
underscored: true
});
const ItemTag = this.sequelize.define('ItemTag', {
tag_id: {
type: DataTypes.INTEGER,
references: {
model: 'tags',
key: 'id'
}
},
taggable_id: {
type: DataTypes.INTEGER,
references: null
},
taggable: {
type: DataTypes.STRING
}
}, {
tableName: 'item_tag',
underscored: true
});
Post.belongsToMany(Tag, {
as: 'tags',
foreignKey: 'taggable_id',
constraints: false,
through: {
model: ItemTag,
scope: {
taggable: 'post'
}
}
});
Tag.belongsToMany(Post, {
as: 'posts',
foreignKey: 'tag_id',
constraints: false,
through: {
model: ItemTag,
scope: {
taggable: 'post'
}
}
});
return this.sequelize.sync({ force: true }).then(() => {
return Post.bulkCreate([{
title: 'Polymorphic Associations',
tags: [
{
name: 'polymorphic'
},
{
name: 'associations'
}
]
}, {
title: 'Second Polymorphic Associations',
tags: [
{
name: 'second polymorphic'
},
{
name: 'second associations'
}
]
}], {
include: [{
model: Tag,
as: 'tags',
through: {
model: ItemTag
}
}]
}
);
}).then(savedPosts => {
// The saved post should include the two tags
expect(savedPosts[0].tags.length).to.equal(2);
expect(savedPosts[1].tags.length).to.equal(2);
// The saved post should be able to retrieve the two tags
// using the convenience accessor methods
return Promise.all([
savedPosts[0].getTags(),
savedPosts[1].getTags()
]);
}).then(savedTagGroups => {
// All nested tags should be returned
expect(savedTagGroups[0].length).to.equal(2);
expect(savedTagGroups[1].length).to.equal(2);
}).then(() => {
return ItemTag.findAll();
}).then(itemTags => {
// Four "through" models should be created
expect(itemTags.length).to.equal(4);
// And their polymorphic field should be correctly set to 'post'
expect(itemTags[0].taggable).to.equal('post');
expect(itemTags[1].taggable).to.equal('post');
expect(itemTags[2].taggable).to.equal('post');
expect(itemTags[3].taggable).to.equal('post');
});
});
it('should bulkCreate data for BelongsToMany relations with alias', function() {
const User = this.sequelize.define('User', {
username: DataTypes.STRING
});
const Task = this.sequelize.define('Task', {
title: DataTypes.STRING,
active: DataTypes.BOOLEAN
});
const Jobs = User.belongsToMany(Task, { through: 'user_job', as: 'jobs' });
Task.belongsToMany(User, { through: 'user_job' });
return this.sequelize.sync({ force: true }).then(() => {
return User.bulkCreate([{
username: 'John',
jobs: [
{ title: 'Get rich', active: true },
{ title: 'Die trying', active: false }
]
}, {
username: 'Jack',
jobs: [
{ title: 'Prepare sandwich', active: true },
{ title: 'Eat sandwich', active: false }
]
}], {
include: [Jobs]
}).then(savedUsers => {
return Promise.all([
User.findOne({
where: { id: savedUsers[0].id },
include: [Jobs]
}),
User.findOne({
where: { id: savedUsers[1].id },
include: [Jobs]
})
]).then(persistedUsers => {
expect(persistedUsers[0].jobs).to.be.ok;
expect(persistedUsers[0].jobs.length).to.equal(2);
expect(persistedUsers[1].jobs).to.be.ok;
expect(persistedUsers[1].jobs.length).to.equal(2);
});
});
});
});
});
});
});
...@@ -741,6 +741,11 @@ export interface BulkCreateOptions extends Logging, Transactionable { ...@@ -741,6 +741,11 @@ export interface BulkCreateOptions extends Logging, Transactionable {
updateOnDuplicate?: string[]; updateOnDuplicate?: string[];
/** /**
* Include options. See `find` for details
*/
include?: Includeable[];
/**
* Return all columns or only the specified columns for the affected rows (only for postgres) * Return all columns or only the specified columns for the affected rows (only for postgres)
*/ */
returning?: boolean | string[]; returning?: boolean | string[];
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!