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?

23
demandé sur dirtyvagabond 2010-08-07 05:09:18

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}"
17
répondu user199341 2010-08-09 16:35:45

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.

32
répondu LordOfThePigs 2017-05-23 11:47:20

avait quelques recherches sur ce sujet.

  1. 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>")
  2. @SelectProvider fonctionne très bien. Mais c'est un peu compliqué à lire.
  3. PreparedStatement ne vous permet pas de définir la liste des entiers. pstm.setString(index, "1,2,3,4") laissera votre SQL comme ceci select name from sometable where id in ('1,2,3,4') . Mysql convertira les caractères '1,2,3,4' en 1 .
  4. 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);
9
répondu yegong 2015-03-16 11:58:22

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

6
répondu pevgen 2011-06-20 10:33:22

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);
        }
    }
3
répondu Mohit Verma 2012-04-26 10:10:34

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.

2
répondu Tom R. 2012-08-22 10:01:34

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));
    }
}
1
répondu user2665773 2016-05-23 15:41:27

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;
0
répondu Llanfar 2016-06-24 12:54:53

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

0
répondu sgdesmet 2018-05-31 11:51:31