SQL dinámico

Una de las características más potentes de MyBatis ha sido siempre sus capacidades de SQL dinámico. Si tienes experiencia con JDBC o algún framework similar, entenderás que doloroso es concatenar strings de SQL, asegurándose de que no olvidas espacios u omitir una coma al final de la lista de columnas. El SQL dinámico puede ser realmente doloroso de usar.

Aunque trabajar con SQL dinámico no va a ser nunca una fiesta, MyBatis ciertamente mejora la situación con un lenguaje de SQL dinámico potente que puede usarse en cualquier mapped statement.

Los elementos de SQL dinámico deberían ser familiares a aquel que haya usado JSTL o algún procesador de texto basado en XML. En versiones anteriores de MyBatis había un montón de elementos que conocer y comprender. MyBatis 3 mejora esto y ahora hay algo menos de la mitad de esos elementos con los que trabajar. MyBatis emplea potentes expresiones OGNL para eliminar la necesidad del resto de los elementos:

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

if

La tarea más frecuente en SQL dinámico es incluir un trozo de la clausula where condicionalmente. Por ejemplo:

<select id="findActiveBlogWithTitleLike"
     resultType="Blog">
  SELECT * FROM BLOG
  WHERE state = ‘ACTIVE’
  <if test="title != null">
    AND title like #{title}
  </if>
</select>

Este statement proporciona una funcionalidad de búsqueda de texto. Si no se pasa ningún título, entonces se retornan todos los Blogs activos. Pero si se pasa un título se buscará un título como el pasado (para los perspicaces, sí, en este caso tu parámetro debe incluir el carácter de comodín).

¿Y cómo hacemos si debemos buscar opcionalmente por título o autor? Primeramente, yo cambiaría el nombre del statement para que tenga algo más de sentido. Y luego añadir otra condición.

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <if test="title != null">
    AND title like #{title}
  </if>
  <if test="author != null and author.name != null">
    AND author_name like #{author.name}
  </if>
</select>

choose, when, otherwise

En ocasiones no queremos usar una condición sino elegir una de entre varias opciones. De forma similar al switch de Java, MyBatis ofrece el elemento choose.

Usemos el ejemplo anterior, pero ahora vamos a buscar solamente por título si se ha proporcionado un título y por autor si se ha proporcionado un autor. Si no se proporciona ninguno devolvemos una lista de Blogs destacados (quizá una lista seleccionada por los administradores en lugar de una gran lista de blogs sin sentido).

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <choose>
    <when test="title != null">
      AND title like #{title}
    </when>
    <when test="author != null and author.name != null">
      AND author_name like #{author.name}
    </when>
    <otherwise>
      AND featured = 1
    </otherwise>
  </choose>
</select>

trim, where, set

En los ejemplos anteriores se ha sorteado intencionadamente un notorio problema del SQL dinámico. Imagina lo que sucedería si volvemos a nuestro ejemplo del “if”, pero esta vez, hacemos que “ACTIVE = 1” sea también una condición dinámica.

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG
  WHERE
  <if test="state != null">
    state = #{state}
  </if>
  <if test="title != null">
    AND title like #{title}
  </if>
  <if test="author != null and author.name != null">
    AND author_name like #{author.name}
  </if>
</select>

¿Qué sucede si no se cumple ninguna condición? Acabarías con una sentencia SQL con este aspecto:

SELECT * FROM BLOG
WHERE

Y eso fallará. ¿Y qué sucede si se cumple la segunda condición? Acabarías con una sentencia SQL con este aspecto:

SELECT * FROM BLOG
WHERE
AND title like ‘someTitle’

Y eso también fallará. Este problema no se resuelve fácil con condicionales, y si alguna vez tienes que hacerlo, posiblemente no quieras repetirlo nunca más.

MyBatis tiene una respuesta sencilla que funcionará en el 90% de los casos. Y en los casos en los que no funciona puedes personalizarlo para hacerlo funcionar. Con un cambio simple, todo funciona correctamente:

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG
  <where>
    <if test="state != null">
         state = #{state}
    </if>
    <if test="title != null">
        AND title like #{title}
    </if>
    <if test="author != null and author.name != null">
        AND author_name like #{author.name}
    </if>
  </where>
</select>

El elemento where sabe que debe insertar la “WHERE” solo si los tags internos devuelven algún contenido. Más aun, si el contenido comienza con “AND” o “OR”, sabe cómo eliminarlo.

Si el elemento where no se comporta exactamente como te gustaría, lo puedes personalizar definiendo tu propio elemento trim. Por ejemplo, el trim equivalente al elemento where es:

<trim prefix="WHERE" prefixOverrides="AND |OR ">
  ...
</trim>

El atributo prefixOverrides acepta una lista de textos delimitados pro el carácter “| “ donde el espacio en blanco es relevante. El resultado es que se elimina cualquier cosa que se haya especificado en el atributo prefixOverrides, y que se inserta todo lo incluido en el atributo with.

