Ficheros XML de mapeo

La potencia de MyBatis reside en los Mapped Statements. Aquí es donde está la magia. Para lo potentes que son, los ficheros XML de mapeo son relativamente simples. Sin duda, si los comparas con el código JDBC equivalente comprobarás que ahorras el 95% del código.

Los ficheros XML de mapeos SQL solo tienen unos pocos elementos de alto nivel (en el orden en el que deberían definirse):

  • cache – Configuración de la caché para un namespace.
  • cache-ref – Referencia a la caché de otro namespace.
  • resultMap – El elemento más complejo y potente que describe como cargar tus objetos a partir de los ResultSets.
  • parameterMap – Deprecada! Antigua forma de mapear parámetros. Se recomienda el uso de parámetros en línea. Este elemento puede ser eliminado en futuras versiones. No se ha documentado en este manual.
  • sql – Un trozo de SQL reusable que puede utilizarse en otras sentencias.
  • insert – Una sentencia INSERT.
  • update – A Una sentencia UPDATE.
  • delete – Una sentencia DELETE.
  • select – Una sentencia SELECT.

Las siguientes secciones describen estos elementos en detalle, comenzando con los propios elementos.

select

El select statement es uno de los elementos que más utilizarás en MyBatis. No es demasiado útil almacenar datos en la base de datos si no puedes leerlos, de hecho las aplicaciones suelen leer bastantes más datos de los que modifican. Por cada insert, update o delete posiblemente haya varias selects. Este es uno de los principios básicos de MyBatis y la razón por la que se ha puesto tanto esfuerzo en las consultas y el mapeo de resultados. El select statement es bastante simple para los casos simples. Por ejemplo:

<select id="selectPerson" parameterType="int" resultType="hashmap">
  SELECT * FROM PERSON WHERE ID = #{id}
</select>

Esta sentencia se llama “selectPerson”, recibe un parámetro de tipo in (o Integer), y devuelve una HashMap usando los nombres de columna como clave y los valores del registro como valores.

Observa la notación utilizada para los parámetros:

#{id}

Esto le indica a MyBatis que cree un parámetro de PreparedStatement. Con JDBC, ese parámetro iría identificado con una “?” en la select que se le pasa al PreparedStatement, algo así:

// Código JDBC similar, NO MyBatis…
String selectPerson = "SELECT * FROM PERSON WHERE ID=?";
PreparedStatement ps = conn.prepareStatement(selectPerson);
ps.setInt(1,id);

JDBC requiere mucho más código para extraer los resultados y mapearlos a una instancia de un objetos, que es precisamente lo que MyBatis evita que tengas que hacer. Aun queda mucho por conocer sobre los parámetros y el mapeo de resultados. Todos sus detalles merecen su propio capítulo, y serán tratados más adelante.

El select statement tiene más atributos que te permiten configurar como debe comportarse cada select statement.

<select
  id="selectPerson"
  parameterType="int"
  parameterMap="deprecated"
  resultType="hashmap"
  resultMap="personResultMap"
  flushCache="false"
  useCache="true"
  timeout="10"
  fetchSize="256"
  statementType="PREPARED"
  resultSetType="FORWARD_ONLY">
