Подтвердить что ты не робот

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

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

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

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

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

4b9b3361

Ответ 1

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

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

"ЛЮБОЙ" - это то же самое, что "IN" и ":: int []" - это тип, переводящий аргумент в массив из int. Аргумент, который подается в оператор, должен выглядеть примерно так:

"{1,2,3,4}"

Ответ 2

Я считаю, что ответ такой же, как в этом вопросе. Вы можете использовать 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 script, соответствуют тем же соглашениям об именах, что и обычные запросы, поэтому, если вы хотите ссылаться на аргументы метода, используя имена, отличные от "param1", "param2", и т.д.... вам нужно префикс каждого аргумента аннотацией @Param.

Ответ 3

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

  • одним из официальных решений 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>")
  • @SelectProvider работает нормально. Но это немного сложно читать.
  • 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.
  • FIND_IN_SET не работает с индексом mysql.

Посмотрите на динамический SQL-механизм mybatis, он был реализован 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: Расширьте языкDriver, который всегда будет компилировать sql в DynamicSqlSource. Однако вам все равно придется писать \" всюду.
  • Решение 3: Расширьте языкDriver, который может преобразовать вашу собственную грамматику в 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);

Ответ 4

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

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;

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

Евгений

Ответ 5

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

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

Ответ 6

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

  inString = "," + element;

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

На самом деле это должно быть

  inString += "," + element;

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

С другой стороны, аннотация @SelectProvider, как описано Mohit, отлично работает. Нужно только знать, что он создает новый оператор каждый раз, когда мы запускаем запрос с разными параметрами внутри IN-предложения, вместо повторного использования существующего PreparedStatement (поскольку параметры внутри IN-Clause жестко закодированы внутри SQL вместо того, чтобы быть установить в качестве подготовленных параметров оператора). Иногда это может привести к утечкам памяти в базе данных (поскольку БД необходимо хранить все больше подготовленных операторов, и это потенциально не может повторно использовать существующие планы выполнения).

Можно попытаться смешать как @SelectProvider, так и customHandler. Таким образом, вы можете использовать @SelectProvider для создания запроса с таким количеством заполнителей внутри "IN (...)" по мере необходимости, а затем заменить их все в пользовательском TypeHandler. Тем не менее, это немного сложно.

Ответ 7

В моем проекте мы уже используем 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));
    }
}

Ответ 8

В Oracle я использую вариант токенизатора Tom Kyte для обработки неизвестных размеров списка (учитывая, что ограничение Oracle 1k на предложение IN и обострение сделать несколько INs, чтобы обойти его). Это для varchar2, но он может быть адаптирован для чисел (или вы можете просто полагаться на Oracle, зная, что "1" = 1/содрогание).

Предполагая, что вы передаете или выполняете заклинания 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;

Ответ 9

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

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

Ответ 10

Я думаю, что XML - лучшее решение