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

发布于 2024-09-13 06:22:47 字数 295 浏览 8 评论 0原文

我们只想在 MyBatis 中使用注释;我们确实在努力避免使用 xml。我们尝试使用“IN”子句:

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

MyBatis 似乎无法挑选出整数数组并将其放入结果查询中。它似乎“温和地失败”,我们没有得到任何结果。

看起来我们可以使用 XML 映射来完成此任务,但我们确实希望避免这种情况。是否有正确的注释语法?

We'd like to use only annotations with MyBatis; we're really trying to avoid xml. We're trying to use an "IN" clause:

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

MyBatis doesn't seem able to pick out the array of ints and put those into the resulting query. It seems to "fail softly" and we get no results back.

It looks like we could accomplish this using XML mappings, but we'd really like to avoid that. Is there a correct annotation syntax for this?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

就像说晚安 2024-09-20 06:22:47

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

@Select({"<script>",
         "SELECT *", 
         "FROM blog",
         "WHERE id IN", 
           "<foreach item='item' index='index' collection='list'",
             "open='(' separator=',' close=')'>",
             "#{item}",
           "</foreach>",
         "</script>"}) 
List<Blog> selectBlogs(@Param("list") int[] ids);

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

I believe the answer is the same as is given in this question. You can use myBatis Dynamic SQL in your annotations by doing the following:

@Select({"<script>",
         "SELECT *", 
         "FROM blog",
         "WHERE id IN", 
           "<foreach item='item' index='index' collection='list'",
             "open='(' separator=',' close=')'>",
             "#{item}",
           "</foreach>",
         "</script>"}) 
List<Blog> selectBlogs(@Param("list") int[] ids);

The <script> element enables dynamic SQL parsing and execution for the annotation. It must be very first content of the query string. Nothing must be in front of it, not even white space.

Note that the variables that you can use in the various XML script tags follow the same naming conventions as regular queries, so if you want to refer to your method arguments using names other than "param1", "param2", etc... you need to prefix each argument with an @Param annotation.

指尖上的星空 2024-09-20 06:22:47

我相信这是 jdbc 准备好的语句的细微差别,而不是 MyBatis。 此处有一个链接解释了此问题并提供了各种解决方案。不幸的是,这些解决方案都不适合您的应用程序,但是,它仍然是了解准备好的语句与“IN”子句相关的限制的好读物。可以在特定于数据库的方面找到解决方案(可能不是最佳的)。例如,在 postgresql 中,可以使用:

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

“ANY”与“IN”相同,而“::int[]”是将参数类型转换为整数数组。输入到语句中的参数应该类似于:

"{1,2,3,4}"

I believe this is a nuance of jdbc's prepared statements and not MyBatis. There is a link here that explains this problem and offers various solutions. Unfortunately, none of these solutions are viable for your application, however, its still a good read to understand the limitations of prepared statements with regards to an "IN" clause. A solution (maybe suboptimal) can be found on the DB-specific side of things. For example, in postgresql, one could use:

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

"ANY" is the same as "IN" and "::int[]" is type casting the argument into an array of ints. The argument that is fed into the statement should look something like:

"{1,2,3,4}"
调妓 2024-09-20 06:22:47

对这个主题进行了一些研究。

  1. mybatis 的官方解决方案之一是将动态 sql 放入 @Select("") 中。然而,在java注解中写xml是相当不优雅的。考虑这个 @Select("")
  2. @SelectProvider 工作正常。但读起来有点复杂。
  3. ReadyStatement 不允许您设置整数列表。 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("(<foreach collection=\"$1\" item=\"__item\" separator=\",\" >#{__item}</foreach>)");
        }
        script = "<script>" + script + "</script>";
        return super.createSqlSource(configuration, script, parameterType);
    }
}

以及用法:

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

Had some research on this topic.

  1. one of official solution from mybatis is to put your dynamic sql in @Select("<script>...</script>"). However, writing xml in java annotation is quite ungraceful. think about this @Select("<script>select name from sometable where id in <foreach collection=\"items\" item=\"item\" seperator=\",\" open=\"(\" close=\")\">${item}</script>")
  2. @SelectProvider works fine. But it's a little complicated to read.
  3. PreparedStatement not allow you set list of integer. pstm.setString(index, "1,2,3,4") will let your SQL like this select name from sometable where id in ('1,2,3,4'). Mysql will convert chars '1,2,3,4' to number 1.
  4. FIND_IN_SET don't works with mysql index.

Look in to mybatis dynamic sql mechanism, it has been implemented by SqlNode.apply(DynamicContext). However, @Select without <script></script> annotation will not pass parameter via DynamicContext

