{"record":{"id":"04a076b27b1529b5","repo":"nocobase/nocobase","slug":"hasmany-association-hasmanykey-cannot-be-used-w","errorCode":null,"errorMessage":"HasMany association ${hasManyKey} cannot be used with BelongsToMany association ${association.target.name} with same through model","messagePattern":"HasMany association (.+?) cannot be used with BelongsToMany association (.+?) with same through model","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/core/database/src/update-guard.ts","lineNumber":99,"sourceCode":"      const belongsToManyValueKeys = associationValueKeys.filter((key) => {\n        return associations[key].associationType === 'BelongsToMany';\n      });\n\n      const hasManyValueKeys = associationValueKeys.filter((key) => {\n        return associations[key].associationType === 'HasMany';\n      });\n\n      for (const belongsToManyKey of belongsToManyValueKeys) {\n        const association = associations[belongsToManyKey];\n        const through = association.through.model;\n        belongsToManyThroughNames.push(through.name);\n      }\n\n      for (const hasManyKey of hasManyValueKeys) {\n        const association = associations[hasManyKey];\n        if (belongsToManyThroughNames.includes(association.target.name)) {\n          throw new Error(\n            `HasMany association ${hasManyKey} cannot be used with BelongsToMany association ${association.target.name} with same through model`,\n          );\n        }\n      }\n    };\n\n    dfs(values, this.model);\n  }\n\n  /**\n   * Sanitize values by whitelist blacklist\n   * @param values\n   */\n  sanitize(values: UpdateValues) {\n    if (values === null || values === undefined) {\n      return values;\n    }\n\n    values = lodash.clone(values);","sourceCodeStart":81,"sourceCodeEnd":117,"githubUrl":"https://github.com/nocobase/nocobase/blob/fa42722fefe44265490dff2c27d79e2882bce4fa/packages/core/database/src/update-guard.ts#L81-L117","documentation":"NocoBase's UpdateGuard (checkValues → dfs) rejects update values that simultaneously reference a HasMany relation and a BelongsToMany relation whose target model shares the same through table. Updating both would write conflicting rows into the same join table, producing duplicate/corrupt association data, so the write is blocked up front.","triggerScenarios":"Sending a values payload (repository.update / create with values) on a model that has both e.g. posts.tags (BelongsToMany, through='post_tags') and posts.postTags (HasMany on the through model), and including both keys in the same values object.","commonSituations":"Schema/form collections that auto-generate both association flavors for the same through collection; clients copying full record JSON (including inverse relation keys) back into an update payload.","solutions":["Remove the HasMany-on-through-model key (or the BelongsToMany key) from the values payload — update them one at a time","Fix the collection schema so only one association flavor targets the given through model","Strip reverse/redundant relation keys server-side before passing values to repository.update","If both are needed, use two sequential updates instead of one combined payload"],"exampleFix":"// before\nawait repo.update({ filterByTk: 1, values: { tags: [1,2], postTags: [{ id: 9, tagId: 2 }] } });\n// after\nawait repo.update({ filterByTk: 1, values: { tags: [1,2] } }); // manage through-model via BelongsToMany only","handlingStrategy":"validation","validationCode":"function hasThroughConflict(values, model) {\n  const keys = Object.keys(values);\n  const btmTargets = keys.filter(k => model.associations[k]?.associationType === 'BelongsToMany')\n    .map(k => model.associations[k].target.name);\n  return keys.some(k => model.associations[k]?.associationType === 'HasMany' && btmTargets.includes(model.associations[k].target.name));\n}","typeGuard":"function isSafeUpdateValues(values: Record<string, unknown>, model: any): boolean {\n  return !hasThroughConflict(values, model);\n}","tryCatchPattern":"try {\n  await repo.update(options);\n} catch (e) {\n  if (e.message.includes('cannot be used with BelongsToMany')) {\n    // split into sequential updates without the conflicting HasMany key\n  } else throw e;\n}","preventionTips":["Keep one association flavor per through model in collection schemas","Strip inverse HasMany-on-through keys from client-submitted values","Review generated forms for duplicated relation keys","Add server-side payload sanitization for relation keys"],"tags":["database","associations","belongs-to-many","has-many","through-model-conflict"],"backgroundTag":"ambiguous-through-association","analyzedSha":"fa42722fefe44265490dff2c27d79e2882bce4fa","analyzedAt":"2026-09-01T00:54:31.202Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}