{"record":{"id":"c0b42300a5e390f7","repo":"Tencent/APIJSON","slug":"join-e-getkey-k-app-join","errorCode":null,"errorMessage":"join:${e.getKey()} 中 ${k} 不合法！@ APP JOIN 必须有且只有一个引用赋值键值对！","messagePattern":"join:(.+?) 中 (.+?) 不合法！@ APP JOIN 必须有且只有一个引用赋值键值对！","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"APIJSONORM/src/main/java/apijson/orm/AbstractParser.java","lineNumber":1735,"sourceCode":"\t\t\t\tmatchSingle = matchSingle == false && k.equals(key);\n\t\t\t\tif (matchSingle) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (k.length() > 1 && k.indexOf(\"@\") == k.length() - 1 && v instanceof String) {\n\t\t\t\t\tString sv = (String) v;\n\t\t\t\t\tint ind = sv.endsWith(\"@\") ? -1 : sv.indexOf(\"/\");\n\t\t\t\t\tif (ind == 0 && key == null) {  // 指定了某个就只允许一个 ON 条件\n\t\t\t\t\t\tString p = sv.substring(1);\n\t\t\t\t\t\tint ind2 = p.indexOf(\"/\");\n\t\t\t\t\t\tString tk = ind2 < 0 ? null : p.substring(0, ind2);\n\n\t\t\t\t\t\tapijson.orm.Entry<String, String> te = tk == null || p.substring(ind2 + 1).indexOf(\"/\") >= 0 ? null : Pair.parseEntry(tk, true);\n\n\t\t\t\t\t\tif (te != null && isTableKey(te.getKey()) && request.get(tk) instanceof Map<?, ?>) {\n\t\t\t\t\t\t\tif (isAppJoin) {\n\t\t\t\t\t\t\t\tif (refObj.size() >= 1) {\n\t\t\t\t\t\t\t\t\tthrow new IllegalArgumentException(apijson.JSONRequest.KEY_JOIN + \":\" + e.getKey() + \" 中 \" + k + \" 不合法！\"\n\t\t\t\t\t\t\t\t\t\t\t+ \"@ APP JOIN 必须有且只有一个引用赋值键值对！\");\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tif (StringUtil.isName(k.substring(0, k.length() - 1)) == false) {\n\t\t\t\t\t\t\t\t\tthrow new IllegalArgumentException(apijson.JSONRequest.KEY_JOIN + \":'\" + e.getKey() + \"' 中 \" + k + \" 不合法 ！\" +\n\t\t\t\t\t\t\t\t\t\t\t\"@ APP JOIN 只允许 key@:/Table/refKey 这种 = 等价连接！\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\trefObj.put(k, v);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tObject rv = getValueByPath(sv);\n\t\t\t\t\tif (rv != null && rv.equals(sv) == false) {\n\t\t\t\t\t\trequestObj.put(k.substring(0, k.length() - 1), rv);\n\t\t\t\t\t\tcontinue;","sourceCodeStart":1717,"sourceCodeEnd":1753,"githubUrl":"https://github.com/Tencent/APIJSON/blob/5284052872898eddc449a58f629e5c8d588b8e22/APIJSONORM/src/main/java/apijson/orm/AbstractParser.java#L1717-L1753","documentation":"APP JOIN requires exactly one reference-assignment key-value pair ('key@': '/Table/refKey') as its ON condition. While collecting ON candidates from the joined table object, if refObj already holds one entry and another candidate 'k' appears, this IllegalArgumentException is thrown — a second ON condition is not allowed for '@' joins.","triggerScenarios":"{ 'join': '@/Comment', 'Comment': { 'toId@': '/User/id', 'momentId@': '/Moment/id' } } — two key@ entries both pointing at same-layer tables; APP JOIN executes in memory and cannot combine two ON conditions.","commonSituations":"Porting a multi-condition SQL JOIN ON (a.id = b.x AND a.oid = b.y) directly to APP JOIN; adding an extra reference-assignment key later without noticing the join is '@' type.","solutions":["Keep only one key@ entry in the joined table object for '@' joins and delete the rest","Switch to a SQL JOIN type ('</Table', '&/Table', '|/Table') which supports multiple ON conditions via key@ pairs","Express the second condition as a normal WHERE-style key on the joined table instead of a reference assignment"],"exampleFix":"// before\n{ 'join': '@/Comment', 'Comment': { 'toId@': '/User/id', 'momentId@': '/Moment/id' } }\n// after\n{ 'join': '</Comment/momentId@', 'Comment': { 'toId@': '/User/id', 'momentId@': '/Moment/id' } }","handlingStrategy":"validation","validationCode":"function checkAppJoinSingleOn(req, joinValue) {\n  const paths = typeof joinValue === 'string' ? [joinValue] : Object.keys(joinValue).filter(p => p.startsWith('@/'));\n  for (const p of paths) {\n    const rest = p.substring(p.indexOf('/') + 1);\n    const parts = rest.split('/');\n    const tbl = req[parts[0].split(':')[0]];\n    if (tbl) {\n      const refs = Object.keys(tbl).filter(k => k.endsWith('@'));\n      if (refs.length > 1) return `APP JOIN ${p}: found ${refs.length} key@ pairs (${refs.join(', ')}), max 1`;\n    }\n  }\n  return null;\n}","typeGuard":null,"tryCatchPattern":"Catch IllegalArgumentException with '必须有且只有一个引用赋值键值对'; downgrade to SQL join type or drop extra key@ pairs.","preventionTips":["For '@' joins, keep exactly one key@ in the joined object","Need multiple ON conditions? Use '<', '&', '|' join types instead"],"tags":["apijson","join","app-join","on-condition","validation"],"backgroundTag":null,"analyzedSha":"5284052872898eddc449a58f629e5c8d588b8e22","analyzedAt":"2026-08-14T15:15:29.577Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}