Introducción
En aplicaciones complejas donde se necesita almacenar y leer objetos de una base de datos es frecuente encontrar implementaciones del Patrón DAO. Aunque cada implementación puede ser distinta, la idea de fondo es la misma: desacoplar la lógica de negocio de la lógica de acceso a datos. Cada implementación del Patrón DAO se debe adaptar a los objetos almacenados en BBDD y a sus clases homólogas (en este ejemplo utilizamos Java); sin embargo, cuando tenemos muchas clases distintas, a menudo se tiende a la repetición de código para cada tipo concreto. Es en este sentido donde el uso de Generics resulta de gran utilidad.
A lo largo del artículo mostramos implementaciones concretas de un número mínimo de métodos donde, sin cambiar ninguna línea, podemos acceder y modificar clases completamente distintas entre sí. Mostramos cómo usar Reflection para realizar Updates en BBDD con la consecuente complejidad de código y cómo Hibernate ayuda a minimizar y simplificar estas operaciones.
Consideraciones iniciales
Supongamos una implementación de DAO que pretenda dar respuesta a las exigencias planteadas como premisa: mínimo número de métodos para acceso a un número arbitrario de clases distintas. Para mayor desacoplamiento podemos proponer que nuestra implementación derive de dos interfaces distintas -quizá inicialmente no queríamos comprometernos a usar Hibernate y en posteriores versiones del código decidimos aprovechar sus virtudes-, con lo que la cabecera de la clase podría tener la forma:
public class AccesoDB implements DAOInterface,DAOHibernateInterface
Donde, cada interfaz se encarga, de forma desacoplada, de exigir métodos distintos que no interfieren entre sí.
public interface DAOInterface {
public <T extends IHasIntID> T selectGenericByAutoID(Class<T> t, int ID);
public <T> List<T> selectGenericFrom(Class<T> t, String criteria);
public <T extends IHasIntID> void updateGenericByID( Class<T> t, int ID, String propertyName, Object value);
}
La interfaz DAOInterface nos indica que debemos implementar tres métodos genéricos que permiten maximizar la reutilización de código. Es importante observar que dos de los métodos piden que la clase T implemente una interfaz:
public <T extends IHasIntID> T selectGenericByAutoID ...
public <T extends IHasIntID> void updateGenericByID ...
Esto es así porque, aunque las clases que utilicemos sean distintas para cada búsqueda, pedimos que implementen una interfaz IHasIntID, que, por simplicidad, sólo quiere indicar que las clases deben tener algún int que sirva como clave para acceder a la BBDD y leer desde ella.
Por otro lado, conociendo la potencia de Hibernate, decidimos que queremos utilizarlo para nuestra capa DAO. Para ello, pedimos a nuestra implementación AccesoDB que también implemente otra interfaz con lo necesario para garantizar que podamos utilizar los SessionFactory de Hibernate, así como métodos auxiliares para construir y cerrar las sesiones.
public interface DAOHibernateInterface {
public SessionFactory buildSessionFactory();
public SessionFactory getSessionFactory();
public void closeFactory();
public void executeHQLQuery(String query);
}
Si estamos convencidos de que vamos a integrar la funcionalidad de Hibernate en nuestro proyecto, entonces podemos crear una única interfaz que integre todos los métodos y usarla como DAO para acceder a ella desde el resto del programa:
public interface DAO {
public <T extends IHasIntID> T selectGenericByAutoID(Class<T> t, int ID);
public <T> List<T> selectGenericFrom(Class<T> t, String criteria);
public <T extends IHasIntID> void updateGenericByID(
Class<T> t, int ID, String propertyName, Object value);
public SessionFactory buildSessionFactory();
public SessionFactory getSessionFactory();
public void closeFactory();
public void executeHQLQuery(String query);
}
Implementación de método para obtener un objeto genérico mediante su ID
Para reducir la extensión del artículo, mostramos como único ejemplo la implementación del método que nos permite obtener objetos desde la base de datos mediante ID. El resto de código puede consultarse en mi repositorio de código.
public <T extends IHasIntID> T selectGenericByAutoID(Class<T> t, int ID) {
T result = null;
Session session = getSessionFactory().openSession();
//Comenzamos Transacción
Transaction transaction =session.beginTransaction();
try {
result =t.cast(session.get(t, ID));
//Commit
transaction.commit();
}
catch (HibernateException hibernateEx) {
try {
transaction.rollback();
} catch(RuntimeException runtimeEx){
System.err.printf("Error en RollBack Transaction", runtimeEx);
}
hibernateEx.printStackTrace();
}
finally {
session.close();
}
return result;
}
Fijémonos que el método no se compromete con ninguna clase concreta, tan sólo exige que implemente la interfaz IHasIntID que, en este ejemplo sencillo no es más que un Getter y un Setter. Es importante comprobar que el método realiza un cast en cada caso, garantizando que por cada tipo t de clase buscada se devuelva el tipo correcto:
result =t.cast(session.get(t, ID));
De esta forma, para este caso, podríamos usar este método para obtener dos clases que no tienen nada que ver entre sí:
@Entity
@Table(name="empleados")
public class Empleado implements IHasIntID{
public Empleado() {}
public Empleado(int idDepartamento, String nombreEmpleado, int sueldo) {
IdDepartamento = idDepartamento;
NombreEmpleado = nombreEmpleado;
Sueldo = sueldo;
}
//@Override
//public String toString()...
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) //Autoincrementado
@Column(name="IdEmpleado")
private int IdEmpleado;
@Column(name="IdDepartamento")
private int IdDepartamento;
@Column(name="NombreEmpleado")
private String NombreEmpleado;
@Column(name="Sueldo")
private int Sueldo;
//Getters Setters...
}
También podríamos utilizar el mismo método para esta otra clase:
@Entity
@Table(name="departamentos")
public class Departamento implements IHasIntID {
public Departamento(){}
public Departamento(int idDep, String nombreDep) {
IdDep = idDep;
NombreDep = nombreDep;
}
//@Override
//public String toString()...
@Id
@Column(name="IdDepartamento")
private int IdDep;
@Column(name="NombreDepartamento")
private String NombreDep;
//Getters Setters..
}
Y así sucesivamente para un numero arbitrario de clases. La única condición es introducir el nombre de las clases en el fichero xml para poder mapearlas correctamente. En dicho fichero también se debe incluir el nombre de la BBDD, el usuario y la contraseña correspondientes.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="hibernate.connection.url">jdbc:mysql://NOMBREDB?useSSL=false&serverTimezone=UTC</property>
<property name="hibernate.connection.username">USER</property>
<property name="hibernate.connection.password">PASS</property>
<property name="hibernate.show_sql">true</property>
<property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
<mapping class="com.zalost.Modelo.Departamento"/>
<mapping class="com.zalost.Modelo.Empleado"/>
</session-factory>
</hibernate-configuration>
Modificación y Update en BBDD mediante Reflection.
Al emplear tipos genéricos surge la dificultad de que cada clase que implementa IHasIntID puede tener un numero distinto de propiedades, cada una de distinto tipo con getters y setters asociados. Usando Reflection, podemos acceder a cada getter y setter conociendo su nombre, realizar los cambios oportunos y devolver la versión actualizada a la base de datos. Una posible implementación sería la siguiente:
public <T extends IHasIntID> void updateGenericByID(
Class<T> t, int ID, String propertyName, Object value){
Session session = getSessionFactory().openSession();
T result = null;
//Comenzamos Transacción
Transaction transaction =session.beginTransaction();
try {
//Obtenemos el objeto desde DB
result = t.cast(session.get(t, ID));
//Cambiamos el valor de la variable usando Reflection
invokeSetter(result, propertyName, value);
//Commit
transaction.commit();
}
catch (HibernateException hibernateEx) {
try {
transaction.rollback();
} catch(RuntimeException runtimeEx){
System.err.printf("Error en RollBack Transaction", runtimeEx);
}
hibernateEx.printStackTrace();
}
finally {
session.close();
}
}
En este caso, hacemos una llamada al setter mediante el método invokeSetter(result, propertyName, value) que actúa sobre el objeto recogido de la base de datos, la propiedad que queremos modificar y el nuevo valor que queremos darle:
//Métodos de reflection
//Información y ejemplos en https://java2blog.com/invoke-getters-setters-using-reflection-java/
//Más información y explicación:
//https://java2blog.com/invoke-getters-setters-using-reflection-java/
private void invokeSetter(Object obj, String propertyName, Object variableValue)
{
PropertyDescriptor descriptor;
try {
descriptor = new PropertyDescriptor(propertyName, obj.getClass());
Method setter = descriptor.getWriteMethod();
try {
setter.invoke(obj,variableValue);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
e.printStackTrace();
}
} catch (IntrospectionException e) {
e.printStackTrace();
}
}
Se puede encontrar información más detallada sobre el método de invocación en el siguiente enlace. Es evidente la complejidad adicional que supone utilizar Reflection para modificar los objetos antes de devolverlos a la BBDD. ¿No sería posible utilizar Hibernate para simplificar este proceso?
Gracias a la sintaxis HQL(Hibernate Query Languaje), podemos realizar updates y otras operaciones CRUD de una forma mucho más sencilla:
public void executeHQLQuery(String query) {
Session session = getSessionFactory().openSession();
//Comenzamos Transacción
Transaction transaction =session.beginTransaction();
try {
//Read
session.createQuery(query).executeUpdate();
//Commit
transaction.commit();
}
catch (HibernateException hibernateEx) {
try {
transaction.rollback();
} catch(RuntimeException runtimeEx){
System.err.printf("Error en RollBack Transaction", runtimeEx);
}
hibernateEx.printStackTrace();
}
finally {
session.close();
}
}
Ahora, sin usar Reflection, podemos realizar Updates directamente escribiendo la query utilizando la implementación de la interfaz DAO:
DAO acceso = DAOManager.getInstance().getAcceso();
acceso.executeHQLQuery(update Departamento set NombreDep='NombreActualizado' where IdDep=2");
De esta manera, Hibernate nos permite ganar flexibilidad y realizar consultas variadas en HQL usando un único método.
Conclusiones
El empleo de tipos genéricos en patrones DAO nos permite minimizar el número de métodos necesarios para realizar operaciones CRUD. Aunque existe la posibilidad de acceder a los miembros de las clases utilizadas en cada caso mediante Reflection, Hibernate permite realizar estas operaciones de forma mucho más sencilla utilizando queries escritas en HQL. Se puede encontrar el código completo de este ejemplo y muchos otros en mi repositorio de código.