Comment utiliser les Annotations avec iBatis (myBatis) pour une requête IN?
nous aimerions utiliser seulement des annotations avec MyBatis; nous essayons vraiment d'éviter xml. Nous essayons d'utiliser une clause "IN":
@Select("SELECT * FROM blog WHERE id IN (#{ids})")
List<Blog> selectBlogs(int[] ids);
MyBatis ne semble pas capable de repérer le tableau des entrées et de les mettre dans la requête résultante. Il semble "échouer doucement" et nous n'obtenons aucun résultat.
il semble que nous pourrions accomplir ceci en utilisant des mappages XML, mais nous aimerions vraiment éviter cela. Y a-t-il une syntaxe d'annotation correcte pour cela?
9 réponses
je crois que c'est une nuance des déclarations préparées par jdbc et non MyBatis. Il y a un lien ici qui explique ce problème et offre diverses solutions. Malheureusement, aucune de ces solutions n'est viable pour votre application, cependant, il est encore une bonne lecture pour comprendre les limites des déclarations préparées en ce qui concerne une clause "dans". Une solution (peut-être sous-optimale) peut être trouvée sur le côté DB-specific des choses. Par exemple, en postgresql, on pourrait utiliser:
"SELECT * FROM blog WHERE id=ANY(#{blogIds}::int[])"
"TOUT" est le même que le "EN" et "::int[]" est le type coulée de l'argument dans un tableau d'entiers. L'argument qui est introduit dans la déclaration devrait ressembler à quelque chose comme:
"{1,2,3,4}"
je crois que la réponse est la même que celle donnée dans cette question . Vous pouvez utiliser myBatis Dynamic SQL dans vos annotations en faisant ce qui suit:
@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);
l'élément <script>
permet l'analyse et l'exécution dynamiques de SQL pour l'annotation. Ce doit être le premier contenu de la chaîne de requête. Rien ne doit être en face de lui, même pas l'espace blanc.
Notez que les variables que vous pouvez utiliser dans les différentes Les balises de script XML suivent les mêmes conventions de nommage que les requêtes régulières, donc si vous voulez vous référer à vos arguments de méthode en utilisant des noms autres que "param1", "param2", etc... vous devez préfixer chaque argument avec une annotation @Param.
avait quelques recherches sur ce sujet.
- une des solutions officielles de mybatis est de mettre votre sql dynamique dans
@Select("<script>...</script>")
. Cependant, écrire xml dans java annotation est tout à fait ingrate. pensez à ce@Select("<script>select name from sometable where id in <foreach collection=\"items\" item=\"item\" seperator=\",\" open=\"(\" close=\")\">${item}</script>")
-
@SelectProvider
fonctionne très bien. Mais c'est un peu compliqué à lire. - PreparedStatement ne vous permet pas de définir la liste des entiers.
pstm.setString(index, "1,2,3,4")
laissera votre SQL comme ceciselect name from sometable where id in ('1,2,3,4')
. Mysql convertira les caractères'1,2,3,4'
en1
. - FIND_IN_SET ne fonctionne pas avec mysql index.
Regarder dans mybatis sql dynamique mécanisme, il a été mis en œuvre par SqlNode.apply(DynamicContext)
. Cependant, @Select sans <script></script>
l'annotation ne passera pas le paramètre via DynamicContext
voir aussi
-
org.apache.ibatis.scripting.xmltags.XMLLanguageDriver
-
org.apache.ibatis.scripting.xmltags.DynamicSqlSource
-
org.apache.ibatis.scripting.xmltags.RawSqlSource
,
- Solution 1: Use @SelectProvider
- Solution 2: étendre LanguageDriver qui compilera toujours sql à
DynamicSqlSource
. Cependant, vous devez encore écrire\"
partout. - Solution 3: étendre LanguageDriver qui peut convertir votre propre grammaire en mybatis one.
- Solution 4: Ecrire votre propre LanguageDriver qui compilent SQL avec un modèle de renderer, tout comme mybatis-velocity project. De cette façon, vous pouvez même intégrer groovy.
mon projet prend la solution 3 et voici le 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=\"\" item=\"__item\" separator=\",\" >#{__item}</foreach>)");
}
script = "<script>" + script + "</script>";
return super.createSqlSource(configuration, script, parameterType);
}
}
et l'usage:
@Lang(MybatisExtendedLanguageDriver.class)
@Select("SELECT " + COLUMNS + " FROM sometable where id IN (#{ids})")
List<SomeItem> loadByIds(@Param("ids") List<Integer> ids);
j'ai fait un petit truc dans mon 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);
}
et j'ai utilisé ce MyHandler dans 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;
ça marche maintenant :) J'espère que cela aidera quelqu'un.
Evgeny
autre option peut être
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);
}
}
je crains que la solution D'Evgeny ne semble fonctionner que parce qu'il y a un petit bug dans l'échantillon de code:
inString = "," + element;
, ce qui signifie que l'insertion ne contient qu'un seul, dernier numéro (au lieu d'une liste de numéros concaténés).
cela devrait en fait être
inString += "," + element;
hélas, si cette erreur est corrigée la base de données commence à signaler" nombre incorrect "exceptions parce que mybatis définit "1,2,3" comme une chaîne de caractères paramètre et la base de données essaie simplement de convertir cette chaîne en un nombre: /
d'un autre côté, l'annotation de @SelectProvider, telle que décrite par Mohit, fonctionne très bien. Il faut seulement être conscient qu'il crée une nouvelle instruction à chaque fois que nous exécutons la requête avec des paramètres différents à l'intérieur de la clause IN-clause plutôt que de réutiliser la préprogrammation existante (car les paramètres à l'intérieur de la Clause IN-Clause sont durcodés à l'intérieur du SQL au lieu d'être définis comme des instructions préparées). paramètre.) Cela peut parfois conduire à des fuites de mémoire dans la base de données (comme la base de données a besoin de stocker de plus en plus d'énoncés préparés et il est possible qu'elle ne réutilise pas les plans d'exécution existants).
on peut essayer de mélanger à la fois @Select Provider et custom typeHandler. De cette façon, on peut utiliser le Provider @Selectpour créer une requête avec autant de placeholders à l'intérieur de "IN (...) "si nécessaire et puis les remplacer tous dans le Custom TypeHandler. Cela devient un peu difficile, cependant.
dans mon projet, nous utilisons déjà Google Guava, donc un raccourci rapide est.
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));
}
}
dans Oracle, j'utilise une variante de tokenizer de Tom Kyte pour gérer des tailles de liste inconnues (compte tenu de la limite 1k D'Oracle sur une clause IN et l'aggravation de faire plusieurs INs pour se déplacer). Cela est pour varchar2, mais il peut être adapté pour les nombres (ou vous pourriez juste compter sur Oracle sachant que '1' = 1 /frissonner).
en supposant que vous passez ou effectuez des incantations myBatis pour obtenir ids
comme une chaîne de caractères, pour l'utiliser:
select @Select("SELECT * FROM blog WHERE id IN (select * from table(string_tokenizer(#{ids}))")
le code:
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;
vous pouvez utiliser un gestionnaire de type personnalisé pour faire cela. Par exemple:
public class InClauseParams extends ArrayList<String> {
//...
// marker class for easier type handling, and avoid potential conflict with other list handlers
}
enregistrez le gestionnaire de type suivant dans votre configuration MyBatis (ou spécifiez dans votre annotation):
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
}
vous pouvez alors les utiliser comme ceci
@Select("SELECT * FROM foo WHERE id IN (#{list})"
List<Bar> select(@Param("list") InClauseParams params)
cependant, ne fonctionnera pas pour MySQL, car le connecteur MySQL ne supporte pas setArray()
pour les déclarations préparées.
une solution possible pour MySQL est d'utiliser FIND_IN_SET
au lieu de IN
:
@Select("SELECT * FROM foo WHERE FIND_IN_SET(id, #{list}) > 0")
List<Bar> select(@Param("list") InClauseParams params)
et votre type handler devient:
@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 ) );
}
Note: Je ne sais pas la performance de FIND_IN_SET
, donc tester si c'est important