07 noviembre, 2006

Desarrollo de Frameworks

Este artículo está pendiente de revisión

En este capítulo analizaremos la construcción de un framework de ORM para mapear y acceder a las tablas de la base de datos.

Nuestro framework se llama XEntity y se utiliza de la siguiente manera:

Test.java


La Clase XEntity, de alguna manera será comparable a la clase Session de Hibernate. Todo lo que necesitemos nos lo resolverá XEntity, con métodos estáticos.


Comenzando...

Según vemos en el ejemplo, XEntity tiene el método estático:

public static boolean
findByPrimaryKey
(Connection con, XDto dto);

Este método recibe un XDto con valores seteados en los atributos que representan los campos de la primary key de la tabla que el dto representa (la tabla EMP en este caso).

Las preguntas que surgen son:

¿ Como sabe el método findByPrimaryKey
  • cual es la tabla que representa el dto ?
  • cuales atributos del dto son PK ?
Las respuestas a estas preguntas las encontraremos en el código de la clase EmpDTO.

EmpDTO.java


La clase, además de definir los atributos, setters y getters, extiende a la clase base XDto de la que hereda los métodos getPKFields() (abstracto) y getTablename() entre otros.

Por lo tanto esos metodos están siendo sobreescritos. XEntity.findByPrimaryKey() le aplica los métodos al dto y de esa forma obtiene los datos.


La clase XEntity

Veremos ahora la clase XEntity, el método findByPrimaryKey(). Antes ir al código haremos un pequeño resumen de como está resuelto.

El método tiene tres partes principales:
  • Armar el SQL (parametrizado)
  • Setear los parámetros en el PreparedStatement
  • Si la fila existe, setear los atributos en el dto
Genericamente el SQL será así:

SELECT campo1, campo2,..., campoN
FROM nombreTabla
WHERE campoPK1=? AND campoPK2=? AND...

Para poder resolver dinamicamente el SQL necesitamos obtener los nombres de los campos de la tabla, el nombre de la tabla y los campos que constituyen la primary key de la tabla.

Una vez armado el SQL dinámico, podremos crear un PreparedStatement. El próximo desafio será setearle al PreparedStatement los parámetros, ya que los valores a setear están en los atributos del dto. Por lo tanto tendremos que invocarle al dto los getters correspondientes para obtener estos valores.

Con el PreparedStatement creado y con sus parámetros seteados lo podemos ejecutar y así obtendremos un ResultSet. Si el ResultSet tiene filas (esperamos una única fila) entonces hay que obtener cada uno de sus valores y luego invocar los setters del dto para seterlos.

XEntity.java (parte 1)


Lo primero que hacemos en el código es crear el SQL dinámico. El nombre de la tabla lo obtenemos a través del método dto.getTablename(). Para la lista de campos y la condición del WHERE utilizamos métodos privados que analizaremos más adelante.

Con el SQL dinámico creamos el PreparedStatement y ejecutamos el query. Obtenemos el ResultSet.

...continua (parte 2)


El método busca por primary key. Por lo tanto esperamos obtener una única fila (si la pk existe) o ninguna (si la pk no existe).

Si (rs.next() == true) entonces existe la fila. Tenemos que setear en el dto todos los valores contenidos en el ResultSet. Lo hacemos con el método _setearAtributos() que analizaremos más adelante.

Luego de setear los atributos, volvemos a avanzar el ResultSet. Debería devolver false ya que debe existir una única fila para la primary key. Por eso, si devuelve true entonces tiramos una excepción indicando que la tabla está corrupta.

...continua (parte 3)


Por último, cerramos el ResultSet y el PreparedStatement. La conexión no la cerramos porque no es nuestra. Nos la pasaron por parámetro.


Análisis del método findByPrimaryKey (parte 1)

Tenemos pendiente el análisis de los métodos privados que invocamos para resolver el SQL dinámico: _obtenerListaDeCampos() y _obtenerCondicion().

Primero será necesario ver el código de la clase XDto.

XDto.java



Vemos que XDto es una clase abstracta. Quien la extienda deberá sobreescribir el método getPKFields(). Así, los dto (instancias de las subclases de XDto) brindarán información a XEntity sobre cuales campos de la tabla que mapean son PK (primary key).

Pero además del método abstracto, la clase XDto tiene los siguientes métodos: getTablename(), getFields() y toString() (este último heredado de la clase base Object).

El método getTablename() lo resuelve obteniendo el nombre de la clase del dto y pasandoselo a un método en una clase utilitaria UEntity (la cual veremos luego). Lo que hace es convertir un "nombre de clase a un nombre de tabla" según las siguientes convensiones de nombres.


Convensiones de Nombres

Nombres


Si el nombre de la tabla está compuesto por dos a más palabras entonce se asume que las palabras estarán separadas por un caracter "_". Para convertir ese nombre de tabla en un nombre de clase se eliminan los "_" y cada palabra se separa de la siguiente mediante un cambio de minúscula a mayúscula.

Nombres


Para convertir el nombre de un campo a un nombre de atributo el análisis es el mismo. Solo que el nombre de atributo comenzará con minúscula.


Pero en nuestro ejemplo la clase que hereda de XDto se llama EmpDTO y la tabla que mapea es la tabla EMP. Por lo tanto El nombre de la clase no se podrá convertir al nombre de la tabla así que (como ya hemos visto) la clase EmpDTO sobreescribe el método getTablename() y retorna el String "emp";

Si la clase se hubiera llamado Emp en lugar de EmpDTO entonces no se necesitaba subreescribir el método getTablename() ya que (por default) lo hubiera convertido respetando las convensiones estandar.


Ahora analizaremos el método _getFields(). Este método tambien tiene un funcionamiento "por default". Si los atributos y los nombres de los campos de la tabla respetan la convensión de nombres entonces los obtiene introspectando la clase del dto.

