mirror of
https://github.com/strapi/strapi.git
synced 2025-11-27 23:54:18 +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**:
|
||||
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**.
|
||||
|
||||
**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() {
|
||||
return this.morphTo(
|
||||
'component',
|
||||
...relatedComponents.map(component => {
|
||||
return GLOBALS[component.globalId];
|
||||
})
|
||||
...relatedComponents.map(component => 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 } = {}) => {
|
||||
const { GLOBALS, connection, ORM } = ctx;
|
||||
|
||||
@ -354,40 +375,16 @@ module.exports = async ({ models, target }, ctx, { selfFinalize = false } = {})
|
||||
}
|
||||
case 'belongsToMorph':
|
||||
case 'belongsToManyMorph': {
|
||||
const association = definition.associations.find(
|
||||
association => association.alias === name
|
||||
const association = _.find(definition.associations, { 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.
|
||||
const options = {
|
||||
requireFetch: false,
|
||||
@ -401,7 +398,7 @@ module.exports = async ({ models, target }, ctx, { selfFinalize = false } = {})
|
||||
related: function() {
|
||||
return this.morphTo(
|
||||
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.
|
||||
// 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() {
|
||||
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;
|
||||
}
|
||||
@ -632,28 +648,33 @@ module.exports = async ({ models, target }, ctx, { selfFinalize = false } = {})
|
||||
target[model].privateAttributes = contentTypesUtils.getPrivateAttributes(target[model]);
|
||||
|
||||
return async () => {
|
||||
await buildDatabaseSchema({
|
||||
ORM,
|
||||
definition,
|
||||
loadedModel,
|
||||
connection,
|
||||
model: target[model],
|
||||
});
|
||||
try {
|
||||
await buildDatabaseSchema({
|
||||
ORM,
|
||||
definition,
|
||||
loadedModel,
|
||||
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) {
|
||||
if (err instanceof TypeError || err instanceof ReferenceError) {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
@ -424,7 +424,7 @@ module.exports = {
|
||||
const attr = strapi.plugins[current].models[entity].attributes[attribute];
|
||||
|
||||
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];
|
||||
|
||||
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];
|
||||
|
||||
if ((attr.collection || attr.model || '').toLowerCase() === model.toLowerCase()) {
|
||||
acc.push(strapi.components[entity].globalId);
|
||||
acc.push(strapi.components[entity]);
|
||||
}
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const models = _.uniq(appModels.concat(pluginsModels).concat(componentModels));
|
||||
const models = _.uniqWith(appModels.concat(pluginsModels, componentModels), _.isEqual);
|
||||
|
||||
definition.associations.push({
|
||||
alias: key,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user