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:
Pierre Noël 2020-11-06 14:35:36 +01:00 committed by GitHub
parent ff5307e8b6
commit 60bf24ce8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 76 additions and 133 deletions

View File

@ -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 |
:::
::::

View File

@ -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])
);
},
});

View File

@ -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,6 +648,7 @@ module.exports = async ({ models, target }, ctx, { selfFinalize = false } = {})
target[model].privateAttributes = contentTypesUtils.getPrivateAttributes(target[model]);
return async () => {
try {
await buildDatabaseSchema({
ORM,
definition,
@ -641,19 +658,23 @@ module.exports = async ({ models, target }, ctx, { selfFinalize = false } = {})
});
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);
}
};

View File

@ -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,