mirror of
https://github.com/strapi/strapi.git
synced 2025-11-30 09:01:16 +00:00
Fix query when populating morph relations (#8548)
* change query when populating morph relations Signed-off-by: Pierre Noël <petersg83@gmail.com> * refacto Signed-off-by: Pierre Noël <petersg83@gmail.com> * refacto Signed-off-by: Pierre Noël <petersg83@gmail.com> * refacto Signed-off-by: Pierre Noël <petersg83@gmail.com>
This commit is contained in:
parent
ff5307e8b6
commit
60bf24ce8b
@ -475,19 +475,6 @@ Let's stay with our `Image` model which might belong to **a single `Article` or
|
|||||||
**NOTE**:
|
**NOTE**:
|
||||||
In other words, it means that an `Image` entry can be associated to one entry. This entry can be a `Article` or `Product` entry.
|
In other words, it means that an `Image` entry can be associated to one entry. This entry can be a `Article` or `Product` entry.
|
||||||
|
|
||||||
**Path —** `./api/image/models/Image.settings.json`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"attributes": {
|
|
||||||
"related": {
|
|
||||||
"model": "*",
|
|
||||||
"filter": "field"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Also our `Image` model might belong to **many `Article` or `Product` entries**.
|
Also our `Image` model might belong to **many `Article` or `Product` entries**.
|
||||||
|
|
||||||
**NOTE**:
|
**NOTE**:
|
||||||
@ -574,69 +561,6 @@ An `Image` model might belong to many `Article` models or `Product` models.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Database implementation
|
|
||||||
|
|
||||||
If you're using MongoDB for your database, you don't need to do anything. Everything is natively handled by Strapi. However, to implement a polymorphic relationship with SQL databases, you need to create two tables.
|
|
||||||
|
|
||||||
**Path —** `./api/image/models/Image.settings.json`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"attributes": {
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"related": {
|
|
||||||
"collection": "*",
|
|
||||||
"filter": "field"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The first table to create is the table which has the same name as your model.
|
|
||||||
|
|
||||||
```
|
|
||||||
CREATE TABLE `image` (
|
|
||||||
`id` int(11) NOT NULL,
|
|
||||||
`name` text NOT NULL,
|
|
||||||
`text` text NOT NULL
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
**NOTE**:
|
|
||||||
If you've overridden the default table name given by Strapi by using the `collectionName` attribute. Use the value set in the `collectionName` to name the table.
|
|
||||||
|
|
||||||
The second table will allow us to associate one or many others entries to the `Image` model. The name of the table is the same as the previous one with the suffix `_morph`.
|
|
||||||
|
|
||||||
```
|
|
||||||
CREATE TABLE `image_morph` (
|
|
||||||
`id` int(11) NOT NULL,
|
|
||||||
`image_id` int(11) NOT NULL,
|
|
||||||
`related_id` int(11) NOT NULL,
|
|
||||||
`related_type` text NOT NULL,
|
|
||||||
`field` text NOT NULL
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
- `image_id` is using the name of the first table with the suffix `_id`.
|
|
||||||
- **Attempted value:** It corresponds to the id of an `Image` entry.
|
|
||||||
- `related_id` is using the attribute name where the relation happens with the suffix `_id`.
|
|
||||||
- **Attempted value:** It corresponds to the id of an `Article` or `Product` entry.
|
|
||||||
- `related_type` is using the attribute name where the relation happens with the suffix `_type`.
|
|
||||||
- **Attempted value:** It corresponds to the table name where the `Article` or `Product` entry is stored.
|
|
||||||
- `field` is using the filter property value defined in the model. If you change the filter value, you have to change the name of this column as well.
|
|
||||||
- **Attempted value:** It corresponds to the attribute of an `Article`, `Product` with which the `Image` entry is related.
|
|
||||||
|
|
||||||
| id | image_id | related_id | related_type | field |
|
|
||||||
| --- | -------- | ---------- | ------------ | ------ |
|
|
||||||
| 1 | 1738 | 39 | product | cover |
|
|
||||||
| 2 | 4738 | 58 | article | avatar |
|
|
||||||
| 3 | 1738 | 71 | article | avatar |
|
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
::::
|
::::
|
||||||
|
|||||||
@ -41,9 +41,7 @@ const createComponentModels = async ({ model, definition, ORM, GLOBALS }) => {
|
|||||||
component() {
|
component() {
|
||||||
return this.morphTo(
|
return this.morphTo(
|
||||||
'component',
|
'component',
|
||||||
...relatedComponents.map(component => {
|
...relatedComponents.map(component => GLOBALS[component.globalId])
|
||||||
return GLOBALS[component.globalId];
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -36,6 +36,27 @@ const getDatabaseName = connection => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isARelatedField = (morphAttrInfo, attr) => {
|
||||||
|
const samePlugin =
|
||||||
|
morphAttrInfo.plugin === attr.plugin || (_.isNil(morphAttrInfo.plugin) && _.isNil(attr.plugin));
|
||||||
|
const sameModel = [attr.model, attr.collection].includes(morphAttrInfo.model);
|
||||||
|
const isMorph = attr.via === morphAttrInfo.name;
|
||||||
|
|
||||||
|
return isMorph && sameModel && samePlugin;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRelatedFieldsOfMorphModel = morphAttrInfo => morphModel => {
|
||||||
|
const relatedFields = _.reduce(
|
||||||
|
morphModel.attributes,
|
||||||
|
(fields, attr, attrName) => {
|
||||||
|
return isARelatedField(morphAttrInfo, attr) ? fields.concat(attrName) : fields;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { collectionName: morphModel.collectionName, relatedFields };
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = async ({ models, target }, ctx, { selfFinalize = false } = {}) => {
|
module.exports = async ({ models, target }, ctx, { selfFinalize = false } = {}) => {
|
||||||
const { GLOBALS, connection, ORM } = ctx;
|
const { GLOBALS, connection, ORM } = ctx;
|
||||||
|
|
||||||
@ -354,40 +375,16 @@ module.exports = async ({ models, target }, ctx, { selfFinalize = false } = {})
|
|||||||
}
|
}
|
||||||
case 'belongsToMorph':
|
case 'belongsToMorph':
|
||||||
case 'belongsToManyMorph': {
|
case 'belongsToManyMorph': {
|
||||||
const association = definition.associations.find(
|
const association = _.find(definition.associations, { alias: name });
|
||||||
association => association.alias === name
|
const morphAttrInfo = {
|
||||||
|
plugin: definition.plugin,
|
||||||
|
model: definition.modelName,
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
const morphModelsAndFields = association.related.map(
|
||||||
|
getRelatedFieldsOfMorphModel(morphAttrInfo)
|
||||||
);
|
);
|
||||||
|
|
||||||
const morphValues = association.related.map(id => {
|
|
||||||
let models = Object.values(strapi.models).filter(model => model.globalId === id);
|
|
||||||
|
|
||||||
if (models.length === 0) {
|
|
||||||
models = Object.values(strapi.components).filter(model => model.globalId === id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (models.length === 0) {
|
|
||||||
models = Object.keys(strapi.plugins).reduce((acc, current) => {
|
|
||||||
const models = Object.values(strapi.plugins[current].models).filter(
|
|
||||||
model => model.globalId === id
|
|
||||||
);
|
|
||||||
|
|
||||||
if (acc.length === 0 && models.length > 0) {
|
|
||||||
acc = models;
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (models.length === 0) {
|
|
||||||
strapi.log.error(`Impossible to register the '${model}' model.`);
|
|
||||||
strapi.log.error('The collection name cannot be found for the morphTo method.');
|
|
||||||
strapi.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
return models[0].collectionName;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define new model.
|
// Define new model.
|
||||||
const options = {
|
const options = {
|
||||||
requireFetch: false,
|
requireFetch: false,
|
||||||
@ -401,7 +398,7 @@ module.exports = async ({ models, target }, ctx, { selfFinalize = false } = {})
|
|||||||
related: function() {
|
related: function() {
|
||||||
return this.morphTo(
|
return this.morphTo(
|
||||||
name,
|
name,
|
||||||
...association.related.map((id, index) => [GLOBALS[id], morphValues[index]])
|
...association.related.map(morphModel => [morphModel, morphModel.collectionName])
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -413,12 +410,31 @@ module.exports = async ({ models, target }, ctx, { selfFinalize = false } = {})
|
|||||||
|
|
||||||
// Hack Bookshelf to create a many-to-many polymorphic association.
|
// Hack Bookshelf to create a many-to-many polymorphic association.
|
||||||
// Upload has many Upload_morph that morph to different model.
|
// Upload has many Upload_morph that morph to different model.
|
||||||
|
const populateFn = qb => {
|
||||||
|
qb.where(qb => {
|
||||||
|
for (const modelAndFields of morphModelsAndFields) {
|
||||||
|
qb.orWhere(qb => {
|
||||||
|
qb.where({ related_type: modelAndFields.collectionName }).whereIn(
|
||||||
|
'field',
|
||||||
|
modelAndFields.relatedFields
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
loadedModel[name] = function() {
|
loadedModel[name] = function() {
|
||||||
if (verbose === 'belongsToMorph') {
|
if (verbose === 'belongsToMorph') {
|
||||||
return this.hasOne(GLOBALS[options.tableName], `${definition.collectionName}_id`);
|
return this.hasOne(
|
||||||
|
GLOBALS[options.tableName],
|
||||||
|
`${definition.collectionName}_id`
|
||||||
|
).query(populateFn);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.hasMany(GLOBALS[options.tableName], `${definition.collectionName}_id`);
|
return this.hasMany(
|
||||||
|
GLOBALS[options.tableName],
|
||||||
|
`${definition.collectionName}_id`
|
||||||
|
).query(populateFn);
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -632,28 +648,33 @@ module.exports = async ({ models, target }, ctx, { selfFinalize = false } = {})
|
|||||||
target[model].privateAttributes = contentTypesUtils.getPrivateAttributes(target[model]);
|
target[model].privateAttributes = contentTypesUtils.getPrivateAttributes(target[model]);
|
||||||
|
|
||||||
return async () => {
|
return async () => {
|
||||||
await buildDatabaseSchema({
|
try {
|
||||||
ORM,
|
await buildDatabaseSchema({
|
||||||
definition,
|
ORM,
|
||||||
loadedModel,
|
definition,
|
||||||
connection,
|
loadedModel,
|
||||||
model: target[model],
|
connection,
|
||||||
});
|
model: target[model],
|
||||||
|
});
|
||||||
|
|
||||||
await createComponentJoinTables({ definition, ORM });
|
await createComponentJoinTables({ definition, ORM });
|
||||||
|
} catch (err) {
|
||||||
|
if (['ER_TOO_LONG_IDENT'].includes(err.code)) {
|
||||||
|
strapi.stopWithError(
|
||||||
|
err,
|
||||||
|
`A table name is too long. If it is the name of a join table automatically generated by Strapi, you can customise it by adding \`collectionName: "customName"\` in the corresponding model's attribute.
|
||||||
|
When this happens on a manyToMany relation, make sure to set this parameter on the dominant side of the relation (e.g: where \`dominant: true\` is set)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
strapi.stopWithError(err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof TypeError || err instanceof ReferenceError) {
|
if (err instanceof TypeError || err instanceof ReferenceError) {
|
||||||
strapi.stopWithError(err, `Impossible to register the '${model}' model.`);
|
strapi.stopWithError(err, `Impossible to register the '${model}' model.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['ER_TOO_LONG_IDENT'].includes(err.code)) {
|
|
||||||
strapi.stopWithError(
|
|
||||||
err,
|
|
||||||
`A table name is too long. If it is the name of a join table automatically generated by Strapi, you can customise it by adding \`collectionName: "customName"\` in the corresponding model's attribute.
|
|
||||||
When this happens on a manyToMany relation, make sure to set this parameter on the dominant side of the relation (e.g: where \`dominant: true\` is set)`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
strapi.stopWithError(err);
|
strapi.stopWithError(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -424,7 +424,7 @@ module.exports = {
|
|||||||
const attr = strapi.plugins[current].models[entity].attributes[attribute];
|
const attr = strapi.plugins[current].models[entity].attributes[attribute];
|
||||||
|
|
||||||
if ((attr.collection || attr.model || '').toLowerCase() === model.toLowerCase()) {
|
if ((attr.collection || attr.model || '').toLowerCase() === model.toLowerCase()) {
|
||||||
acc.push(strapi.plugins[current].models[entity].globalId);
|
acc.push(strapi.plugins[current].models[entity]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -437,7 +437,7 @@ module.exports = {
|
|||||||
const attr = strapi.models[entity].attributes[attribute];
|
const attr = strapi.models[entity].attributes[attribute];
|
||||||
|
|
||||||
if ((attr.collection || attr.model || '').toLowerCase() === model.toLowerCase()) {
|
if ((attr.collection || attr.model || '').toLowerCase() === model.toLowerCase()) {
|
||||||
acc.push(strapi.models[entity].globalId);
|
acc.push(strapi.models[entity]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -449,14 +449,14 @@ module.exports = {
|
|||||||
const attr = strapi.components[entity].attributes[attribute];
|
const attr = strapi.components[entity].attributes[attribute];
|
||||||
|
|
||||||
if ((attr.collection || attr.model || '').toLowerCase() === model.toLowerCase()) {
|
if ((attr.collection || attr.model || '').toLowerCase() === model.toLowerCase()) {
|
||||||
acc.push(strapi.components[entity].globalId);
|
acc.push(strapi.components[entity]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const models = _.uniq(appModels.concat(pluginsModels).concat(componentModels));
|
const models = _.uniqWith(appModels.concat(pluginsModels, componentModels), _.isEqual);
|
||||||
|
|
||||||
definition.associations.push({
|
definition.associations.push({
|
||||||
alias: key,
|
alias: key,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user