Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ManyToMany relation - v3 workaround for ER_CANT_UPDATE_USED_TABLE_IN_SF_OR_TRG mysql error #11

Merged
merged 2 commits into from
Aug 10, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 90 additions & 1 deletion lib/relations/manyToMany/ManyToManyModifyMixin.js
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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);

Expand All @@ -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;

Expand Down Expand Up @@ -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);

Expand All @@ -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,
Expand Down