web-dev-qa-db-fra.com

Comment utiliser Annotations avec iBatis (myBatis) pour une requête IN?

Nous aimerions utiliser uniquement les 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 sélectionner le tableau d'ints et de les mettre dans la requête résultante. Il semble "échouer doucement" et nous n'obtenons aucun résultat.

Il semblerait que nous puissions accomplir cela en utilisant des mappages XML, mais nous aimerions vraiment éviter cela. Existe-t-il une syntaxe d'annotation correcte pour cela?

25
dirtyvagabond

Je crois que ceci est une nuance des déclarations préparées par jdbc et non pas MyBatis. Il existe un lien ici qui explique ce problème et propose diverses solutions. Malheureusement, aucune de ces solutions n’est viable pour votre application. Cependant, c’est toujours une bonne lecture pour comprendre les limitations des déclarations préparées en ce qui concerne une clause "IN". Une solution (peut-être sous-optimale) peut être trouvée du côté des choses spécifiques à la DB. Par exemple, dans postgresql, on pourrait utiliser:

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

"ANY" est identique à "IN" et ":: int []" à un type transtypant l'argument dans un tableau d'ints. L'argument qui est introduit dans la déclaration devrait ressembler à ceci:

"{1,2,3,4}"
20
user199341

Je crois que la réponse est la même que celle donnée dans cette question . Vous pouvez utiliser le SQL dynamique myBatis dans vos annotations en procédant comme 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 syntaxique SQL dynamique et l'exécution de l'annotation. Il doit s'agir du tout premier contenu de la chaîne de requête. Rien ne doit être en avant, pas même un espace blanc.

Notez que les variables que vous pouvez utiliser dans les différentes balises de script XML respectent les mêmes conventions de dénomination que les requêtes classiques. Par conséquent, si vous souhaitez faire référence aux arguments de votre méthode en utilisant des noms autres que "param1", "param2", etc. Il est nécessaire de préfixer chaque argument avec une annotation @Param.

34
LordOfThePigs

Avait des recherches sur ce sujet.

  1. une des solutions officielles de mybatis est de mettre votre SQL dynamique en @Select("<script>...</script>"). Cependant, écrire du XML dans les annotations Java est peu gracieux. Pensez à cette @Select("<script>select name from sometable where id in <foreach collection=\"items\" item=\"item\" seperator=\",\" open=\"(\" close=\")\">${item}</script>")
  2. @SelectProvider fonctionne bien. Mais c'est un peu compliqué à lire.
  3. PreparedStatement ne vous permet pas de définir une liste d'entiers. pstm.setString(index, "1,2,3,4") laissera votre code SQL ressembler à ceci select name from sometable where id in ('1,2,3,4'). Mysql convertira les caractères '1,2,3,4' en nombre 1.
  4. FIND_IN_SET ne fonctionne pas avec mysql index.

Regardez dans le mécanisme sql dynamique de mybatis, il a été implémenté par SqlNode.apply(DynamicContext). Cependant, @Select sans l'annotation <script></script> ne passera pas de paramètre via DynamicContext 

voir également

  • org.Apache.ibatis.scripting.xmltags.XMLLanguageDriver
  • org.Apache.ibatis.scripting.xmltags.DynamicSqlSource
  • org.Apache.ibatis.scripting.xmltags.RawSqlSource

Alors,

  • Solution 1: utilisez @SelectProvider
  • Solution 2: Étendez LanguageDriver, qui compilera toujours SQL en DynamicSqlSource. Cependant, vous devez toujours écrire \" partout.
  • Solution 3: Étendez LanguageDriver, qui peut convertir votre propre grammaire en mybatis. 
  • Solution 4: écrivez votre propre pilote de langage qui compilera le code SQL avec un rendu de modèle, comme le fait le projet mybatis-velocity. 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=\"$1\" 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);
13
yegong

J'ai fait un petit tour 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;

Cela fonctionne maintenant:) J'espère que cela aidera quelqu'un.

Evgeny

6
pevgen

Une 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
Mohit Verma

Dans Oracle, j'utilise une variante de le tokenizer de Tom Kyte pour gérer les tailles de liste inconnues (étant donné la limite de 1 Ko imposée par Oracle sur une clause IN et l'aggravation du fait de faire plusieurs IN pour la contourner). C'est pour varchar2, mais cela peut être personnalisé pour les nombres (ou vous pouvez simplement vous fier à Oracle sachant que '1' = 1/shudder).

En supposant que vous passiez ou exécutiez les incantations de myBatis pour obtenir ids en tant que chaîne, utilisez-la:

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
Llanfar

Vous pouvez utiliser un gestionnaire de type personnalisé pour le faire. 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 types suivant dans votre configuration MyBatis (ou spécifiez-le 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 ensuite les utiliser comme ça

@Select("SELECT * FROM foo WHERE id IN (#{list})"
List<Bar> select(@Param("list") InClauseParams params)

Cependant, cela fonctionnera avec not pour MySQL car le connecteur MySQL ne prend pas en charge setArray() pour les instructions préparées.

Une solution possible pour MySQL consiste à 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 gestionnaire de types 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 connais pas les performances de FIND_IN_SET, alors testez ceci si c'est important

0
sgdesmet

Dans mon projet, nous utilisons déjà Google Guava, ce qui en fait un raccourci rapide.

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