Skip to content

Commit 9d68b0a

Browse files
authored
Merge pull request #15231 from Automattic/vkarpov15/gh-15210
Introduce populate ordered option for populating in series rather than in parallel
2 parents ce1f8ad + 5d2a5a4 commit 9d68b0a

File tree

5 files changed

+85
-11
lines changed

5 files changed

+85
-11
lines changed

lib/document.js

+3
Original file line numberDiff line numberDiff line change
@@ -4509,6 +4509,8 @@ Document.prototype.equals = function(doc) {
45094509
* @param {Object|Function} [options.match=null] Add an additional filter to the populate query. Can be a filter object containing [MongoDB query syntax](https://www.mongodb.com/docs/manual/tutorial/query-documents/), or a function that returns a filter object.
45104510
* @param {Function} [options.transform=null] Function that Mongoose will call on every populated document that allows you to transform the populated document.
45114511
* @param {Object} [options.options=null] Additional options like `limit` and `lean`.
4512+
* @param {Boolean} [options.forceRepopulate=true] Set to `false` to prevent Mongoose from repopulating paths that are already populated
4513+
* @param {Boolean} [options.ordered=false] Set to `true` to execute any populate queries one at a time, as opposed to in parallel. We recommend setting this option to `true` if using transactions, especially if also populating multiple paths or paths with multiple models. MongoDB server does **not** support multiple operations in parallel on a single transaction.
45124514
* @param {Function} [callback] Callback
45134515
* @see population https://mongoosejs.com/docs/populate.html
45144516
* @see Query#select https://mongoosejs.com/docs/api/query.html#Query.prototype.select()
@@ -4535,6 +4537,7 @@ Document.prototype.populate = async function populate() {
45354537
}
45364538

45374539
const paths = utils.object.vals(pop);
4540+
45384541
let topLevelModel = this.constructor;
45394542
if (this.$__isNested) {
45404543
topLevelModel = this.$__[scopeSymbol].constructor;

lib/model.js

+29-8
Original file line numberDiff line numberDiff line change
@@ -4369,6 +4369,7 @@ Model.validate = async function validate(obj, pathsOrOptions, context) {
43694369
* @param {Object} [options.options=null] Additional options like `limit` and `lean`.
43704370
* @param {Function} [options.transform=null] Function that Mongoose will call on every populated document that allows you to transform the populated document.
43714371
* @param {Boolean} [options.forceRepopulate=true] Set to `false` to prevent Mongoose from repopulating paths that are already populated
4372+
* @param {Boolean} [options.ordered=false] Set to `true` to execute any populate queries one at a time, as opposed to in parallel. Set this option to `true` if populating multiple paths or paths with multiple models in transactions.
43724373
* @return {Promise}
43734374
* @api public
43744375
*/
@@ -4386,11 +4387,21 @@ Model.populate = async function populate(docs, paths) {
43864387
}
43874388

43884389
// each path has its own query options and must be executed separately
4389-
const promises = [];
4390-
for (const path of paths) {
4391-
promises.push(_populatePath(this, docs, path));
4390+
if (paths.find(p => p.ordered)) {
4391+
// Populate in series, primarily for transactions because MongoDB doesn't support multiple operations on
4392+
// one transaction in parallel.
4393+
// Note that if _any_ path has `ordered`, we make the top-level populate `ordered` as well.
4394+
for (const path of paths) {
4395+
await _populatePath(this, docs, path);
4396+
}
4397+
} else {
4398+
// By default, populate in parallel
4399+
const promises = [];
4400+
for (const path of paths) {
4401+
promises.push(_populatePath(this, docs, path));
4402+
}
4403+
await Promise.all(promises);
43924404
}
4393-
await Promise.all(promises);
43944405

43954406
return docs;
43964407
};
@@ -4510,12 +4521,22 @@ async function _populatePath(model, docs, populateOptions) {
45104521
return;
45114522
}
45124523

4513-
const promises = [];
4514-
for (const arr of params) {
4515-
promises.push(_execPopulateQuery.apply(null, arr).then(valsFromDb => { vals = vals.concat(valsFromDb); }));
4524+
if (populateOptions.ordered) {
4525+
// Populate in series, primarily for transactions because MongoDB doesn't support multiple operations on
4526+
// one transaction in parallel.
4527+
for (const arr of params) {
4528+
await _execPopulateQuery.apply(null, arr).then(valsFromDb => { vals = vals.concat(valsFromDb); });
4529+
}
4530+
} else {
4531+
// By default, populate in parallel
4532+
const promises = [];
4533+
for (const arr of params) {
4534+
promises.push(_execPopulateQuery.apply(null, arr).then(valsFromDb => { vals = vals.concat(valsFromDb); }));
4535+
}
4536+
4537+
await Promise.all(promises);
45164538
}
45174539

4518-
await Promise.all(promises);
45194540

45204541
for (const arr of params) {
45214542
const mod = arr[0];

lib/utils.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -551,8 +551,8 @@ exports.populate = function populate(path, select, model, match, options, subPop
551551
};
552552
}
553553

554-
if (typeof obj.path !== 'string') {
555-
throw new TypeError('utils.populate: invalid path. Expected string. Got typeof `' + typeof path + '`');
554+
if (typeof obj.path !== 'string' && !(Array.isArray(obj.path) && obj.path.every(el => typeof el === 'string'))) {
555+
throw new TypeError('utils.populate: invalid path. Expected string or array of strings. Got typeof `' + typeof path + '`');
556556
}
557557

558558
return _populateObj(obj);
@@ -600,7 +600,11 @@ function _populateObj(obj) {
600600
}
601601

602602
const ret = [];
603-
const paths = oneSpaceRE.test(obj.path) ? obj.path.split(manySpaceRE) : [obj.path];
603+
const paths = oneSpaceRE.test(obj.path)
604+
? obj.path.split(manySpaceRE)
605+
: Array.isArray(obj.path)
606+
? obj.path
607+
: [obj.path];
604608
if (obj.options != null) {
605609
obj.options = clone(obj.options);
606610
}

test/document.populate.test.js

+40
Original file line numberDiff line numberDiff line change
@@ -1075,4 +1075,44 @@ describe('document.populate', function() {
10751075
assert.deepStrictEqual(codeUser.extras[0].config.paymentConfiguration.paymentMethods[0]._id, code._id);
10761076
assert.strictEqual(codeUser.extras[0].config.paymentConfiguration.paymentMethods[0].code, 'test code');
10771077
});
1078+
1079+
it('supports populate with ordered option (gh-15231)', async function() {
1080+
const docSchema = new Schema({
1081+
refA: { type: Schema.Types.ObjectId, ref: 'Test1' },
1082+
refB: { type: Schema.Types.ObjectId, ref: 'Test2' },
1083+
refC: { type: Schema.Types.ObjectId, ref: 'Test3' }
1084+
});
1085+
1086+
const doc1Schema = new Schema({ name: String });
1087+
const doc2Schema = new Schema({ title: String });
1088+
const doc3Schema = new Schema({ content: String });
1089+
1090+
const Doc = db.model('Test', docSchema);
1091+
const Doc1 = db.model('Test1', doc1Schema);
1092+
const Doc2 = db.model('Test2', doc2Schema);
1093+
const Doc3 = db.model('Test3', doc3Schema);
1094+
1095+
const doc1 = await Doc1.create({ name: 'test 1' });
1096+
const doc2 = await Doc2.create({ title: 'test 2' });
1097+
const doc3 = await Doc3.create({ content: 'test 3' });
1098+
1099+
const docD = await Doc.create({
1100+
refA: doc1._id,
1101+
refB: doc2._id,
1102+
refC: doc3._id
1103+
});
1104+
1105+
await docD.populate({
1106+
path: ['refA', 'refB', 'refC'],
1107+
ordered: true
1108+
});
1109+
1110+
assert.ok(docD.populated('refA'));
1111+
assert.ok(docD.populated('refB'));
1112+
assert.ok(docD.populated('refC'));
1113+
1114+
assert.equal(docD.refA.name, 'test 1');
1115+
assert.equal(docD.refB.title, 'test 2');
1116+
assert.equal(docD.refC.content, 'test 3');
1117+
});
10781118
});

types/populate.d.ts

+6
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ declare module 'mongoose' {
3939
foreignField?: string;
4040
/** Set to `false` to prevent Mongoose from repopulating paths that are already populated */
4141
forceRepopulate?: boolean;
42+
/**
43+
* Set to `true` to execute any populate queries one at a time, as opposed to in parallel.
44+
* We recommend setting this option to `true` if using transactions, especially if also populating multiple paths or paths with multiple models.
45+
* MongoDB server does **not** support multiple operations in parallel on a single transaction.
46+
*/
47+
ordered?: boolean;
4248
}
4349

4450
interface PopulateOption {

0 commit comments

Comments
 (0)