Introduccion al uso de JPA 2 generico

Anuncio
Introducción al uso de JPA 2.0, Reflection, Annotations, Generics con
Firebird Database en Java 6 Standard Edition
Lic. Guillermo Cherencio – UNLu – Programación III – Base de Datos
En el artículo anterior que escribí para Uds. "Introducción al uso de JPA
2.0 y Swing con Firebird Database en Java 6 Standard Edition" se mostró una
interfaz gráfica hecha en swing utilizando el componente JTable, el cual trabaja
bajo el patrón de diseño MVC (Model View Controller Pattern), a través de la
implementación de la clase ClienteModel vimos como controlar y proveer de
datos a la JTable, permitiendo hacer todo tipo de operaciones utilizando JPA 2.0
(consultas, actualizaciones y bajas). El único problema -a mi entender- es que
esto sirve sólo para la entidad Cliente. Es demasiado específico, poco genérico.
Un desarrollo bajo este diseño implicaría la implementación de una clase model
para cada entidad.
¿De qué forma podemos hacer este código más genérico? Java 6 nos
provee de tres tecnologías que pueden ayudarnos: Anotaciones (Annotations),
Clases Genéricas (Generics) y Reflexión (Reflection).
Reflection
Permite -en tiempo de ejecución- que una clase Java pueda urgar en el
contenido de otra clase Java. Por ejemplo, podemos averiguar qué atributos
tiene, sus tipos de datos, si tienen anotaciones, sus métodos (obtener una
referencia a los mismos para luego ejecutarlos!), etc. Permite acceder a toda
información de metadata vinculada con nuestro código Java sin necesidad de
contar con el código fuente!.
Casi todo el trabajo podemos hacerlo desde la clase Class, toda clase
tiene un atributo estático llamado class que nos permite obtener una referencia
a un objeto de tipo Class, ejemplo: String.class, Integer.class,
Empleado.class, etc.
Otra forma de obtener una referencia de tipo Class, es a través del
nombre completo de una clase y haciendo uso del método estático forName()
de la clase Class:
...
String nombre = "java.lang.String";
Class strClass = null;
try {
Class strClass = Class.forName(nombre);
} catch(ClassNotFoundException cnfe) {
System.err.println("Error, la clase ["+nombre+"]
cargada");
}
...
no
puede
ser
Como es de suponer, ¿Qué pasa si le pasamos un nombre equivocado al
método forName()? forName() lanzará una excepción chequeable1 que se
llama ClassNotFoundException.
Una vez que tenemos una referencia de tipo Class, podemos usar
métodos tales como:
Metodo de la clase Class
getAnnotations()
getConstructors()
getDeclaredFields()
getDeclaredMethods()
getInterfaces()
getName()
newInstance()
Descripción
Devuelve un arreglo con todas las anotaciones
Devuelve un arreglo con todos los constructores de
la clase
Devuelve un arreglo con todos los atributos de la
clase
Devuelve un arreglo con todos los métodos de la
clase
Devuelve un arreglo con todas las interfases
implementadas por la clase
Devuelve el nombre de la clase
Crea una nueva instancia (un objeto) de la clase
Los tipos de estos arreglos permiten -a su vez- invocar métodos propios
que permiten indagar aun más sobre cada tipo de elemento de la clase que
pretendemos analizar.
Annotations
Permite introducir metadata en nuestro código, a nivel de clase, de
atributo, de método, etc. Son algo así como aclaraciones adicionales sobre lo
que pretendemos hacer en la siguiente línea de codigo. Son como comentarios
"activos", que tienen impacto en la compilación, no son ignorados por el
compilador (como sucede con los comentarios). Esta información de metadata
puede persistir dentro de nuestro código para luego estar disponible en runtime
(para que otra clase -utilizando Reflection- pueda acceder a esta información) o
sólo afectar a la compilación, sin dejar rastro dentro de nuestros archivos
compilados (.class). Es muy utilizado en J2EE, pero también esta disponible
en J2SE. Por ejemplo, una entidad -como ya vimos- es una simple clase POJO
(Plain Old Java Object) que actúa como un Java Bean (clase que tiene atributos,
metodos para acceder (getters) y para modificar dichos atributos (setters),
constructor nulo y tambien puede agregar metodos propios (custom)). La
mayoría de las clases tienen esta estructura, entonces, si esta clase debe ser
administrada por un EntityManager para poder ser persistida en una base de
1
Todas las excepciones chequeables son aquellas que derivan de la clase Exception. El
compilador chequeará que el programador aplique la regla "handle & declare": o bien se escribe
un bloque try - catch manipulando la excepción (handle) o bien declarás que dicho método puede
lanzar dicha excepción usando la cláusula throws.
datos utilizando JPA 2.0, ¿cómo sabe JPA 2.0 que esta clase es una entidad?
muy simple: anotándola como Entidad:
@Entity
public class MiEntidad {
...
}
@Entity, en este caso, es una anotación a nivel de clase, que persiste
en runtime y le permite a un proveedor de persistencia JPA 2.0 poder identificar
a una clase como persistente haciendo uso de reflection en runtime. Toda clase
que no este "marcada" con @Entity no podrá ser persistida.
El lenguaje Java ha sido plagado de anotaciones en todos sus paquetes2
que lo componen, viene con cientos de anotaciones predefinidas que permiten
interpretar el código de distinta forma y en el caso de J2EE permite que los
contenedores de clases analicen el codigo en runtime de las clases contenidas
para actuar en función de lo anotado y permitiendo realizar cosas sorprendentes
como "inyectar dependencias"3.
Además de las anotaciones provistas por el lenguaje, podemos crear
nuestras propias anotaciones, sin ningún tipo de restricción!, de forma similar a
como se declaran las interfases. Por ejemplo, podriamos declarar la anotacion
Columna (para luego anotar código como @Columna(nombre="Nombre de
Columna") de la siguiente forma:
...
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Columna {
String nombre() default "";
}
Para definir anotaciones tambien se usan anotaciones!, las cuales,
obviamente se llaman meta-anotaciones, tales como: @Documented,
@Retention, @Target, etc.
@Documented permite indicar que dicha
anotación sea tenida en cuenta cuando se genera la documentación de la clase
usando javadoc4, @Retention indica el tipo de persistencia de la información
2
Toda clase java puede agruparse en paquetes (packages) acorde con la funcionalidad de las
mismas. Por ejemplo, todo el núcleo del lenguaje java se encuentra dentro del package java.lang
allí podemos encontrar clases tales como String, por lo tanto, esta clase, en realidad se llama
java.lang.String y cuando fue implementada, su codigo fuente se ubicaba en la carpeta /java/lang
3
Por ejemplo, podríamos declarar una referencia de tipo EntityManager sin instanciarla y
anotarla de forma tal, que el contenedor pueda localizar una referencia del objeto EntityManager
apropiado y copiar dicha referencia en nuestro código en runtime!. A partir de allí, si bien en mi
clase yo sólo declaré la referencia, la puedo considerar "inyectada" automáticamente por el
contenedor, despreocupándome dicha tarea.
4
Herramienta provista con el J2SDK que permite documentar nuestras clases (al mismo estilo
de la propia API de Java) utilizando unos tags javadoc que se ingresan dentro de comentarios
del tipo /** */ . Estos tags javadoc son muy similares en cuanto a su sintaxis con respecto a las
de metadata, en este caso, quiero que persista dentro del .class para que
pueda ser accedida en runtime por otra clase, @Target permite indicar el
alcance de la anotación! (lo cual afecta a los controles del compilador!, por
ejemplo, si yo pretendo usar esta anotación para anotar a una clase, el
compilador generará un error de compilación, puesto que esta anotación es para
atributos (ElementType.FIELD) y no para clase).
Ejemplo de uso de la anotación Columna:
...
@Entity
public class Cliente {
...
@Columna(nombre="Razón Social")
@Column(nullable=false,length=40)
private String nombre;
...
}
Podría usarse para indicar el título que quiero ver en la columna de la
grilla JTable para visualizar el contenido del atributo nombre de la entidad
Cliente !.
Realmente esta tecnología me ha sorprendido y la considero un enorme
avance en la programación.
Generics
Las clases genéricas son el equivalente Java de los templates de C++.
Esta tecnología está relacionada con los conceptos de upcasting, downcasting y
polimorfismo. Supongamos que tenemos una clase Persona y una clase
Empleado que deriva de Persona. Podemos decir que una operación de
upcasting "es segura", puesto que todo Empleado es una Persona:
...
Persona p;
Empleado e = new Empleado();
p = e;
p.metodoDePersona()
...
No sucede lo mismo con "downcasting" puesto que no toda Persona es
un Empleado:
...
Persona p = new Empleado();
Empleado e = (Empleado) p;
e.metodoDeEmpleado();
...
anotaciones (ejemplo: @author, @since, @version, etc.), pero son sólo comentarios, no
persistentes e ignorados por el compilador. No confundir con anotaciones.
En este caso funciona, puesto que p en realidad apunta a una instancia
de tipo Empleado, pero ello no siempre es asi:
...
Persona p = new Persona();
Empleado e = (Empleado) p;
e.metodoDeEmpleado();
...
// error en runtime!
se genera un error en runtime, ya que el compilador no puede detectar
ninguna violación. Lo ideal es tratar de detectar estos errores en tiempo de
compilación5.
Otro ejemplo de situaciones que pueden llevarnos a errores de
downcasting es el uso de colecciones. El framework Collections fue
programado para almacenar colecciones de objetos de cualquier tipo, por lo
tanto, tuvieron que definirlo en términos de la clase Object (puesto que toda
clase es de tipo Object) para permitir el almacenamiento de cualquier tipo de
objeto. El problema se nos presenta al intentar recuperar los objetos que allí
guardamos, no nos queda otra que hacer downcasting y el compilador no puede
controlar que efectivamente estemos recuperando el tipo de objeto que pusimos
originalmente dentro de la colección!. Ejemplo:
Lista lista = new ArrayList();
Integer i1 = new Integer(12);
Integer i2 = new Integer(1234);
lista.add(i1);
lista.add(i2);
...
// recupero el primer elemento de la lista asumiendo que se trata de un
// Integer
Integer i = (Integer) lista.get(0);
...
El método get() devuelve algo de tipo Object, pero no todo Object es
un Integer !! y encima el compilador no puede ayudarnos!!. Solución: uso de
Generics.
Una clase genérica implica dos cosas: implementarla genéricamente y
luego usarla genéricamente. Volviendo al ejemplo anterior, un uso genérico y por
lo tanto seguro de nuestra operación de downcasting sería el siguiente:
Lista<Integer> lista = new ArrayList<Integer>();
Integer i1 = new Integer(12);
Integer i2 = new Integer(1234);
// ahora el metodo add() recibe algo de tipo Integer en vez de Object
lista.add(i1);
lista.add(i2);
...
// ahora el metodo get() devuelve algo de tipo Integer en vez de Object
Integer i = lista.get(0);
5
Sin perder las ventajas polimórficas del código.
...
Ahora sí el compilador puede controlar que hagamos un uso correcto de
la colección:
Lista<Integer> lista = new ArrayList<Integer>();
Integer i1 = new Integer(12);
Integer i2 = new Integer(1234);
// ahora el metodo add() recibe algo de tipo Integer en vez de Object
lista.add(i1);
lista.add(i2);
...
// ahora el metodo get() devuelve algo de tipo Integer en vez de Object
Empleado e = lista.get(0); // error de compilación!!
...
esto puede lograrse gracias a que el framewrok Collections fue
reescrito en forma genérica, entonces, ahora el metodo add() de la interfase
Collection esta declarado genéricamente:
public interface Collection<E> {
...
boolean add(E e);
...
}
¿Que es <E>? <E> puede ser sustituida -en tiempo de compilación- por
cualquier clase, pero eso sí, el método add() deberá recibir una instancia de
ese mismo tipo que acabo de sustituir. Observe la declaración del método
get() de la interfase List (que deriva de Collection):
public interface List<E> extends Collection {
...
E get(int index);
...
}
Ahora el método get() devolverá algo de un tipo genérico, el mismo que
envié como argumento del método add(), por lo tanto, si invoco al método
add() con un String, el método get() devoverá algo de tipo String, si no
fuese así, el compilador dará un error, advirtiéndonos de nuestro error antes de
ejecutarlo! facilitando el código y haciendo ahorrar mucho tiempo. Maravilloso.
Hay mucho más sobre generics, también se pueden usar expresiones del
tipo <T extends W> en donde también hacemos referencias genericas de
relaciones entre generics, hasta incluso usar <? extends W> o simplemente
<?>.
Juntando todo (Putting all together)
Combinando estas tres tecnologías podemos resolver nuestro problema:
podemos implementar una versión genérica de nuestra específica clase model
ClienteModel, la llamaremos Model<T>, que tendrá este aspecto:
public class Model<T> extends AbstractTableModel implements KeyListener
{
private List<T> lista;
private List<T> listaAlta = new ArrayList<T>();
private String columnas[];
private Class clase_columnas[];
private Method getters[];
private Method setters[];
private T miobj;
....
public Model(final String nomGenerico,List<T> c, ... ) { ... }
...
}
Ahora <T> podría sustituirse por <Cliente> cuando la usemos desde
Main2:
public class Main2 {
...
public static void pantalla() {
...
List<Cliente> clientes = qry.getResultList();
Model<Cliente> cm = new
Model<Cliente>("Cliente",clientes,Main2.em,tabla);
tabla.setModel(cm);
tabla.addKeyListener(cm);
...
}
...
}
Al constructor de la clase Model<T> se le pasa el nombre de la clase
para permitir crear una instancia de la misma (miobj) (puesto que debe
contemplarse la posiblidad de que la lista clientes estuviera vacía), indagarla
utilizando reflection, crear (con la cantidad de elementos apropiada) los arreglos
(cuyas
referencias
estan
previamente
declaradas)
columnas,
clase_columnas, setters, getters, etc.
La indagación hecha en el constructor incluye la búsqueda de la
anotación Columna para determinar el nombre (título) de cada columna6 de la
grilla:
public class Model<T> extends AbstractTableModel implements KeyListener
{
...
public Model(final String nomGenerico,List<T> c,EntityManager
e,JTable t) {
...
// para cada uno de los atributos de la clase hacer:
6
Previamente, el título de cada columna se había asignado con el nombre de cada atributo.
for(Field f : listaClass.getDeclaredFields()) {
...
// determino si la columna es clave primaria y/o esta
// anotada con la anotación Columna
pk_columnas[i]=false;
for(Annotation a:f.getAnnotations()) {
if ( a.toString().startsWith("@javax.persistence.Id(") )
{ pk_columnas[i]=true; }
if ( a.toString().startsWith("@Columna(") ) {
Columna aa = (Columna ) a;
columnas[i]=aa.nombre();
}
}
...
}
}
...
}
Obsérvese que se indaga acerca de las anotaciones que tiene cada
atributo de la clase entidad <T>, la representación en forma de String de cada
anotación incluye su nombre y sus atributos actuales, una vez que nos
aseguramos que el atributo contiene la anotación Columna, obtenemos una
referencia de tipo anotación Columna e invocamos su método nombre() el cual
nos devuelve el título de la columna!.
El resto del código de la clase Model<T> sigue la misma lógica que
ClienteModel, el código es algo más extenso en cuanto a la indagación a
través de reflection (que se realiza en el constructor de Model<T>), pero el resto
del código -hasta incluso- podría ser más simple que ClienteModel, por
ejemplo, en el método setValueAt() y getValueAt() antes debíamos hacer
un switch(col) { } para determinar cuál era el método getter o setter
apropiado invocar, ahora, la versión genérica es más compacta:
public class Model<T> extends AbstractTableModel implements KeyListener
{
...
public Object getValueAt(int row, int col) {
if ( !modoAlta && row > lista.size() ) return null;
if ( modoAlta && row > 0 ) return null;
List<T> l = (!modoAlta) ? lista : listaAlta;
try {
if ( getters[col] != null ) return
getters[col].invoke(l.get(row));
} catch(Exception e) {
System.err.println("Model:Exception ejecutando getter de
columna"+col+":\n"+e.getMessage());
}
return null;
}
public void setValueAt(Object value, int row, int col) {
if ( row > lista.size() ) return;
if ( !modoAlta && col == 0 ) return;
List<T> l = (!modoAlta) ? lista : listaAlta;
if ( !modoAlta ) emt.begin();
try {
if ( setters[col] != null )
setters[col].invoke(l.get(row),value);
} catch(Exception e) {
System.err.println("Model:Exception ejecutando setter de
columna"+col+":\n"+e.getMessage());
}
if ( !modoAlta ) emt.commit();
fireTableCellUpdated(row, col);
}
...
}
Obsérvese que a través de una referencia al método setter o getter
podemos realizar su invocación usando el método invoke(), cuyo primer
argumento es el objeto sobre el cual se va a invocar al método7 y del segundo
argumento en adelante, son los argumentos del método setter o getter.
El proyecto Bluej jpaqry incluye el código explicado en el artículo
anterior así como también el código explicado en este artículo. Para ejecutar la
versión genérica de la grilla, debemos generar el .jar de este proyecto, elegir
los .jar que forman parte de nuestra aplicación (jaybird jdbc driver, jpa 2.0,
eclipseLink (proveedor de jpa 2.0), nuestro código), generarlo en un directorio X,
posicionarse en el directorio X y desde la consola ejecutar la aplicación: java jar jpaqrytest.jar (asumiendo que nuestro código se empaquetó bajo el
nombre jpaqrytest.jar y que se indicó a Main2 como clase ejecutable de
nuestro proyecto). Esta operación ya fue explicada en el primer artículo. El
resultado de la ejecución de Main2 debería ser idéntico a la versión no genérica.
Este proyecto es una muestra elemental de la potencia de combinar estas
tecnologías las cuales nos permiten desacoplar cada vez más nuestro modelo
de objetos del modelo relacional, permitiendo que la misma aplicación pueda
interactuar libremente con distintas implementaciones de la base de datos. Se
podrían continuar agregando más anotaciones y/o propiedades a la actual
anotación Columna8 para "customizar" aún más nuestra grilla sin necesidad de
escribir nuestras clases Model y Controller para realizar altas, bajas y
modificaciones sobre nuestras entidades. Con esto finalizo esta serie de 3
artículos dedicados a JPA 2.0 usado desde J2SE 6, espero que le haya sido útil
para su proyecto y futuros desarrollos.
Atte. Lic. Guillermo Cherencio
7
Esto implica que teniendo una única referencia de tipo Method puedo realizar todas las
invocaciones que desee, sobre distintos objetos.
8
Por ejemplo Ud. podría agregar la propiedad "ancho" o "width" para indicar la cantidad de pixels
de ancho que debería tener inicialmente esta columna en la grilla. También podría modificar
Model<T> para inyectar dependencias o tomar conocimiento de los métodos que tratarán
eventos de persistencia tales como: @PrePersist, @PreUpdate (ver método validar() de la clase
Cliente), @PostLoad, @PostPersist, @PostUpdate (ver método calcular() de la clase Cliente).
Descargar