Atributos de Select
Atributo Descripción
id Un identificador único dentro del namespace que se utiliza para identificar el statement.
parameterType El nombre completamente cualificado de la clase o el alias del parámetro que se pasará al statement. Este atributo es opcional porque MyBatis puede calcular el TypeHandler a utlizar a partir del parametro actual usado en la invocación al statement. Por defecto: no informado.
parameterMap Este es un atributo obsoleto que permite referenciar a un elemento parameterMap externo. Se recomienda utilizar mapeos en línea (in-line) y el atributo parameterType.
resultType El nombre completamente cualificado o el alias del tipo de retorno de este statement. Ten en cuenta que en el caso de las colecciones el parámetro debe ser el tipo contenido en la colección, no el propio tipo de la colección. Puedes utilizar resultType o resultMap, pero no ambos.
resultMap Una referencia una un resultMap externo. Los resultMaps son la característica más potente de MyBatis, con un conocimiento detallado de los mismos, se pueden resolver muchos casos complejos de mapeos. Puedes utilizar resultMap O resultType, pero no ambos.
flushCache Informar esta propiedad a true hará que la cache local y la de segundo nivel se vacíen cada vez que se llame a este statement. Por defecto es false para select statements.
useCache Informar esta propiedad a true hará que los resultados de la ejecución de este statement se cacheen en la caché de segundo nivel. Por defecto es true para las select statements.
timeout Establece el número de segundos que el driver esperará a que la base de datos le devuelva una respuesta antes de lanzar una excepción. Por defecto: no informado (depende del driver de base de datos).
fetchSize Este es un atributo que “sugiere” al driver que devuelva los resultados en bloques de filas en el número indicado por el parámetro. Por defecto: no informado (depende del driver de base de datos).
statementType Puede valer STATEMENT, PREPARED o CALLABLE. Hace que MyBatis use Statement, PreparedStatement o CallableStatement respectivamente. Por defecto: PREPARED.
resultSetType Puede valer FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE|DEFAULT(same as unset). Por defecto: no informado (depende del driver de base de datos).
databaseId Si hay un DatabaseIdProvider configurado. MyBatis cargará todos los statements sin el atributo databaseId o aquellos con un databaseId que coincide con el actual. Si se encuentra un statement con y sin databaseId el último se descartará.
resultOrdered De aplicación exclusiva para select anidadas. Si es true, se asume que los resultados anidados están agrupados de forma que cuando se lee un nuevo resultado principal nuevo, no habrá más referencias a resultados principales anteriores. De esta forma los resultados anidados se rellenarán de una manera mucho ás eficiente en términos de memoria. Defecto: false.
resultSets This is only applicable for multiple result sets. It lists the result sets that will be returned by the statement and gives a name to each one. Names are separated by commas.
affectData Set this to true when writing a INSERT, UPDATE or DELETE statement that returns data so that the transaction is controlled properly. Also see Transaction Control Method. Default: false (since 3.5.12)

insert, update and delete

Los insert, update y delete statements son muy similares en su implementación:

<insert
  id="insertAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  keyProperty=""
  keyColumn=""
  useGeneratedKeys=""
  timeout="20">

<update
  id="updateAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">

<delete
  id="deleteAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">
Insert, Update and Delete Attributes
Atributo Descripción
id Un identificador único dentro del namespace que se utiliza para identificar el statement.
parameterType El nombre completamente cualificado de la clase o el alias del parámetro que se pasará al statement. Este atributo es opcional porque MyBatis puede calcular el TypeHandler a utlizar a partir del parametro actual usado en la invocación al statement. Por defecto: no informado.
parameterMap Método deprecado de referirse a un parameterMap externo. Usa mapeos inline y el atributo paramterType.
flushCache Informar esta propiedad a true hará que la caché se vacíe cada vez que se llame a este statement. Por defecto es false para select statements.
timeout Establece el número máximo de segundos que el driver esperará a que la base de datos le devuelva una respuesta antes de lanzar una excepción. Por defecto: no informado (depende del driver de base de datos).
statementType Puede valer STATEMENT, PREPARED o CALLABLE. Hace que MyBatis use Statement, PreparedStatement o CallableStatement respectivamente. Por defecto: PREPARED.
useGeneratedKeys (solo en insert y update) Indica a MyBatis que utilice el método getGeneratedKeys de JDBC para recuperar las claves autogeneras automáticamente por la base de datos. (ej. campos autoincrementales en SGBD como MySQL o SQL Server). Por defecto: false
keyProperty (solo en insert y update) Indica la propiedad a la que MyBatis debe asignar la clave autogenerada devuelva por getGeneratedKeys o por un elemento hijo de tipo selectKey. Por defecto: no informado. Puede contener una lista de nombres seperados por comas en el caso de que se esperen varios valores autogenerados.
keyColumn (solo en insert y update) Indica el nombre de la columna en tabla con clave generada. Solo se requiere en algunas bases de datos (como PostgreSQL) donde la columna clave no es la primera de la tabla. Puede contener una lista de nombres seperados por comas en el caso de que se esperen varios valores autogenerados.
databaseId Si hay un DatabaseIdProvider configurado. MyBatis cargará todos los statements sin el atributo databaseId o aquellos con un databaseId que coincide con el actual. Si se encuentra un statement con y sin databaseId el último se descartará.

A continuación se muestran unos ejemplos de insert, update y delete.