Hay una solución similar para updates dinámicos llamada set. El elemento set se pude usar para incluir dinámicamente columnas para modificar y dejar fuera las demás. Por ejemplo:

<update id="updateAuthorIfNecessary">
  update Author
    <set>
      <if test="username != null">username=#{username},</if>
      <if test="password != null">password=#{password},</if>
      <if test="email != null">email=#{email},</if>
      <if test="bio != null">bio=#{bio}</if>
    </set>
  where id=#{id}
</update>

En este caso, el elemento set prefijará dinámicamente el valor SET y además eliminará todas las comas sobrantes que pudieran quedar tras las asignaciones de valor después de que se hayan aplicado las condiciones.

Alternativamente, puede utilizar el elemento trim para obtener el mismo efecto:

<trim prefix="SET" suffixOverrides=",">
  ...
</trim>

Fíjate que en este caso estamos sobrescribiendo un sufijo y añadiendo un prefijo.

foreach

Otra necesidad común del SQL dinámico es iterar sobre una colección, habitualmente para construir una condición IN. Por ejemplo:

<select id="selectPostIn" resultType="domain.blog.Post">
  SELECT *
  FROM POST P
  <where>
    <foreach item="item" index="index" collection="list"
        open="ID in (" separator="," close=")" nullable="true">
          #{item}
    </foreach>
  </where>
</select>

El elemento foreach es muy potente, permite especificar una colección y declarar variables elemento e índice que pueden usarse dentro del cuerpo del elemento. Permite también abrir y cerrar strings y añadir un separador entre las iteraciones. Este elemento es inteligente en tanto en cuanto no añade separadores extra accidentalmente.

NOTA You can pass any Iterable object (for example List, Set, etc.), as well as any Map or Array object to foreach as collection parameter. When using an Iterable or Array, index will be the number of current iteration and value item will be the element retrieved in this iteration. When using a Map (or Collection of Map.Entry objects), index will be the key object and item will be the value object.

Esto finaliza la discusión sobre la configuración XML y los ficheros de mapeo XML. En la sección siguiente hablaremos del API Java en detalle, de forma que puedas obtener el máximo rendimiento de los mapeos que has creado.

script

For using dynamic SQL in annotated mapper class, script element can be used. For example:

    @Update({"<script>",
      "update Author",
      "  <set>",
      "    <if test='username != null'>username=#{username},</if>",
      "    <if test='password != null'>password=#{password},</if>",
      "    <if test='email != null'>email=#{email},</if>",
      "    <if test='bio != null'>bio=#{bio}</if>",
      "  </set>",
      "where id=#{id}",
      "</script>"})
    void updateAuthorValues(Author author);

bind

El elemento bind te permite crear una variable a partir de una expresion OGNL y asociarla al contexto. Por ejemplo:

<select id="selectBlogsLike" resultType="Blog">
  <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
  SELECT * FROM BLOG
  WHERE title LIKE #{pattern}
</select>

Soporte de multiples vendedores de base de datos

Si se ha configurado un databaseIdProvider la variable "_databaseId" estará disponible en el código dinámico, de esta forma puedes constrir distintas sntencias dependiendo del fabricante de la base de datos. Mira el siguiente ejemplo:

<insert id="insert">
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    <if test="_databaseId == 'oracle'">
      select seq_users.nextval from dual
    </if>
    <if test="_databaseId == 'db2'">
      select nextval for seq_users from sysibm.sysdummy1"
    </if>
  </selectKey>
  insert into users values (#{id}, #{name})
</insert>

Lenguajes de scripting customizados para SQL dinámico

Desde la versión 3.2 MyBatis soporta la adición de lenguajes de scripting de forma que puedes añadir un driver de lenguaje y usar dicho lenguaje para escribir tus sentencias SQL.

Puedes añadir un lenguaje implementando el siguiente interfaz:

public interface LanguageDriver {
  ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
  SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
  SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
}

Una vez tienes tu driver de lenguaje puedes puede hacer que sea el de uso por defecto estableciéndolo en el fichero mybatis-config.xml:

<typeAliases>
  <typeAlias type="org.sample.MyLanguageDriver" alias="myLanguage"/>
</typeAliases>
<settings>
  <setting name="defaultScriptingLanguage" value="myLanguage"/>
</settings>

En lugar de cambiar el valor por defecto, puedes indicar el lenguaje para un statement específico añadiendo el atributo lang de la siguiente forma:

<select id="selectBlog" lang="myLanguage">
  SELECT * FROM BLOG
</select>

O, en el caso de que uses mappers, usando la anotación @Lang:

public interface Mapper {
  @Lang(MyLanguageDriver.class)
  @Select("SELECT * FROM BLOG")
  List<Blog> selectBlog();
}

NOTA Puedes utilizar Apache Velocity como lenguaje dinámico. Echa un vistazo al proyecto MyBatis-Velocity para conocer los detalles.

Todos los tags que has visto en las secciones previas se proporcionan por el lenguaje por defecto de MyBatis cuyo driver es org.apache.ibatis.scripting.xmltags.XmlLanguageDriver que tiene un alias llamado xml.