2010-08-07 16 views
20

Queremos utilizar solo anotaciones con MyBatis; Realmente estamos tratando de evitar xml. Estamos tratando de utilizar una cláusula de "IN":¿Cómo usar Anotaciones con iBatis (myBatis) para una consulta IN?

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

MyBatis no parece capaz de seleccionar la matriz de enteros y poner en los que la consulta resultante. Parece "fallar suavemente" y no obtenemos ningún resultado.

Parece que podríamos lograr esto utilizando asignaciones de XML, pero realmente nos gustaría evitar eso. ¿Hay una sintaxis de anotación correcta para esto?

+0

normal SQL requiere SQL dinámico para utilizar una variable que representa una lista de valores separados por comas –

+0

@OMG Ponies: Mis disculpas, ¿no estoy seguro de lo que intentan decir? Si tuviera que tomar su sabiduría y aplicarla a este problema, ¿cómo se vería mi solución específicamente? – dirtyvagabond

+0

Nunca he trabajado con iBatis, pero ¿puede crear la instrucción SQL como una cadena (incluidos los contenidos de las variables) antes de que ocurra algo más? Eso es todo lo que el SQL dinámico realmente es ... –

Respuesta

15

Creo que este es un matiz de las declaraciones preparadas de jdbc y no de MyBatis. Hay un enlace here que explica este problema y ofrece varias soluciones. Desafortunadamente, ninguna de estas soluciones es viable para su aplicación, sin embargo, sigue siendo una buena lectura para comprender las limitaciones de las declaraciones preparadas con respecto a una cláusula "IN". Una solución (quizás no óptima) se puede encontrar en el lado específico de DB. Por ejemplo, en postgresql, se podría utilizar:

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

"ANY" es la misma que "IN" y ":: int []" es escribir fundición el argumento en una matriz de enteros. El argumento que se alimenta en la declaración debe ser similar a:

"{1,2,3,4}" 
+0

El enlace JavaRanch presenta una idea interesante de dividir la matriz en múltiples fragmentos y ejecutar lotes. Esto no es postgres específico y podría implementarse en iBatis con un TypeHandler como la sugerencia de @ pevgen. – AngerClown

+0

En MySQL, use la siguiente consulta, pasando "blogIds" como una cadena con los identificadores separados por coma: "SELECT * FROM blog WHERE FIND_IN_SET (id, # {blogIds}) <> 0" –

+0

Este fue un gran primer paso, pero no funcionó para mí Finalmente tuve que escribir un manejador de tipos para ArrayList (usando connection.createArrayOf()), luego referenciar el manejador de tipos directamente en la {} sección before :: int []. Gracias por la buena ventaja, sin embargo. – Blamkin86

6

He hecho un pequeño truco en mi código.

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

Y utilicé esta MyHandler en 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; 

Funciona ahora :) espero que esto ayude a alguien.

Evgeny

+0

Está creando una única cadena grande con todos los valores. ¿Esto requiere lanzar en la base de datos? No estoy seguro de si esto funcionaría en todos los DB. – AngerClown

+0

Gracias por tu comentario. Tienes razón. Solo lo logré en el DB Oracle. – pevgen

3

Otra opción puede ser

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); 
     } 
    } 
2

Me temo solución de Evgeny sólo parece funcionar porque hay un pequeño error en el ejemplo de código:

inString = "," + element; 

Lo que significa that inString siempre contiene un único y último número (en lugar de una lista de números concatenados).

Esto debería ser en realidad

inString += "," + element; 

Por desgracia, si no se corrige este error la base de datos de informes se inicia excepciones "número incorrecto" porque MyBatis conjuntos "1,2,3" como un parámetro de cadena y la base de datos simplemente intenta convertir esta cadena a un número:/

Por otro lado, la anotación @SelectProvider, como la describe Mohit, funciona bien. Solo se debe tener en cuenta que crea una nueva declaración cada vez que ejecutamos la consulta con diferentes parámetros dentro de la cláusula IN en lugar de reutilizar el PreparedStatement existente (ya que los parámetros dentro de la IN-Clause están codificados dentro del SQL en lugar de siendo establecido como los parámetros de la declaración preparada). Esto a veces puede provocar pérdidas de memoria en la base de datos (ya que el DB necesita almacenar más y más declaraciones preparadas y potencialmente no reutilizará los planes de ejecución existentes).

Se puede intentar mezclar @SelectProvider y custom typeHandler. De esta forma, uno puede usar @SelectProvider para crear una consulta con tantos marcadores de posición dentro de "IN (...)" como sea necesario y luego reemplazarlos todos en el Administrador de tipos personalizado. Sin embargo, se pone un poco complicado.

26

Creo que la respuesta es la misma que se da en this question. Puede utilizar mybatis SQL dinámico en sus anotaciones haciendo lo siguiente:

@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); 

El elemento <script> permite el análisis sintáctico SQL dinámico y ejecución para la anotación. Debe ser el primer contenido de la cadena de consulta. Nada debe estar delante de él, ni siquiera espacio en blanco.

Tenga en cuenta que las variables que puede usar en las diversas etiquetas de scripts XML siguen las mismas convenciones de nomenclatura que las consultas regulares, por lo que si desea consultar los argumentos de su método utilizando nombres que no sean "param1", "param2", etc. ... debe agregar un prefijo a cada argumento con una anotación @Param.

+0

Cada vez que hago esto, recibo una excepción: "org.apache.ibatis.binding.BindingException: parámetro 'item' no encontrado". ¿Cuál es la versión mínima de mybatis requerida para que esto funcione? – Justin

+0

Esto solo funciona con myBatis 3. No estoy seguro exactamente qué versiones menores lo admiten. Asegúrese también de que no haya espacios en blanco antes del primer '' 'de la etiqueta'

0

En Oracle, utilizo una variante de Tom Kyte's tokenizer para manejar tamaños de lista desconocidos (determinado límite de 1k de Oracle en una cláusula IN y el agravamiento de hacer múltiples ins para conseguir alrededor de él). Esto es para varchar2, pero se puede personalizar para los números (o simplemente puede confiar en Oracle sabiendo que '1' = 1/shudder).

Suponiendo que pase o realizar mybatis encantamientos para conseguir ids como una cadena, se usa:

select @Select("SELECT * FROM blog WHERE id IN (select * from table(string_tokenizer(#{ids}))") 

El código:

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; 
Cuestiones relacionadas