springboot启动包和mybatis整合springboot的组件包版本不兼容导致的sql异常

问题描述

在自定义的Mapper映射器中,in查询使用${}映射字段值时发生异常:

Error querying database.  Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '{specValueIdIn}))

控制台打印出来的sql语句如下:

SELECT * FROM tb_spec_value WHERE (`spec_value_id` in (${specValueIdIn}) AND `spec_id` = ?) ORDER BY `store_id` DESC 

由此可见,in查询的参数值未被正确解析到sql中。

以下是自定义Mapper映射器的部分代码

public interface BaseMapper<I, E> {

   /**
    * 根据实体条件查询符合条件的实体list
    *
    * @param criteria 条件实体
    * @return list
    */
   @SelectProvider(type = SelectByCriteriaSqlProvider.class, method = "sql")
   List<I> selectByCriteria(E criteria);


   /**
    * 基类
    */
   abstract class BaseSqlProviderSupport {
       /**
        * key -> mapper class   value -> tableInfo
        */
       private static final Map<Class<?>, TableInfo> tableCache = new ConcurrentHashMap<>(128);

       /**
        * 获取表信息结构
        * @param context provider context
        * @return 表基本信息
        */
       protected TableInfo tableInfo(ProviderContext context) {
           // 如果不存在则创建
           return tableCache.computeIfAbsent(context.getMapperType(), TableInfo::of);
       }
   }


   /**
    * 根据条件查询
    */
   class SelectByCriteriaSqlProvider extends BaseSqlProviderSupport {
       /**
        * sql
        * @param criteria entity 条件
        * @param context  context
        * @return sql
        */
       public String sql(Object criteria, ProviderContext context) {
           TableInfo table = tableInfo(context);

           return new SQL()
                   .SELECT(table.selectColumns)
                   .FROM(table.tableName)
                   .WHERE(Stream.of(table.fields)
                           .filter(field -> ReflectUtils.getFilterFieldValue(field, criteria) != null)
                           .map(TableInfo::assignParameter)
                           .toArray(String[]::new));
       }


   }

   class TableInfo {
       /**
        * 表前缀
        */
       private static final String TABLE_PREFIX = "tb_";

       /**
        * 主键名
        */
       private static final String DEFAULT_PRIMARY_KEY = "id";

       /**
        * 表名
        */
       private String tableName;

       /**
        * 所有列名
        */
       private String[] columns;

       /**
        * 所有select sql的列名,有带下划线的将其转为aa_bb AS aaBb
        */
       private String[] selectColumns;

       private TableInfo() {
       }

       /**
        * 获取TableInfo的简单工厂
        * @param mapperType mapper类型
        * @return {@link TableInfo}
        */
       public static TableInfo of(Class<?> mapperType) {
           Class<?> entityClass = entityType(mapperType);
           Class<?> entityTableClass = entityTableType(mapperType);
           Field[] tableFields = excludeNoColumnField(entityTableClass);
           TableInfo tableInfo = new TableInfo();
           tableInfo.tableName = tableName(entityTableClass);
           tableInfo.columns = columns(tableFields);
           tableInfo.selectColumns = selectColumns(tableFields);
           return tableInfo;
       }

       /**
        * 获取BaseMapper接口中的泛型类型
        * @param mapperType mapper类型
        * @return 实体类型
        */
       public static Class<?> entityType(Class<?> mapperType) {
           return Stream.of(mapperType.getGenericInterfaces())
                   .filter(ParameterizedType.class::isInstance)
                   .map(ParameterizedType.class::cast)
                   .filter(type -> type.getRawType() == BaseMapper.class)
                   .findFirst()
                   .map(type -> type.getActualTypeArguments()[1])
                   .filter(Class.class::isInstance).map(Class.class::cast)
                   .orElseThrow(() -> new IllegalStateException("未找到BaseMapper的泛型类 " + mapperType.getName() + "."));
       }

       public static Class<?> entityTableType(Class<?> mapperType) {
           return Stream.of(mapperType.getGenericInterfaces())
                   .filter(ParameterizedType.class::isInstance)
                   .map(ParameterizedType.class::cast)
                   .filter(type -> type.getRawType() == BaseMapper.class)
                   .findFirst()
                   .map(type -> type.getActualTypeArguments()[0])
                   .filter(Class.class::isInstance).map(Class.class::cast)
                   .orElseThrow(() -> new IllegalStateException("未找到BaseMapper的泛型类 " + mapperType.getName() + "."));
       }


       /**
        * 获取表名
        * @param entityType 实体类型
        * @return 表名
        */
       public static String tableName(Class<?> entityType) {
           Table table = entityType.getAnnotation(Table.class);
           return table == null ? TABLE_PREFIX + StringUtils.camel2Underscore(entityType.getSimpleName()) : table.value();
       }

       /**
        * 过滤含有@NoColumn注解或者是静态的field
        * @param entityClass 实体类型
        * @return 不包含@NoColumn注解的fields
        */
       public static Field[] excludeNoColumnField(Class<?> entityClass) {
           Field[] allFields = ReflectUtils.getFields(entityClass);
           List<String> excludeColumns = getClassExcludeColumns(entityClass);
           return Stream.of(allFields)
                   //过滤掉类上指定的@NoCloumn注解的字段和字段上@NoColumn注解或者是静态的field
                   .filter(field -> !CollectionUtils.contains(excludeColumns, field.getName())
                           && (!field.isAnnotationPresent(NoColumn.class) && !Modifier.isStatic(field.getModifiers())))
                   .toArray(Field[]::new);
       }

