如何使用带有iBatis(myBatis)的注释进行IN查询?

我们只想使用MyBatis的注释; 我们真的想避免使用xml。 我们正在尝试使用“IN”子句:

@Select("SELECT * FROM blog WHERE id IN (#{ids})") List selectBlogs(int[] ids); 

MyBatis似乎无法选择整数数组并将其放入生成的查询中。 似乎“软弱地失败”,我们得不到任何结果。

看起来我们可以使用XML映射来实现这一点,但我们真的想避免这种情况。 是否有正确的注释语法?

我相信这是jdbc准备好的陈述的细微差别,而不是MyBatis。 这里有一个链接可以解释这个问题并提供各种解决方案。 遗憾的是,这些解决方案都不适用于您的应用程序,但是,对于理解“IN”子句中预准备语句的限制仍然是一个很好的解读。 可以在特定于DB的方面找到解决方案(可能是次优的)。 例如,在postgresql中,可以使用:

 "SELECT * FROM blog WHERE id=ANY(#{blogIds}::int[])" 

“ANY”与“IN”相同,“:: int []”是将参数类型转换为int数组。 提供给语句的参数应该类似于:

 "{1,2,3,4}" 

我相信答案与这个问题的答案是一致的。 您可以通过执行以下操作在注释中使用myBatis Dynamic SQL:

 @Select({""}) List selectBlogs(@Param("list") int[] ids); 

元素为注释启用动态SQL解析和执行。 它必须是查询字符串的第一个内容。 没有什么必须在它面前,甚至没有白色空间。

请注意,您可以在各种XML脚本标记中使用的变量遵循与常规查询相同的命名约定,因此如果您想使用“param1”,“param2”等名称之外的名称来引用您的方法参数...需要使用@Param注释为每个参数添加前缀。

有一些关于这个主题的研究。

  1. @Select("")的官方解决方案之一是将动态sql放在@Select("") 。 但是,在java注释中编写xml是非常不合适的。 想想这个@Select("")
  2. @SelectProvider工作正常。 但阅读起来有点复杂。
  3. PreparedStatement不允许您设置整数列表。 pstm.setString(index, "1,2,3,4")会让你的SQL像这样select name from sometable where id in ('1,2,3,4') 。 Mysql会将字符'1,2,3,4'转换为数字1
  4. FIND_IN_SET不适用于mysql索引。

查看mybatis动态sql机制,它已由SqlNode.apply(DynamicContext) 。 但是,没有注释的@Select将不会通过DynamicContext传递参数

也可以看看

  • org.apache.ibatis.scripting.xmltags.XMLLanguageDriver
  • org.apache.ibatis.scripting.xmltags.DynamicSqlSource
  • org.apache.ibatis.scripting.xmltags.RawSqlSource

所以,

  • 解决方案1:使用@SelectProvider
  • 解决方案2:扩展LanguageDriver,它总是将sql编译为DynamicSqlSource 。 但是,你还是要写\"到处都是。
  • 解决方案3:扩展LanguageDriver,它可以将您自己的语法转换为mybatis。
  • 解决方案4:编写自己的LanguageDriver,使用一些模板渲染器编译SQL,就像mybatis-velocity项目一样。 通过这种方式,您甚至可以整合groovy。

我的项目采用解决方案3,这是代码:

 public class MybatisExtendedLanguageDriver extends XMLLanguageDriver implements LanguageDriver { private final Pattern inPattern = Pattern.compile("\\(#\\{(\\w+)\\}\\)"); public SqlSource createSqlSource(Configuration configuration, String script, Class parameterType) { Matcher matcher = inPattern.matcher(script); if (matcher.find()) { script = matcher.replaceAll("(#{__item})"); } script = ""; return super.createSqlSource(configuration, script, parameterType); } } 

用法:

 @Lang(MybatisExtendedLanguageDriver.class) @Select("SELECT " + COLUMNS + " FROM sometable where id IN (#{ids})") List loadByIds(@Param("ids") List ids); 

我在我的代码中做了一个小技巧。

 public class MyHandler implements TypeHandler { public void setParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException { Integer[] arrParam = (Integer[]) parameter; String inString = ""; for(Integer element : arrParam){ inString = "," + element; } inString = inString.substring(1); ps.setString(i,inString); } 

我在SqlMapper中使用了这个MyHandler:

  @Select("select id from tmo where id_parent in (#{ids, typeHandler=ru.transsys.test.MyHandler})") public List getSubObjects(@Param("ids") Integer[] ids) throws SQLException; 

它现在工作:)我希望这将有助于某人。

