Как использовать аннотации с iBatis (myBatis) для запроса IN? - PullRequest
28 голосов
/ 07 августа 2010

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

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

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

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

Ответы [ 8 ]

35 голосов
/ 26 марта 2014

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

22 голосов
/ 09 августа 2010

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

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

«ANY» - это то же самое, что «IN», а «:: int []» - это приведение аргумента типа к массиву целых чисел.Аргумент, который вводится в оператор, должен выглядеть примерно так:

"{1,2,3,4}"
15 голосов
/ 16 марта 2015

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

  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 dynamic 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.
  • Решение 4: Напишите свой собственный LanguageDriver, который компилирует SQL с помощью какого-либо средства визуализации шаблонов, как это делает проект mybatis-speed. Таким образом, вы можете даже интегрировать 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);
6 голосов
/ 17 декабря 2010

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

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;

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

Евгений

3 голосов
/ 26 апреля 2012

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

    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);
        }
    }
0 голосов
/ 31 мая 2018

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

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

0 голосов
/ 24 июня 2016

В Oracle я использую вариант токенайзера Тома Кайта для обработки неизвестных размеров списка (учитывая ограничение Oracle в 1k для предложения IN и ухудшение выполнения нескольких 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;
0 голосов
/ 23 мая 2016

В моем проекте мы уже используем 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));
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...