|
|
@@ -0,0 +1,82 @@
|
|
|
+package com.payment.platform.module.system.role.mapper;
|
|
|
+
|
|
|
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
|
|
|
+import org.apache.ibatis.annotations.Insert;
|
|
|
+import org.apache.ibatis.mapping.SqlSource;
|
|
|
+import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver;
|
|
|
+import org.apache.ibatis.session.Configuration;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+
|
|
|
+import java.lang.reflect.Method;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 钉住 RoleMapper 里两条**批量插入**注解生成出来的 SQL 形状。
|
|
|
+ *
|
|
|
+ * <p>【为什么必须单独钉】{@code RoleServiceTest} 把 RoleMapper 整个 mock 掉 ⇒
|
|
|
+ * 这些注解里的 {@code <foreach>} 一次都没被 MyBatis 真正求值过,生成的 SQL 长什么样
|
|
|
+ * 在测试里完全不可见。线上实测(2026-09-28 11:48,{@code /system/role/permission/setting}):
|
|
|
+ * 内层 {@code <foreach>} 少了 {@code separator=','},一个角色配 28 个菜单生成的是
|
|
|
+ * {@code INSERT INTO sys_role_menus (role_id, menu_id) VALUES (?, ?) (?, ?) ...}
|
|
|
+ * —— 元组之间没有逗号 —— MyBatis-Plus 的租户行拦截器拿 JSQLParser 解这条 SQL 时抛
|
|
|
+ * {@code MybatisPlusException: Failed to process, Error SQL: ...},整个保存 500。
|
|
|
+ *
|
|
|
+ * <p>【量具一】不手抄注解字符串:走 MyBatis 自己的注解动态 SQL 解析
|
|
|
+ * ({@link XMLLanguageDriver}),求值出来的就是线上那条 BoundSql。
|
|
|
+ * 归一化只做两件事:空白折叠、逗号两侧留一个空格 —— 逗号**有没有**必须由断言管,
|
|
|
+ * 逗号前的空格是 MyBatis foreach 拼串的产物,不是被测对象。
|
|
|
+ *
|
|
|
+ * <p>【量具二】再拿 {@link CCJSqlParserUtil} 解一遍。这不是我自制的规则:MyBatis-Plus 的
|
|
|
+ * {@code JsqlParserSupport} 走的就是这支解析器,线上那个异常的出处正是它。
|
|
|
+ */
|
|
|
+class RoleMapperSqlTest {
|
|
|
+
|
|
|
+ /** 取注解里的 script,交给 MyBatis 真求值,返回空白归一的最终 SQL。 */
|
|
|
+ private static String boundSql(String method, Map<String, Object> params) {
|
|
|
+ Method m = Arrays.stream(RoleMapper.class.getDeclaredMethods())
|
|
|
+ .filter(x -> x.getName().equals(method))
|
|
|
+ .findFirst()
|
|
|
+ .orElseThrow(() -> new AssertionError("RoleMapper 里没有方法 " + method));
|
|
|
+ Insert insert = m.getAnnotation(Insert.class);
|
|
|
+ if (insert == null) throw new AssertionError(method + " 上不是 @Insert");
|
|
|
+ SqlSource sqlSource = new XMLLanguageDriver()
|
|
|
+ .createSqlSource(new Configuration(), insert.value()[0], Object.class);
|
|
|
+ return sqlSource.getBoundSql(params).getSql()
|
|
|
+ .replaceAll("\\s+", " ")
|
|
|
+ .replaceAll("\\s*,\\s*", ", ")
|
|
|
+ .trim();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void assertUsableMultiRowInsert(String expected, String sql) {
|
|
|
+ assertEquals(expected, sql);
|
|
|
+ assertDoesNotThrow(() -> CCJSqlParserUtil.parse(sql),
|
|
|
+ "MyBatis-Plus 租户拦截器解析不了这条 SQL(线上就是这里炸的): " + sql);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void insertRoleMenus_一个角色三个菜单_元组之间必须有逗号() {
|
|
|
+ assertUsableMultiRowInsert(
|
|
|
+ "INSERT INTO sys_role_menus (role_id, menu_id) VALUES (?, ?), (?, ?), (?, ?)",
|
|
|
+ boundSql("insertRoleMenus", Map.of("roleIds", List.of(1L), "menuIds", List.of(11L, 12L, 13L))));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void insertRoleDepts_一个角色两个部门_元组之间必须有逗号() {
|
|
|
+ assertUsableMultiRowInsert(
|
|
|
+ "INSERT INTO sys_role_depts (role_id, dept_id) VALUES (?, ?), (?, ?)",
|
|
|
+ boundSql("insertRoleDepts", Map.of("roleIds", List.of(1L), "deptIds", List.of(7L, 8L))));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 多个角色时,角色块之间由外层 foreach 的 separator 补逗号;这里钉住它没补成两个。 */
|
|
|
+ @Test
|
|
|
+ void insertRoleMenus_两个角色两个菜单_四个元组三个逗号() {
|
|
|
+ assertUsableMultiRowInsert(
|
|
|
+ "INSERT INTO sys_role_menus (role_id, menu_id) VALUES (?, ?), (?, ?), (?, ?), (?, ?)",
|
|
|
+ boundSql("insertRoleMenus", Map.of("roleIds", List.of(1L, 2L), "menuIds", List.of(11L, 12L))));
|
|
|
+ }
|
|
|
+}
|