see also

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

So,

  • Solution 1: Use @SelectProvider
  • Solution 2: Extend LanguageDriver which will always compile sql to DynamicSqlSource. However, you still have to write \" everywhere.
  • Solution 3: Extend LanguageDriver which can convert your own grammar to mybatis one.
  • Solution 4: Write your own LanguageDriver which compile SQL with some template renderer, just like mybatis-velocity project does. In this way, you can even integrate groovy.

My project take solution 3 and here's the code:

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("(<foreach collection=\"$1\" item=\"__item\" separator=\",\" >#{__item}</foreach>)");
        }
        script = "<script>" + script + "</script>";
        return super.createSqlSource(configuration, script, parameterType);
    }
}

And the usage:

@Lang(MybatisExtendedLanguageDriver.class)
@Select("SELECT " + COLUMNS + " FROM sometable where id IN (#{ids})")
List<SomeItem> loadByIds(@Param("ids") List<Integer> ids);
酒浓于脸红 2024-09-20 06:22:47

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

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<Double> getSubObjects(@Param("ids") Integer[] ids) throws SQLException;

它现在可以工作了:)
我希望这会对某人有所帮助。

叶夫根尼

I've made a small trick in my code.

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);
}

And I used this MyHandler in SqlMapper :

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

It works now :)
I hope this will help someone.

Evgeny

翻身的咸鱼 2024-09-20 06:22:47

其他选项可以是

    public class Test
    {
        @SuppressWarnings("unchecked")
        public static String getTestQuery(Map<String, Object> params)
        {

            List<String> idList = (List<String>) 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<Blog> selectBlogs(@Param("idList") int[] ids);
        }
    }

Other option can be

    public class Test
    {
        @SuppressWarnings("unchecked")
        public static String getTestQuery(Map<String, Object> params)
        {

            List<String> idList = (List<String>) 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<Blog> selectBlogs(@Param("idList") int[] ids);
        }
    }
浮云落日 2024-09-20 06:22:47

在我的项目中,我们已经在使用 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));
    }
}

In my project, we are already using Google Guava, so a quick shortcut is.

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));
    }
}
夜夜流光相皎洁 2024-09-20 06:22:47

在 Oracle 中,我使用 的变体Tom Kyte 的分词器 用于处理未知的列表大小(考虑到 Oracle 对 IN 子句的 1k 限制以及执行多个 IN 来绕过它的加剧)。这是针对 varchar2 的,但它可以针对数字进行定制(或者您可以仅依赖 Oracle 知道 '1' = 1 /shudder)。

假设您传递或执行 myBatis 咒语以字符串形式获取 ids ,并使用它:

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;

In Oracle, I use a variant of Tom Kyte's tokenizer to handle unknown list sizes (given Oracle's 1k limit on an IN clause and the aggravation of doing multiple INs to get around it). This is for varchar2, but it can be tailored for numbers (or you could just rely on Oracle knowing that '1' = 1 /shudder).

Assuming you pass or perform myBatis incantations to get ids as a String, to use it:

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

The code:

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;
情独悲 2024-09-20 06:22:47

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

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

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

public class InClauseTypeHandler extends BaseTypeHandler<InClauseParams> {

    @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<Bar> 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<Bar> 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 的性能>,所以测试一下它是否重要

You could use a custom type handler to do this. For example:

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

Register the following type handler in your MyBatis config (or specify in your annotation):

public class InClauseTypeHandler extends BaseTypeHandler<InClauseParams> {

    @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
}

You can then use them like this

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

However, this will not work for MySQL, because the MySQL connector does not support setArray() for prepared statements.

A possible workaround for MySQL is to use FIND_IN_SET instead of IN:

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

And your type handler becomes:

@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 ) );
    }

Note: I don't know the performance of FIND_IN_SET, so test this if it is important

风筝在阴天搁浅。 2024-09-20 06:22:47

我是用 postgresql 完成的。

    @Update('''
         UPDATE sample_table 
         SET start = null, finish = null
         WHERE id=ANY(#{id});
            ''')
    int resetData(@Param("id") String[] id)

ANY 的工作方式与 IN 类似。

上面的代码使用的是groovy,但可以通过将单引号替换为双引号来转换为java

I had done this with postgresql.

    @Update('''
         UPDATE sample_table 
         SET start = null, finish = null
         WHERE id=ANY(#{id});
            ''')
    int resetData(@Param("id") String[] id)

ANY works like the IN.

Code above is using groovy but can be converted into java by replacing the single quotes into double.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文