jueves, 1 de junio de 2006

iBatis Datamapper (sql maps) parte 1

El framework iBatis DataMapper (también conocido como SQL
Maps) permite reducir significativamente la codificación en
java para una aplicación que maneja base de datos relacional.
Quizás la primera impresión que uno tenga es que sea
igual a Hibernate (que es un mapeador de objetos con tablas
relacionales – ORM). Ibatis es diferente. Como veremos en este
tutor, iBatis mapea las consultas SQL y permite interactuarlas con
JavaBeans tanto como parámetros de entrada y como salidas.


Manos a la obra


Para comenzar, obtendremos el framework de la página de
ASF: http://ibatis.apache.org/
El archivo que habremos bajado (iBATIS_DBL-2.1.7.XX.zip) contendrá
tres archivos .jar. En este tutor solo usaremos ibatis-common-2.jar y
ibatis-sqlmap-2.jar. No necesita de algún otro .jar, al menos
para este capítulo.


Definiendo la estructura de los datos


Crearemos nuestro JavaBean Categoria con la
siguiente estructura.


package com.jugperu.tutores.ibatis.beans;

public class Categoria {
private int id; // podemos utilizar tipos de datos nativos
private String nombre;
/*... poner sus respectivos métodos
setXX() y getXX() para que tenga el
patrón JavaBean (value object)*/
}


También necesitamos crear nuestra base de datos. Podemos usar
cualquier motor; naturalmente necesitaremos su respectivo driver.


Crearemos nuestra tabla CATEGORIA, la cual tendrá la
siguiente estructura:




















Nombre



Tipo



CAT_ID



Autonumérico (clave principal)



CAT_NOMBRE



Varchar(30)



Como se puede ver, hemos puesto nombres de campos distintos a las
propiedades de nuestro JavaBean. Veremos como en iBatis podremos
asociar cada columna con su respectiva propiedad en los objetos.


Configuración de la conexión a la base de datos
desde nuestra aplicación


Crearemos el archivo database.properties dentro del paquete
com.jugperu.tutores.ibatis.resources el cual tendrá los
siguientes valores:


#el driver de nuestra base de datos
jdbc.driver=org.hsqldb.jdbcDriver
#el url para nuestra conexion.
jdbc.url=jdbc:hsqldb:file:data/productos.hsqldb
#el usuario
jdbc.username=sa
#y la contraseña
jdbc.password=


Crearemos el archivo SqlMapConfig.xml que tendrá el
siguiente contenido:


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-config-2.dtd">
<sqlMapConfig>
<properties resource="com/jugperu/tutores/ibatis/resources/database.properties"/>
<transactionManager type="JDBC">
<dataSource type="SIMPLE">
<property name="JDBC.Driver" value="${jdbc.driver}"/>
<property name="JDBC.ConnectionURL" value="${jdbc.url}"/>
<property name="JDBC.Username" value="${jdbc.username}"/>
<property name="JDBC.Password" value="${jdbc.password}"/>
</dataSource>
</transactionManager>
<sqlMap resource="com/jugperu/tutores/ibatis/resources/Almacen.xml"/>
</sqlMapConfig>


El atributo “resource” del elemento “properties”
debe apuntar a la ubicación del archivo .properties que
acabamos de crear. Debe estar en posición relativa a nuestras
clases. Si deseamos utilizar una ubicación que esté
fuera del alcance de la aplicación, utilizaremos el atributo
“url”.


  <properties url="file://c:/proyecto/database.properties"/>


Vemos que las propiedades de las conexión (elementos <property
/>) utiliza variables como ${jdbc.driver}. Esto es porque
está utilizando los valores nuestro archivo
database.properties. El archivo SqlMapConfig.xml solo
puede utilizar un archivo .properties para estos casos.


El elemento <sqlMap/> apunta a un archivo
Almacen.xml. Este archivo lo describiremos a continuación.
Al igual que el elemento <properties /> se puede
especificar un url para apuntar a un recurso que se encuentre fuera
del alcance de la aplicación.


Los demás elementos de este .xml lo detallaremos en el
siguiente capítulo.


Definiendo el mapa de consultas.


