How to use Annotations with iBatis (myBatis) for an IN query?

42,306

Solution 1

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

Solution 2

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.

Solution 3

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

Solution 4

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

Solution 5

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);
        }
    }
Share:
42,306
dirtyvagabond
Author by

dirtyvagabond

Updated on April 20, 2021

Comments

  • dirtyvagabond
    dirtyvagabond about 3 years

    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?

  • AngerClown
    AngerClown over 13 years
    The JavaRanch link presents an interesting idea of breaking the array into multiple chunks and executing batches. This is not postgres specific and could be implemented in iBatis with a TypeHandler like @pevgen's suggestion.
  • AngerClown
    AngerClown over 13 years
    You are creating a single big String with all the values in it. Does this require casting on the DB? Not sure if this would work on all DBs.
  • pevgen
    pevgen over 13 years
    Thank's for your comment. You are right. I made it to the DB Oracle only.
  • Italo Borssatto
    Italo Borssatto about 12 years
    In MySQL, use the following query, passing "blogIds" as a String with the ids separated by comma: "SELECT * FROM blog WHERE FIND_IN_SET(id, #{blogIds}) <> 0"
  • Justin Killen
    Justin Killen over 9 years
    Whenever I do this, I get an exception: "org.apache.ibatis.binding.BindingException: Parameter 'item' not found." What is the minimum version of mybatis required for this to work?
  • LordOfThePigs
    LordOfThePigs over 9 years
    This only works with myBatis 3. I'm not sure exactly which minor versions support it. Also make absolutely sure that there is no whitespace before the first < of the <script> tag, or you'll get all sorts of strange errors.
  • Justin Killen
    Justin Killen over 9 years
    For anybody experiencing this problem, I was previously using version 3.1.1 and it wasn't working. I updated to version 3.2.7 and now it works, so it must have been fixed somewhere between those two versions.
  • Blamkin86
    Blamkin86 over 9 years
    This was a great first step, but did not work for me. Ultimately I had to write a typehandler for the ArrayList (using connection.createArrayOf()), then reference the typehandler directly in the {} section before ::int[]. Thanks for the good lead, however.
  • slnowak
    slnowak about 8 years
    Can't believe we still have to do this in 2016. Am I missing something?
  • Jin Kwon
    Jin Kwon over 6 years
    Is this functionality official?
  • fankai
    fankai over 5 years
    in 2019, still seems to be the best solution~
  • Chloe
    Chloe about 5 years
    This won't work because it creates a query like where id_parent in ('1,2,3') with 1 parameter instead of where id_parent in (1,2,3) with 3 parameters. It's worse if you use strings as that would create where name in ('''Tom'',''Dick'',''Harry''') with 1 parameter and the quotes escaped with doubling.
  • Chloe
    Chloe about 5 years
    This won't work because it creates a query like where id_parent in ('1,2,3') with 1 parameter instead of where id_parent in (1,2,3) with 3 parameters. It's worse if you use strings as that would create where name in ('''Tom'',''Dick'',''Harry''') with 1 parameter and the quotes escaped with doubling.