Merge pull request #15643 from strapi/fix/performance-degradation-in-deeply-nested-coimpos

This commit is contained in:
Marc 2023-02-08 11:02:50 +01:00 committed by GitHub
commit 5645d2ad0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 91 additions and 40 deletions

View File

@ -366,7 +366,16 @@ describe('Entity service', () => {
const fakeStrapi = {
getModel: jest.fn((modelName) => fakeModels[modelName]),
query: jest.fn(() => fakeQuery),
db: {
dialect: {
client: 'sqlite',
},
},
};
global.strapi = fakeStrapi;
instance = createEntityService({
strapi: fakeStrapi,
db: fakeDB,

View File

@ -3,10 +3,12 @@
const _ = require('lodash');
const { has, prop, omit, toString, pipe, assign } = require('lodash/fp');
const { contentTypes: contentTypesUtils } = require('@strapi/utils');
const { contentTypes: contentTypesUtils, mapAsync } = require('@strapi/utils');
const { ApplicationError } = require('@strapi/utils').errors;
const { getComponentAttributes } = require('@strapi/utils').contentTypes;
const isDialectMySQL = () => strapi.db.dialect.client === 'mysql';
const omitComponentData = (contentType, data) => {
const { attributes } = contentType;
const componentAttributes = Object.keys(attributes).filter((attributeName) =>
@ -43,10 +45,12 @@ const createComponents = async (uid, data) => {
throw new Error('Expected an array to create repeatable component');
}
const components = [];
for (const value of componentValue) {
components.push(await createComponent(componentUID, value));
}
// MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
const components = await mapAsync(
componentValue,
(value) => createComponent(componentUID, value),
{ concurrency: isDialectMySQL() ? 1 : Infinity }
);
componentBody[attributeName] = components.map(({ id }) => {
return {
@ -78,19 +82,23 @@ const createComponents = async (uid, data) => {
throw new Error('Expected an array to create repeatable component');
}
const dynamicZoneData = [];
for (const value of dynamiczoneValues) {
const createDynamicZoneComponents = async (value) => {
const { id } = await createComponent(value.__component, value);
dynamicZoneData.push({
return {
id,
__component: value.__component,
__pivot: {
field: attributeName,
},
});
}
};
};
componentBody[attributeName] = dynamicZoneData;
// MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
componentBody[attributeName] = await mapAsync(
dynamiczoneValues,
createDynamicZoneComponents,
{ concurrency: isDialectMySQL() ? 1 : Infinity }
);
continue;
}
@ -139,10 +147,12 @@ const updateComponents = async (uid, entityToUpdate, data) => {
throw new Error('Expected an array to create repeatable component');
}
const components = [];
for (const value of componentValue) {
components.push(await updateOrCreateComponent(componentUID, value));
}
// MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
const components = await mapAsync(
componentValue,
(value) => updateOrCreateComponent(componentUID, value),
{ concurrency: isDialectMySQL() ? 1 : Infinity }
);
componentBody[attributeName] = components.filter(_.negate(_.isNil)).map(({ id }) => {
return {
@ -176,19 +186,22 @@ const updateComponents = async (uid, entityToUpdate, data) => {
throw new Error('Expected an array to create repeatable component');
}
const dynamicZoneData = [];
for (const value of dynamiczoneValues) {
const { id } = await updateOrCreateComponent(value.__component, value);
dynamicZoneData.push({
id,
__component: value.__component,
__pivot: {
field: attributeName,
},
});
}
// MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
componentBody[attributeName] = await mapAsync(
dynamiczoneValues,
async (value) => {
const { id } = await updateOrCreateComponent(value.__component, value);
componentBody[attributeName] = dynamicZoneData;
return {
id,
__component: value.__component,
__pivot: {
field: attributeName,
},
};
},
{ concurrency: isDialectMySQL() ? 1 : Infinity }
);
continue;
}
@ -290,14 +303,18 @@ const deleteComponents = async (uid, entityToDelete, { loadComponents = true } =
if (attribute.type === 'component') {
const { component: componentUID } = attribute;
for (const subValue of _.castArray(value)) {
await deleteComponent(componentUID, subValue);
}
// MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
await mapAsync(_.castArray(value), (subValue) => deleteComponent(componentUID, subValue), {
concurrency: isDialectMySQL() ? 1 : Infinity,
});
} else {
// delete dynamic zone components
for (const subValue of _.castArray(value)) {
await deleteComponent(subValue.__component, subValue);
}
// MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
await mapAsync(
_.castArray(value),
(subValue) => deleteComponent(subValue.__component, subValue),
{ concurrency: isDialectMySQL() ? 1 : Infinity }
);
}
continue;

View File

@ -4,3 +4,9 @@ export type MapAsync<T = any, R = any> = lodash.CurriedFunction3<
{ concurrency?: number },
Promise<R[]>
>;
export type ForEachAsync<T = any, R = any> = (
array: T[],
func: (element: T, index: number) => R | Promise<R>,
options?: { concurrency?: number }
) => Promise<R[]>;

View File

@ -20,7 +20,15 @@ function pipeAsync(...methods) {
*/
const mapAsync = curry(pMap);
/**
* @type { import('./async').ForEachAsync }
*/
const forEachAsync = curry(async (array, func, options) => {
await mapAsync(array, func, options);
});
module.exports = {
mapAsync,
forEachAsync,
pipeAsync,
};

View File

@ -37,7 +37,7 @@ const providerFactory = require('./provider-factory');
const pagination = require('./pagination');
const sanitize = require('./sanitize');
const traverseEntity = require('./traverse-entity');
const { pipeAsync, mapAsync } = require('./async');
const { pipeAsync, mapAsync, forEachAsync } = require('./async');
const convertQueryParams = require('./convert-query-params');
const importDefault = require('./import-default');
const template = require('./template');
@ -82,6 +82,7 @@ module.exports = {
pagination,
pipeAsync,
mapAsync,
forEachAsync,
errors,
validateYupSchema,
validateYupSchemaSync,

View File

@ -65,6 +65,11 @@ const setGlobalStrapi = () => {
},
},
},
db: {
dialect: {
client: 'sqlite',
},
},
};
};

View File

@ -2,8 +2,11 @@
const { prop, isNil, isEmpty } = require('lodash/fp');
const { forEachAsync } = require('@strapi/utils');
const { getService } = require('../utils');
const isDialectMySQL = () => strapi.db.dialect.client === 'mysql';
/**
* Adds the default locale to an object if it isn't defined yet
* @param {Object} data a data object before being persisted into db
@ -32,9 +35,10 @@ const syncLocalizations = async (entry, { model }) => {
return strapi.query(model.uid).update({ where: { id }, data: { localizations } });
};
for (const localization of entry.localizations) {
await updateLocalization(localization.id);
}
// MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
await forEachAsync(entry.localizations, (localization) => updateLocalization(localization.id), {
concurrency: isDialectMySQL() ? 1 : Infinity,
});
}
};
@ -58,9 +62,10 @@ const syncNonLocalizedAttributes = async (entry, { model }) => {
return strapi.entityService.update(model.uid, id, { data: nonLocalizedAttributes });
};
for (const localization of entry.localizations) {
await updateLocalization(localization.id);
}
// MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
await forEachAsync(entry.localizations, (localization) => updateLocalization(localization.id), {
concurrency: isDialectMySQL() ? 1 : Infinity,
});
}
};