diff --git a/lib/relations/manyToMany/ManyToManyModifyMixin.js b/lib/relations/manyToMany/ManyToManyModifyMixin.js index 93f741d93..c835d6777 100644 --- a/lib/relations/manyToMany/ManyToManyModifyMixin.js +++ b/lib/relations/manyToMany/ManyToManyModifyMixin.js @@ -1,6 +1,10 @@ 'use strict'; const { ManyToManyFindOperation } = require('./find/ManyToManyFindOperation'); +const { isMySql } = require('../../utils/knexUtils'); + +const FindByIdSelector = /^findByIds?$/; +const RelateUnrelateSelector = /relate$/; // This mixin contains the shared code for all modify operations (update, delete, relate, unrelate) // for ManyToManyRelation operations. @@ -26,7 +30,7 @@ const ManyToManyModifyMixin = (Operation) => { onBuild(builder) { this.modifyFilterSubquery = this.createModifyFilterSubquery(builder); - if (this.modifyMainQuery) { + if (this.modifyMainQuery && this.modifyFilterSubquery) { // We can now remove the where and join statements from the main query. this.removeFiltersFromMainQuery(builder); @@ -38,6 +42,21 @@ const ManyToManyModifyMixin = (Operation) => { } createModifyFilterSubquery(builder) { + // Check if the subquery is needed (it may be not if there are no operations other than findById(s) on the main query) + // and only if passed builder belongs to joinTableModelClass + if (builder.modelClass() === this.relation.joinTableModelClass) { + const checkQuery = builder + .clone() + .toFindQuery() + .modify(this.relation.modify) + .clear(RelateUnrelateSelector) + .clear(FindByIdSelector) + .clearOrder(); + if (checkQuery.isSelectAll()) { + return null; + } + } + const relatedModelClass = this.relation.relatedModelClass; const builderClass = builder.constructor; @@ -71,6 +90,12 @@ const ManyToManyModifyMixin = (Operation) => { } applyModifyFilterForJoinTable(builder) { + const builderWithTriggerFix = this.applyManyToManyRelationTriggerFix(builder); + // null here means fix is not applicable + if (builderWithTriggerFix !== null) { + return builderWithTriggerFix; + } + const joinTableOwnerRefs = this.relation.joinTableOwnerProp.refs(builder); const joinTableRelatedRefs = this.relation.joinTableRelatedProp.refs(builder); @@ -84,6 +109,70 @@ const ManyToManyModifyMixin = (Operation) => { .whereInComposite(joinTableOwnerRefs, ownerValues); } + /** + * Workaround for ER_CANT_UPDATE_USED_TABLE_IN_SF_OR_TRG mysql error + * when a trigger on join table was operating on the related table + * - targeting mysql only + * - we return null if this fix is not applicable! + * - filters/modify on join table of m2m relations + * - if subquery is not needed at all (e.g. a query with just a findById(s) operation - usually coming from graph upsert) - skip it + * - otherwise extract a subquery reading related ids to separate query run before the delete query for m2m unrelate operation + * + * This is an upgraded (to objection v3) version of: + * - https://github.com/ovos/objection.js/pull/3 + * - https://github.com/ovos/objection.js/pull/1 + * Originally based on: + * - https://github.com/ovos/objection.js/pull/2 + */ + applyManyToManyRelationTriggerFix(builder) { + // this workaround is only needed for MySQL + if (!isMySql(builder.knex())) { + return null; + } + + if (this.modifyFilterSubquery && builder.isFind()) { + return null; + } + + const joinTableOwnerRefs = this.relation.joinTableOwnerProp.refs(builder); + const joinTableRelatedRefs = this.relation.joinTableRelatedProp.refs(builder); + const ownerValues = this.owner.getProps(this.relation); + + if (this.modifyFilterSubquery) { + // if subquery is used (in a non-find query): + // extract the subquery selecting related ids to separate query run before the main query + // to avoid ER_CANT_UPDATE_USED_TABLE_IN_SF_OR_TRG mysql error + // when executing a db trigger on a join table which updates related table + const relatedRefs = this.relation.relatedProp.refs(builder); + const subquery = this.modifyFilterSubquery.clone().select(relatedRefs); + + builder + .runBefore(() => subquery.execute()) + .runBefore((related, builder) => { + if (!related.length) { + builder.resolve([]); + return; + } + builder.whereInComposite( + joinTableRelatedRefs, + related.map((m) => m.$values(this.relation.relatedProp.props)) + ); + }); + } else if (builder.parentQuery()) { + // if subquery is not used: + // rewrite findById(s) from related table to join table + builder.parentQuery().forEachOperation(FindByIdSelector, (op) => { + if (op.name === 'findByIds') { + builder.whereInComposite(joinTableRelatedRefs, op.ids); + } else { + builder.whereComposite(joinTableRelatedRefs, op.id); + } + }); + } + + return builder.whereInComposite(joinTableOwnerRefs, ownerValues); + } + toFindOperation() { return new ManyToManyFindOperation('find', { relation: this.relation,