Как использовать аннотации с iBatis (myBatis) для запроса IN?
Мы хотим использовать только аннотации с MyBatis; мы действительно пытаемся избежать xml. Мы пытаемся использовать предложение "IN":
@Select("SELECT * FROM blog WHERE id IN (#{ids})")
List<Blog> selectBlogs(int[] ids);
MyBatis, похоже, не в состоянии выделить массив int и поместить их в результирующий запрос. Кажется, что он "терпит неудачу", и мы не получаем никаких результатов.
Похоже, мы могли бы выполнить это с использованием сопоставлений XML, но нам бы очень хотелось этого избежать. Есть ли правильный синтаксис аннотации для этого?
Ответы
Ответ 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 - лучшее решение