<insert id="insertAuthor">
  insert into Author (id,username,password,email,bio)
  values (#{id},#{username},#{password},#{email},#{bio})
</insert>

<update id="updateAuthor">
  update Author set
    username = #{username},
    password = #{password},
    email = #{email},
    bio = #{bio}
  where id = #{id}
</update>

<delete id="deleteAuthor">
  delete from Author where id = #{id}
</delete>

Tal y como se ha indicado, insert es algo más complejo dado que dispone de algunos atributos extra para gestionar la generación de claves de varias formas distintas.

Primeramente, si tu base de datos soporta la auto-generación de claves (ej. MySQL y SQL Server), entonces puedes simplemente informar el atributo useGeneratedKeys=”true” e informar también en keyProperty el nombre del la propiedad donde guardar el valor y ya has terminado. Por ejemplo, si la columna id de la tabla Author del ejemplo siguiente fuera autogenerada el insert statement se escribiría de la siguiente forma:

<insert id="insertAuthor" useGeneratedKeys="true"
    keyProperty="id">
  insert into Author (username,password,email,bio)
  values (#{username},#{password},#{email},#{bio})
</insert>

If your database also supports multi-row insert, you can pass a list or an array of Authors and retrieve the auto-generated keys.

<insert id="insertAuthor" useGeneratedKeys="true"
    keyProperty="id">
  insert into Author (username, password, email, bio) values
  <foreach item="item" collection="list" separator=",">
    (#{item.username}, #{item.password}, #{item.email}, #{item.bio})
  </foreach>
</insert>

MyBatis puede tratar las claves autogeneradas de otra forma para el caso de las bases de datos que no soportan columnas autogeneradas, o porque su driver JDBC no haya incluido aun dicho soporte.

A continuación se muestra un ejemplo muy simple que genera un id aleatorio (algo que posiblemente nunca harás pero que demuestra la flexibilidad de MyBatis y cómo MyBatis ignora la forma en la que se consigue la clave):

<insert id="insertAuthor">
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
  </selectKey>
  insert into Author
    (id, username, password, email,bio, favourite_section)
  values
    (#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})
</insert>

En el ejemplo anterior, el selectKey statement se ejecuta primero, la propiedad id de Author se informará y posteriormente se invocará al insert statement. Esto proporciona un comportamiento similar a la generación de claves en base de datos sin complicar el código Java.

El elemento selectKey tiene el siguiente aspecto:

<selectKey
  keyProperty="id"
  resultType="int"
  order="BEFORE"
  statementType="PREPARED">
selectKey Attributes
Attribute Description
keyProperty La propiedad destino con la que debe informarse el resultado del selectKey statement. Puede contener una lista de nombres seperados por comas en el caso de que se esperen varios valores autogenerados.
keyColumn Los nombres de columnas en el ResultSet que corresponden con las propiedades. Puede contener una lista de nombres seperados por comas en el caso de que se esperen varios valores autogenerados.
resultType El tipo de retorno. MyBatis puede adivinarlo pero no está de más añadirlo para asegurarse. MyBatis permite usar cualquier tipo simple, incluyendo Strings.
order Puede contener BEFORE o AFTER. Si se informa a BEFORE, entonces la obtención de la clave se realizará primero, se informará el campo indicado en keyProperty y se ejecutará la insert. Si se informa a AFTER se ejecuta primero la insert y después la selectKey – Esto es habitual en bases de datos como Oracle que soportan llamadas embebidas a secuencias dentro de una sentencia insert.
statementType Al igual que antes, MyBatis soporta sentencias de tipo STATEMENT, PREPARED and CALLABLE que corresponden Statement, PreparedStatement y CallableStatement respectivamente.

As an irregular case, some databases allow INSERT, UPDATE or DELETE statement to return result set (e.g. RETURNING clause of PostgreSQL and MariaDB or OUTPUT clause of MS SQL Server). This type of statement must be written as <select> to map the returned data.

<select id="insertAndGetAuthor" resultType="domain.blog.Author"
      affectData="true" flushCache="true">
  insert into Author (username, password, email, bio)
  values (#{username}, #{password}, #{email}, #{bio})
  returning id, username, password, email, bio
</select>

sql

Este elemento se utiliza para definir un fragmento reusable de código SQL que puede ser incluido en otras sentencias. It can be statically (during load phase) parametrized. Different property values can vary in include instances. Por ejemplo:

<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>

Este fragmento de SQL puede ser incluido en otra sentencia, por ejemplo:

<select id="selectUsers" resultType="map">
  select
    <include refid="userColumns"><property name="alias" value="t1"/></include>,
    <include refid="userColumns"><property name="alias" value="t2"/></include>
  from some_table t1
    cross join some_table t2
</select>

Property value can be also used in include refid attribute or property values inside include clause, for example:

<sql id="sometable">
  ${prefix}Table
</sql>

<sql id="someinclude">
  from
    <include refid="${include_target}"/>
</sql>

<select id="select" resultType="map">
  select
    field1, field2, field3
  <include refid="someinclude">
    <property name="prefix" value="Some"/>
    <property name="include_target" value="sometable"/>
  </include>
</select>

Parameters

En todos los statements anteriores se han mostrado ejemplos de parámetros simples. Los parámetros son elementos muy potentes en MyBatis. En los casos simples, probablemente el 90% de los casos, no hay mucho que decir sobre ellos, por ejemplo:

<select id="selectUsers" resultType="User">
  select id, username, password
  from users
  where id = #{id}
</select>

El ejemplo anterior muestra un mapeo muy simple de parámetro con nombre. El atributo parameterType se ha informado a “int”, por lo tanto el nombre del parámetro puede ser cualquiera. Los tipos primitivos y los tipos de datos simples como Integer o String no tienen propiedades relevantes y por tanto el parámetro será reemplazado por su valor. Sin embargo, si pasas un objeto complejo, entonces el comportamiento es distinto. Por ejemplo:

<insert id="insertUser" parameterType="User">
  insert into users (id, username, password)
  values (#{id}, #{username}, #{password})
</insert>

Si se pasa un objeto de tipo User como parámetro en este statement, se buscarán en él las propiedades id, username y password y sus valores se pasarán como parámetros de un PreparedStatement.

Esta es una Buena forma de pasar parámetros a statements. Pero los parameter maps (mapas de parámetros) tienen otras muchas características.features of parameter maps.

Primeramente, es posible especificar un tipo de dato concreto.

#{property,javaType=int,jdbcType=NUMERIC}

Como en otros casos, el tipo de Java (javaType) puede casi siempre obtenerse del objeto recibido como parámetro, salvo si el objeto es un HashMap. En ese caso debe indicarse el javaType para asegurar que se usa el TypeHandler correcto.

NOTA El tipo JDBC es obligatorio para todas las columnas que admiten null cuando se pasa un null como valor. Puedes investigar este tema por tu cuenta leyendo los JavaDocs del método PreparedStatement.setNull().

Si quieres customizar aun más el tratamiento de tipos de datos, puedes indicar un TypeHandler específico (o un alias), por ejemplo:

#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}

Comienza a parecer demasiado verboso, pero lo cierto es que rara vez necesitaras nada de esto.

Para los tipos numéricos hay un atributo numericScale que permite especificar cuantas posiciones decimales son relevantes.

#{height,javaType=double,jdbcType=NUMERIC,numericScale=2}

Finalmente, el atributo mode te permite especificar parámetros IN, OUT o INOUT. Si un parámetro es OUT o INOUT, el valor actual de las propiedades del objeto pasado como parámetro será modificado. Si el mode=OUT (o INOUT) y el jdbcType=CURSOR (ej. Oracle REFCURSOR), debes especificar un resultMap para mapear el RestultSet al tipo del parámetro. Ten en cuenta que el atributo javaType es opcional en este caso, dado que se establecerá automáticamente al valor ResultSet en caso de no haberse especificado si el jdbcType es CURSOR.

#{department, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, resultMap=departmentResultMap}

MyBatis también soporta tipos de datos avanzados como los structs, pero en este caso debes indicar in el statement el jdbcTypeName en la declaración del parámetro de tipo OUT.

#{middleInitial, mode=OUT, jdbcType=STRUCT, jdbcTypeName=MY_TYPE, resultMap=departmentResultMap}

A pesar de estas potentes opciones, la mayoría de las veces simplemente debes especificar el nombre de la propiedad y MyBatis adivinará lo demás. A lo sumo, deberás especificar los jdbcTypes para las columnas que admiten nulos.

#{firstName}
#{middleInitial,jdbcType=VARCHAR}
#{lastName}