       /**
        * 获取查询对应的字段 (不包含pojo中含有@NoColumn主键的属性)
        * @param fields p
        * @return 所有需要查询的查询字段
        */
       public static String[] selectColumns(Field[] fields) {
           return Stream.of(fields).map(TableInfo::selectColumnName).toArray(String[]::new);
       }

       /**
        * 获取所有pojo所有属性对应的数据库字段 (不包含pojo中含有@NoColumn主键的属性)
        * @param fields entityClass所有fields
        * @return 所有的column名称
        */
       public static String[] columns(Field[] fields) {
           return Stream.of(fields).map(TableInfo::columnName).toArray(String[]::new);
       }

       public static String primaryKey(Field[] fields) {
           return Stream.of(fields).filter(field -> field.isAnnotationPresent(PrimaryKey.class))
                   .findFirst()    //返回第一个primaryKey的field
                   .map(TableInfo::objName)
                   .orElse(DEFAULT_PRIMARY_KEY);
       }

       /**
        * 获取单个属性对应的数据库字段(带有下划线字段将其转换为"字段 AS pojo属性名"形式)
        * @param field 字段
        * @return 带有下划线字段将其转换为"字段 AS pojo属性名"形式
        */
       public static String selectColumnName(Field field) {
           String camel = columnName(field);
           return camel.contains("_") ? camel + " AS `" + field.getName() + "`" : camel;
       }

       /**
        * 获取单个属性对应的数据库字段(数据库字段)
        * @param field entityClass中的field
        * @return 字段对应的column
        */
       public static String columnName(Field field) {
           return "`" + StringUtils.camel2Underscore(field.getName()) + "`";
       }

       /**
        * 获取单个属性对应的数据库字段(对象字段)
        * @param field entityClass中的field
        * @return 字段对应的column
        */
       public static String objName(Field field) {
           return field.getName();
       }

       /**
        * 绑定参数
        * @param field 字段
        * @return 参数格式
        */
       public static String bindParameter(Field field) {
           return "#{" + field.getName() + "}";
       }

       /**
        * 绑定参数
        * @param field 字段
        * @return 参数格式
        */
       public static String bind$Parameter(Field field) {
           return "${" + field.getName() + "}";
       }

       /**
        * 获取该字段的参数赋值语句,如 user_name = #{userName}
        * @param field 字段
        * @return 参数赋值语句
        */
       private static String assignParameter(Field field) {
           String keyName = field.getName();
           String result = "";
           if (keyName.contains("Like")) {
               result = columnName(field).replace("_like", "") + " like concat('%', " + bindParameter(field) + ",'%')";
           } else if (matchesExtension(keyName, "In")) {
               result = columnName(field).replace("_in", "") + " in (" + bind$Parameter(field)+ ")";
           } else {
               result = columnName(field) + " = " + bindParameter(field);
           }
           return result;
       }

       private static boolean matchesExtension(String input, String extension) {
           // 构建正则表达式,字段名中出现多个in,只校验末尾为in的字段名
           String regex = ".*" + extension + "$";
           return input.matches(regex);
       }
   }
// 查询部分代码
SpecValueExample specValueExample = new SpecValueExample();
specValueExample.setSpecId(goods.getSpecId());
specValueExample.setSpecValueIdIn("10,12");
List<SpecValue> specValueList = baseMapper.selectByCriteria(specValueExample);

以上,代码没有问题,经过读取源码发现,真正引起异常的是1.3.2版本的 mybatis-spring-boot-starter 包和2.3.5版本的 spring-boot-starter-web 包不兼容导致的:

解决方法

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.3.5</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <!--     使用1.3.*版本会导致版本不兼容,需要升级到2.0以上的版本即可       -->
            <version>2.1.1</version>
        </dependency>

升级mybatis对应的springboot组件包版本后,代码运行正常,sql日志显示如下:

SELECT * FROM tb_spec_value WHERE (`spec_value_id` in (10,12) AND `spec_id` = ?) ORDER BY `store_id` DESC 

结论

通常情况下,mybatis-spring-boot-starter 和 spring-boot-starter-web 之间的冲突可能是由以下原因造成的:

  1. 版本不兼容:确保你使用的 mybatis-spring-boot-starter 和 spring-boot-starter-web 版本相互兼容。如果发现版本不兼容,尝试升级或降级到一个兼容的版本。
  2. 依赖冲突:可能存在多个不同版本的依赖冲突。

另外,为什么上面的in查询不使用#{}的方式,是由于#{}在解析时会自动将字段值映射为一个字符串,也就是会被解析为 in (“10,12”),显而易见,这样每次查询都只会匹配到值为10的数据,导致查询结果不准确,实际上应该是in (“10”,“12”) ,那么只能是选择${}查询,具体也要视情况而定,因为使用#{}查询更安全,防止sql注入的风险。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值