Crearemos el archivo Almacen.xml dentro del paquete
com.jugperu.tutores.ibatis.resources el cual tendrá el
siguiente contenido


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap namespace="almacen">
<typeAlias type="com.jugperu.tutores.ibatis.beans.Categoria" alias="categoria"/>
<insert id="insertCategoria" parameterClass="categoria">
insert into CATEGORIA
(cat_nombre)
values (#nombre#)
</insert>
<select id="getCategoriaPorId" resultClass="categoria" parameterClass="int">
select cat_id as id, cat_nombre as nombre
from categoria
where cat_id=#value#
</select>
</sqlMap>


Detallaremos el contenido de este .xml:


El elemento <typeAlias /> nos permitirá
describir un alias para evitar escribir un nombre clase largo. En
este caso, en vez de escribir
"com.jugperu.tutores.ibatis.beans.Categoria"
usaremos “categoria”


Vemos que tiene elementos que se pueden asociar rápidamente
a las sentencias SQL. Todos estos elementos tienen el atributo “id”.
Este nos permitirá identificar a cada uno de ellos.
Detallaremos la estructura de cada uno:


  <insert id="insertCategoria" parameterClass="categoria">
insert into CATEGORIA
(cat_nombre)
values (#nombre#)
</insert>


Este elemento recibe como parámetro un objeto cuyo tipo está
definido en el atributo “resultClass”. Como el nombre
completo de nuestra clase es bien largo, hemos definido un alias con
el elemento <typeAlias />. Por tanto, el tipo del
parámetro es com.jugperu.tutores.ibatis.beans.Categoria.


El comando INSERT debe guardar relación con la sintaxis del
motor que estamos usando. Los valores encerrados en signos numerales
(#) hacen referencia a las propiedades del objeto recibido como
parámetro.


El elemento <select> recibe como parámetro un
valor numérico “int” y devuelve devuelve un objeto
de tipo “categoria”. El parámetro “int”
es un alias predefinido de la clase java.lang.Integer. Por
tanto, al invocar a esta sentencia SQL pasaremos como parámetro
un objeto Integer.


  <select id="getCategoriaPorId" resultClass="categoria" parameterClass="int">
select cat_id as id, cat_nombre as nombre
from categoria
where cat_id=#value#
</select>


En este elemento tiene una sentencia SQL donde los campos que son
seleccionados tienen un alias. Es decir, el campo CAT_ID tiene como
alias “id”, y CAT_NOMBRE tiene como alias “nombre”.
De esta manera iBatis colocará cada campo de la tabla
resultante y los colocará en sus respectivas propiedades del
objeto a devolver.


Programa de prueba


La mejor manera de hacer un programa de prueba es utilizando un
TestCase en JUnit. Crearemos nuestro TestCase
llamándolo IbatisMapsTestCase y tendrá el siguiente
método setUp().


        Reader reader = Resources.getResourceAsReader("SqlMapConfig.xml");
// el .xml para la conexión
sqlMap = SqlMapClientBuilder.buildSqlMapClient(reader);
//construye el manejador de llamadas al iBatis


Crearemos nuestro test para registrar objetos Categorias.


    public void testInsertarCategorias() throws SQLException {
try {
sqlMap.startTransaction();
Categoria c1 = new Categoria();
Categoria c2 = new Categoria();
Categoria c3 = new Categoria();
c1.setNombre("memorias");
c2.setNombre("placas");
c3.setNombre("procesadores");

sqlMap.insert("insertCategoria", c1);
sqlMap.insert("insertCategoria", c2);
sqlMap.insert("insertCategoria", c3);

sqlMap.commitTransaction();
} catch (SQLException ex) {
ex.printStackTrace();
assertTrue(false);
} finally {
sqlMap.endTransaction();

}
}


También haremos un test para obtener un objeto.


    public void testGetCategoriaPorId() throws SQLException {
try {
sqlMap.startTransaction();

Categoria c1 = (Categoria) sqlMap.queryForObject(
"getCategoriaPorId", Integer.valueOf(2));
assertNotNull(c1);
mostrar(c1);

} catch (SQLException ex) {
ex.printStackTrace();
assertTrue(false);
} finally {
sqlMap.endTransaction();
}
}

private static void mostrar(Categoria cat) {
System.out.println(cat.getId() + "\t" + cat.getNombre());
}


Claves autogeneradas


El método “insert” de sqlMap devuelve un objeto
que contiene el valor de la nueva clave generada (si aplica). Pero en
nuestro ejemplo el valor que devuelve es null, a pesar que
hemos declarado que la clave primaria es autonumérica . iBatis
nunca sabrá cual es el valor a menos que pongamos en la
sentencia sql qué valor tiene que devolver.


Editemos Almacen.xml en el
elemento <insert > de tal manera que luzca de la siguiente
manera.


...
<insert id="insertCategoria" parameterClass="categoria">
insert into CATEGORIA
(cat_nombre)
values (#nombre#)
<selectKey resultClass="int">
select distinct identity() as id
from categoria
</selectKey>
</insert>
...


Esta es la sentencia para HSQLDB, que es el motor usado en este
ejemplo. Para MySQL el comando SQL que está dentro de
<selectKey> debería ser:


...
<selectKey resultClass="int">
select last_insert_id() as id
</selectKey>
...


y para SQL Server:


...
<selectKey resultClass="int">
select @@IDENTITY as ID
</selectKey>
...


Manejando resultados


Definiendo mapa de resultados


Hemos visto que para asociar todos los campos de una consulta a
las propiedades del JavaBean, cada uno de estos debe tener un alías.
Lo cual nos puede ser un gran problema, ya que si se tratase de
varios campos, tendríamos que poner varios “as”
por cada uno. Y si fueran varias sentencias SQL, también nos
puede resulta problemático hacer una modificación.


iBatis nos permite crear un mapa de resultado. Allí
podremos definir la asociación que utilizaremos entre
propiedades del objeto y campos de la consulta.


Para ello agregaremos el siguiente elemento en Almacen.xml


  <resultMap id="res-categoria" class="categoria">
<result property="id" column="cat_id"/>
<result property="nombre" column="cat_nombre"/>
</resultMap>


Cada vez que utilicemos como respuesta “res-categoria”,
iBatis se encargará de crear una instancia de “categoria”
y colocará en cada propiedad los valores de cada columna que
se utilice.


Agregaremos una consulta nueva:


  <select id="getAllCategoria" resultMap="res-categoria">
select *
from categoria
</select>


Como se puede ver, ya no es necesario colocar un alias por cada
campo.


Y crearemos un nuevo test para probarlo:


            List lista = sqlMap.queryForList("getAllCategoria", null);
for (Iterator iter = lista.iterator(); iter.hasNext(); ) {
Categoria cat = (Categoria) iter.next();
mostrar(cat);
}


Resultados sin JavaBeans


No necesariamente utilizaremos un JavaBean para obtener un
resultado. Podemos utilizar un java.util.HashMap para obtener
todos los campos de una consulta. Cada key será el
nombre del campo, y su respectivo value será el valor
asociado.


  <resultMap id="res-map-categoria" class="java.util.HashMap">
<result property="id" column="cat_id"/>
<result property="nombre" column="cat_nombre"/>
</resultMap>
...
<select id="getMapCategoriaPorId" resultMap="res-map-categoria">
select *
from categoria
where cat_id=#value#
</select>


El objeto devuelto por queryForObject() será de clase
java.util.HashMap.


            HashMap map = (HashMap) sqlMap.queryForObject(
"getMapCategoriaPorId", Integer.valueOf(1));


Por tanto, podríamos construir una consulta con diversos
campos sin preocuparnos por la estructura que va a devolver.


También podemos obtener solo un campo en una lista.


  <resultMap id="res-categoria-nombre" class="java.lang.String">
<result property="nombre" column="cat_nombre"/>
</resultMap>
...
<select id="getNombresCategoria" resultMap="res-categoria-nombre">
select *
from categoria
</select>


Y cada objeto de la lista obtenida de queryForList() será
un java.lang.String.


            List lista = sqlMap.queryForList("getNombresCategoria", null);


Naturalmente, podríamos crear una consulta que tenga como
resultMap un objeto HashMap e invocarlo con
queryForList() . El resultado será una lista donde cada
elemento será un hashmap.


Manejando parámetros


iBatis también permite definir los parámetros que
van a recibir las consultas. Su estructura es muy similar a los
resultMaps.


  <parameterMap id="par-categoria" class="categoria">
<parameter property="id"/>
<parameter property="nombre"/>
</parameterMap>
//......
<update id="updateCategoria" parameterMap="par-categoria">
update categoria
set cat_nombre=#nombre#
where cat_id=#id#
</update>


También podemos prescindir de un JavaBean para enviar
parámetros. Para ello utilizaremos un java.util.Map.
Cada key se accederá como si fuera una propiedad de un
JavaBean.


  <update id="updateMapCategoria" parameterClass="java.util.Map">
update categoria
set cat_nombre=#nombre#
where cat_id=#id#
</update>


Asociaciones


Uno de los principales problemas del manejo de base de datos
relacionales utilizando objetos es la asociación entre tablas.


Crearemos una tabla PRODUCTO con la siguiente estructura:




























Campo



Tipo



PR_ID



Autonumérico (clave principal)



PR_NOMBRE



Varchar(30)



PR_PRECIO



Decimal



PR_STOCK



Numérico entero



PR_CATEGORIA



Numérico no nulo (clave foránea de CATEGORIA)



Crearemos el JavaBean con sus respectivas propiedades:


package com.jugperu.tutores.ibatis.beans;

public class Producto {
private int id;
private String nombre;
private int stock;
private double precio;
private Categoria categoria;
//.....
}


Agregando un registro


Crearemos un <insert > para manejar la inserción
de objetos a la tabla.


  <typeAlias type="com.jugperu.tutores.ibatis.beans.Producto" alias="producto"/>
//...
<insert id="insertProducto" parameterClass="producto">
insert into producto
(pr_nombre,pr_stock,pr_precio,pr_categoria)
values (#nombre#,#stock#,#precio#,#categoria.id#)
</insert>


Note cómo se accede a una propiedad de Producto
que es de clase Categoria. En caso que la propiedad
categoria fuera nulo, iBatis toma toda la expresión
(categoria.id) como nulo.


Probamos insertar un objeto Producto:


            Categoria cat = (Categoria) sqlMap.queryForObject(
"getCategoriaPorId", Integer.valueOf(1));
Producto p1 = new Producto();
p1.setNombre("Kingston");
p1.setPrecio(300.50);
p1.setStock(5);
p1.setCategoria(cat);

sqlMap.insert("insertProducto", p1);


Es necesario recalcar que iBatis no agrega automáticamente los
objetos asociados que no existan en la base de datos. Es decir, si se
crea un objeto Categoria y se asocia a un nuevo objeto de
Producto, al hacer el insert() solo se guardarán
los valores del objeto Producto y no los del objeto Categoria.


Obteniendo un registro


Para obtener un registro mapeado en
objeto, al <resultMap> se agregará un atributo
más:


  <resultMap id="res-producto" class="producto">
<result property="id" column="pr_id"/>
<result property="nombre" column="pr_nombre"/>
<result property="stock" column="pr_stock"/>
<result property="precio" column="pr_precio"/>
<result property="categoria" column="pr_categoria" select="getCategoriaPorId"/>
</resultMap>


Como se ve, para la propiedad categoria se tomará el
campo pr_categoria y se buscará su valor del select
getCategoriaPorId. Al hacer esto, se invocará a dicho
select y se le pasará como parámetro el valor de
pr_categoria obteniendo el objeto correspondiente.


El select para obtener objetos de
Producto será muy simple:


  <select id="getProductoPorId" resultMap="res-producto">
select *
from producto
where pr_id=#value#
</select>


Y la llamada desde java será la misma que se ha estado
manejando:


            Producto p = (Producto) sqlMap.queryForObject("getProductoPorId",
Integer.valueOf(1));
mostrar(p); //muestra cada campo del objeto “p”
//.....

private static void mostrar(Producto p) {
System.out.println(p.getId() + "\t" + p.getNombre() + "\t" +
p.getPrecio() + "\t" + p.getStock() + "(" +
p.getCategoria().getNombre() + ")");
}


Pero esta solución tiene una deficiencia: para obtener un
producto, iBatis hará dos consultas: uno para el producto y
otro para la categoría. Si fueran varios productos, hará
una consulta por los productos y N consultas por cada producto para
obtener su categoria.


Esto se puede solucionar haciendo un join modificando el
<resultMap >.


  <resultMap id="res-producto-opt" class="producto">
<result property="id" column="pr_id"/>
<result property="nombre" column="pr_nombre"/>
<result property="stock" column="pr_stock"/>
<result property="precio" column="pr_precio"/>
<result property="categoria.id" column="pr_categoria"/>
<result property="categoria.nombre" column="cat_nombre"/>
</resultMap>
....
<select id="getAllProductoOpt" resultMap="res-producto-opt">
select *
from producto p, categoria c
where p.pr_categoria=c.cat_id
</select>


Conclusiones


Con iBatis se puede mapear las consultas que necesitamos para
nuestro proyecto. La sintaxis que se utilice para manejar los
registros de la base de datos está fuertemente aislada en la
lógica de negocio. Esto nos permite tener un código
limpio de sentencias SQL. Si es necesario hacer alguna modificación
en SQL, bastará con editar el XML y no una clase en java
evitando la compilación de esta.