问题描述
在自定义的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 之间的冲突可能是由以下原因造成的:
- 版本不兼容:确保你使用的 mybatis-spring-boot-starter 和 spring-boot-starter-web 版本相互兼容。如果发现版本不兼容,尝试升级或降级到一个兼容的版本。
- 依赖冲突:可能存在多个不同版本的依赖冲突。
另外,为什么上面的in查询不使用#{}的方式,是由于#{}在解析时会自动将字段值映射为一个字符串,也就是会被解析为 in (“10,12”),显而易见,这样每次查询都只会匹配到值为10的数据,导致查询结果不准确,实际上应该是in (“10”,“12”) ,那么只能是选择${}查询,具体也要视情况而定,因为使用#{}查询更安全,防止sql注入的风险。