Как использовать аннотации с iBatis (myBatis) для запроса IN?

мы хотели бы использовать только аннотации с MyBatis; мы действительно пытаемся избежать xml. Мы пытаемся использовать предложение "IN":

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

MyBatis, похоже, не может выбрать массив ints и поместить их в результирующий запрос. Кажется, что он "мягко терпит неудачу", и мы не получаем результатов.

похоже, мы могли бы сделать это с помощью XML-сопоставлений, но мы бы очень хотели этого избежать. Есть ли правильный синтаксис аннотации для этого?

9 ответов


Я считаю, что это нюанс подготовленных заявлений jdbc, а не MyBatis. Есть ссылка здесь, который объясняет эту проблему и предлагает различные решения. К сожалению, ни одно из этих решений не является жизнеспособным для вашего приложения, однако, его все еще хорошо читать, чтобы понять ограничения подготовленных заявлений в отношении предложения "IN". Решение (возможно, неоптимальное) можно найти на стороне БД. Например, в PostgreSQL можно использовать:

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

" ANY "- это то же самое, что" IN "и":: int [] " - это тип, бросающий аргумент в массив ints. Аргумент, который вводится в утверждение, должен выглядеть примерно так:

"{1,2,3,4}"

Я считаю, что ответ тот же, что и в этот вопрос. Вы можете использовать MyBatis Dynamic 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);

на <script> элемент включает динамический синтаксический анализ и выполнение SQL для аннотации. Это должно быть самое первое содержимое строки запроса. Перед ним не должно быть ничего, даже белого пространства.

обратите внимание, что переменные, которые можно использовать в различных тегах XML-скрипта, имеют одинаковые имена соглашения как обычные запросы, поэтому, если вы хотите ссылаться на свои аргументы метода, используя имена, отличные от" param1"," param2 " и т. д... вам нужно префикс каждого аргумента с аннотацией @Param.


были некоторые исследования по этой теме.

  1. одно из официальных решений от mybatis-поместить ваш динамический sql в @Select("<script>...</script>"). Однако написание xml в аннотации java довольно неграциозно. подумайте об этом @Select("<script>select name from sometable where id in <foreach collection=\"items\" item=\"item\" seperator=\",\" open=\"(\" close=\")\">${item}</script>")
  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 без <script></script> аннотация не будет передавать параметр через 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 one.
  • решение 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=\"\" 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);

Я сделал небольшой трюк в моем коде.

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

и я использовал этот MyHandler в 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;

Он работает сейчас :) Надеюсь, это кому-то поможет.

Евгений


другой вариант может быть

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

боюсь, решение Евгения работает только потому, что в образце кода есть небольшая ошибка:

  inString = "," + element;

что означает, что inString всегда содержит только одно последнее число (вместо списка сцепленных чисел).

Это должно быть

  inString += "," + element;

увы, если эта ошибка исправлена, база данных начинает сообщать об исключениях" неправильное число", потому что mybatis устанавливает" 1,2,3 " в качестве строкового параметра, и база данных просто пытается преобразовать эту строку в число : /

С другой стороны, аннотация @SelectProvider, как описано Mohit, отлично работает. Нужно только знать, что он создает новый оператор каждый раз, когда мы запускаем запрос с различными параметрами внутри предложения IN, а не повторно используем существующее PreparedStatement (поскольку параметры внутри предложения in жестко кодируются внутри 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, я использую вариант токенизатор Тома Кайта для обработки неизвестных размеров списка (учитывая ограничение Oracle 1k на предложение IN и ухудшение выполнения нескольких INs, чтобы обойти его). Это для 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;

для этого можно использовать обработчик пользовательского типа. Например:

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 connector не поддерживает 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, поэтому проверьте это, если это важно