Volvamos a ver el código del método para poderlo estudiar mejor.



El método retorna un String[] con los nombres de los campos de la tabla representada por el dto.
Para esto obtiene todos los atributos de la clase (con UBean.getAttributes()), convierte cada nombre de atributo en nombre de campo, los mete en un String[] y lo retorna. Simple.

Claro... ¿Y como obtiene los atributos? Utilizamos una nueva clase utilitaria UBean (que veremos más adelante). El método UBean.getAttributes(Class) introspecta la clase que recibe como parámetro para retornar un String[] con los nombres de sus atributos.

Consideramos atributo a aquella variable de instancia que tenga un set y un get.

Por ejemplo: si la clase tiene una variable empno y dos métodos setEmpno() y getEmpno() entonces empno será considerado como atributo.

El método UBean.getAttributes() obtiene todos los métodos de la clase que recibe como parámetro y verifica: que métodos que comienzan con "set" tienen correspondencia con algún otro método que comience con "get". Es decir: si hay un setEmpno() y hay un getEmpno() entonces empno es un atributo.

Veamos el código.

UBean.java



El método recibe un Class. En nuestro caso será la clase de un dto. Por ejemplo dto.getClass() o EmpDTO.class.

La estrategia de resolución es la siguiente: por reflection obtendremos todos los metodos declarados en la clase (no los heredados). Los que comiencen con "get" los metemos en el vector getters. Los que comiencen con set los metemos en el vector setters. Luego: los elementos del vector getters (sin "get") que también estén en el vector setters (sin "set") serán los atributos de la clase. Los metemos en el vector atts y (luego de pasarlos a un String[]) los retornamos.

Utilizamos el método UString.switchChar() que convierte de mayúscula a minúscula o de minúscula a mayúscula un caracter en un String.


Para la primera parte del estudio de XEntity solo nos queda analizar el método privado _obtenerCondicion(). Veamos el código:



Este método es muy simple. Obtiene los campos de la PK con el método getPKFields() ycontatena un String haciendo "campo1=? AND campo2=?..." y así.

Hasta aquí tenemos resuelto el armado del SQL dinámico. Con esto podemos crear el PreparedStatement.

Volvamos a ver esta parte del código de XEntity.findByPrimaryKey().



Solo nos queda estudiar el método privado _setearParametros().

El objetivo del método _setearParametros() es completar cada uno de los "?" que se encuentran en el SQL dinámico. Recordemos que el SQL es un query por primary key, por lo tanto los "?" representan los valores clave con los cuales se realizará la búsqueda en la tabla. Estos valores están seteados en los atributos del dto que se corresponden con los campos PK de la tabla.

Veamos el código del método.



Para resolver este método volvemos a utilizar la clase UBean. En esta clase tenemos un método invokeGetter() que recibe un dto y un nombre de atributo. Le invoca el getter correspondiente y retorna el resultado. El código lo veremos luego.

Teniendo resuelto el método UBean.invokeGetter(), este método se resuelve de la siguiente manera: obtenemos todos los campos PK. Cada campo lo convertimos a atributo, invocamos su getter y seteamos en el PreparedStatemente el resultado.

Podemos pasar a analizar la segunda parte del método findByPrimaryKey() de la clase XEntity.



Veamos el código del método invokeGetter() de la clase UBean



Primero armamos el nombre del método que vamos a invocar. Recibimos el nombre del atributo entonces el getter correspondiente será "get"+attName, switcheando el primer caracter de attName.

Como es un getter sabemos que no espera recibir argumentos. Esto es importante al momento de pedirle el método a la clase. Le pasamos null para indicar que el método que queremos no recibe argumentos. Luego simplemente invocamos el getter.

Ya que estamos podemos analizar también el método invokeSetter(), también de la clase UBean.



La manera de armar el nombre del método es idéntica. "set"+attName switcheando el primer caracter de attName.

La complejidad extra que tiene este método es que al ser un setter recibe un parámetro. El tema es: de que tipo?

Bueno, como nos están pasando el valor para pasarle al setter, seguramente ese valor será del tipo que el setter espera. Entonces, el Class[] con el que vamos a identificar al setter lo armamos con value.getClass().


Análisis del método findByPrimaryKey (parte 2)

Recordemos el código de la segunda parte del método findByPrimaryKey().



Ya tenemos el ResultSet. Si tiene next() entonces existe al menos una fila identificada con la pk que buscamos. En este caso tenemos que setear el resto de los atributos del dto con los valores que tenemos en el ResultSet. Esto lo hacemos con el método privado _setearAtributos() que analizaremos a continuación.

Si rs no tiene next() entonces retornamos false para indicar que no existe fila para la pk.



En este método obtenemos los campos de la tabla (que son los que están cargados en el ResultSet). Los iteramos. Por cada campo le pedimos su valor al rs e invocamos en el dto el setter correspondiente con la clase utilitaria UBean.


Ejercitación Propuesta

Dejo para el lector el desarrollo de los siguientes métodos en la clase XEntity:

// retorna una coleccion de XDto. Uno por cada fila de la tabla
// Este metodo es aplicable a tablas chicas
public static Collection
findAll(Connection con, XDto dto);

// retorna una coleccion de XDto. Uno por cada fila
// cuyos campos coincidan con los valores seteados en los
// atributos del dto
public static Collection
findByAttribute(Connection con, XDto dto);

// inserta una fila en la tabla con los datos seteados en el dto
public static void
insert(Connection con, XDto dto);

// modifica los datos de la fila identificada por el dto
// tomando los nuevos datos del dto
public static void
update(Connection con, XDto dto);

// borra la fila identificada por el dto
public static void
deleteConnection con, XDto dto);