mirror of
				https://github.com/strapi/strapi.git
				synced 2025-11-03 19:36:20 +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,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);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@ -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