叶夫根尼·

其他选择可以

  public class Test { @SuppressWarnings("unchecked") public static String getTestQuery(Map params) { List idList = (List) params.get("idList"); StringBuilder sql = new StringBuilder(); sql.append("SELECT * FROM blog WHERE id in ("); for (String id : idList) { if (idList.indexOf(id) > 0) sql.append(","); sql.append("'").append(id).append("'"); } sql.append(")"); return sql.toString(); } public interface TestMapper { @SelectProvider(type = Test.class, method = "getTestQuery") List selectBlogs(@Param("idList") int[] ids); } } 

我担心Evgeny的解决方案似乎只能起作用,因为代码示例中有一个小错误:

  inString = "," + element; 

这意味着inString始终只包含一个最后一个数字(而不是一个连接数字列表)。

事实上这应该是

  inString += "," + element; 

唉,如果纠正了这个错误,数据库会开始报告“错误号码”exception,因为mybatis将“1,2,3”设置为字符串参数,数据库只是尝试将此字符串转换为数字:/

另一方面,Mohit所描述的@SelectProvider注释工作正常。 我们必须要注意的是,每次我们在IN子句中使用不同的参数运行查询时它会创建一个新的语句,而不是重用现有的PreparedStatement(因为IN-Clause中的参数在SQL内部被硬编码而不是设置为准备语句的参数)。 这有时会导致数据库中的内存泄漏(因为数据库需要存储越来越多的预处理语句,并且可能无法重用现有的执行计划)。

可以尝试混合@SelectProvider和自定义typeHandler。 这样,可以使用@SelectProvider根据需要在“IN(…)”内创建具有尽可能多的占位符的查询,然后在自定义TypeHandler中替换它们。 不过,它有点棘手。

在我的项目中,我们已经在使用Google Guava,因此快捷方便。

 public class ListTypeHandler implements TypeHandler { @Override public void setParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, Joiner.on(",").join((Collection) parameter)); } } 

在Oracle中,我使用Tom Kyte的tokenizer的变体来处理未知的列表大小(给定Oracle对IN子句的1k限制以及多个IN的恶化以绕过它)。 这是针对varchar2的,但它可以针对数字进行定制(或者您可以依靠Oracle知道’1’= 1 /颤抖)。

假设您传递或执行myBatis咒语以将ids作为String获取,则使用它:

 select @Select("SELECT * FROM blog WHERE id IN (select * from table(string_tokenizer(#{ids}))") 

代码:

 create or replace function string_tokenizer(p_string in varchar2, p_separator in varchar2 := ',') return sys.dbms_debug_vc2coll is return_value SYS.DBMS_DEBUG_VC2COLL; pattern varchar2(250); begin pattern := '[^(''' || p_separator || ''')]+' ; select trim(regexp_substr(p_string, pattern, 1, level)) token bulk collect into return_value from dual where regexp_substr(p_string, pattern, 1, level) is not null connect by regexp_instr(p_string, pattern, 1, level) > 0; return return_value; end string_tokenizer; 

您可以使用自定义类型处理程序来执行此操作。 例如:

 public class InClauseParams extends ArrayList { //... // marker class for easier type handling, and avoid potential conflict with other list handlers } 

在MyBatis配置中注册以下类型处理程序(或在注释中指定):

 public class InClauseTypeHandler extends BaseTypeHandler { @Override public void setNonNullParameter(final PreparedStatement ps, final int i, final InClauseParams parameter, final JdbcType jdbcType) throws SQLException { // MySQL driver does not support this :/ Array array = ps.getConnection().createArrayOf( "VARCHAR", parameter.toArray() ); ps.setArray( i, array ); } // other required methods omitted for brevity, just add a NOOP implementation } 

然后你可以像这样使用它们

 @Select("SELECT * FROM foo WHERE id IN (#{list})" List select(@Param("list") InClauseParams params) 

但是,这对MySQL不起作用,因为MySQL连接器不支持预处理语句的setArray()

MySQL的一个可能的解决方法是使用FIND_IN_SET而不是IN

 @Select("SELECT * FROM foo WHERE FIND_IN_SET(id, #{list}) > 0") List select(@Param("list") InClauseParams params) 

你的类型处理程序变为:

 @Override public void setNonNullParameter(final PreparedStatement ps, final int i, final InClauseParams parameter, final JdbcType jdbcType) throws SQLException { // note: using Guava Joiner! ps.setString( i, Joiner.on( ',' ).join( parameter ) ); } 

注意:我不知道FIND_IN_SET的性能,所以如果它很重要,请测试它