Tips, Soluciones y Novedades en Tecnología

19/10/2018

Campos dinámicos con Hibernate





Vamos a ver el tema de los campos dinámicos en Java con Hibernate, muchas herramientas permite que cuando una aplicación se encuentre en producción, este permite agregar campos dinámicos al modelo, he visto este en Python



Pero en este caso que la aplicación se ha implementado con Java y el ORM Hibernate.



Vamos a ver un escenario común.



Tu sistema tiene un modelo como el siguiente.



Modelo Persona

Campos Base:

Id,FirtName,LastPatName,LastMatName,Age,Nro,Email



Suponga que los requerimientos iniciales fue esos campos y que ahora que terminaste de desarrollar e implementar el sistema, la obra maestra se encuentra en producción, genial.



Unos meses después o tras un cambio de gestión el cliente te llama y te dice que como puedo agregarle un campo mas al modelo, y que si puede hacerlo el mismo.



Entonces es cuando nace este problema, de que los modelos que utilizan un ORM en Java es complicado implementar un mantenedor de campos del modelo y que este se interprete por Hibernate y no afecte las consultas y demás acciones.





Tras el escenario descrito vamos a implementar un mecanismo para poder resolver este caso y poder darle a nuestro cliente la autonomía de que pueda agregar nuevos campos al modelo sin molestarnos.





Para abordarlo en Hibernate tenemos que hacer los siguientes pasos:



Paso 1:

Crear un modelo y crear un archivo de mapeo con la clase "Contact" y para hacerlo simple este modelo solo tendrá 2 campos iniciales.



Creamos la clase CustomizableEntity con un atributo Map para poder apilar los campos que sean necesarios.






package com.system.model;

import java.util.HashMap;
import java.util.Map;

public class CustomizableEntity {

private Map customProperties;

public Map getCustomProperties() {
if (customProperties == null) {
customProperties = new HashMap();
}
return customProperties;
}

public void setCustomProperties(Map customProperties) {
this.customProperties = customProperties;
}

public Object getValueOfCustomField(String name) {
return getCustomProperties().get(name);
}

public void setValueOfCustomField(String name, Object value) {
getCustomProperties().put(name, value);
}

}




Posteriormente creamos nuestra clase Contact con sus 2 campos basicos que extienda de la clase anterior.




package com.system.model;

public class Contact extends CustomizableEntity {

private int id;
private String name;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}




Ademas creamos su respectivo archivo de mapeo.






<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping auto-import="true" default-access="property" default-cascade="none" default-lazy="true">
<class abstract="false" name="com.system.model.Contact" table="tbl_contact">
<id column="id" name="id">
<generator class="native"/>
</id>
<property name="name" column="v_name" type="string"/>
<dynamic-component insert="true" name="customProperties" optimistic-lock="true" unique="false" update="true">
</dynamic-component>
</class>
</hibernate-mapping>




Como podemos ver la anotación dynamic-component es la que nos va a permitir hacer la magia de los campos dinámicos. mapeamos a sus respectivos atributos agregado en las clases creadas anteriormente.



Paso 2:



Ahora necesitamos una implementación del EntityManager, como vemos a continuación.






package com.system.hibernate;

import org.hibernate.mapping.Component;

public interface CustomizableEntityManager {

public static String CUSTOM_COMPONENT_NAME = "customProperties";

void addCustomField(String name);

void removeCustomField(String name);

Component getCustomProperties();

Class getEntityClass();
}




Creamos la interface CustomizableEntityManager con un atributo estático que apunte al mismo nombre del atributo de la clase Contact



y posteriormente creamos la clase que implemente la interface creada anteriormente.






package com.system.hibernate;

import java.util.Iterator;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Component;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.mapping.SimpleValue;

public class CustomizableEntityManagerImpl implements CustomizableEntityManager {
private Component customProperties;
private Class entityClass;
public CustomizableEntityManagerImpl(Class entityClass) {
this.entityClass = entityClass;
}
@Override
public Class getEntityClass() {
return entityClass;
}
@Override
public Component getCustomProperties() {
if (customProperties == null) {
Property property = getPersistentClass().getProperty(CUSTOM_COMPONENT_NAME);
customProperties = (Component) property.getValue();
}
return customProperties;
}
@Override
public void addCustomField(String name) {
SimpleValue simpleValue = new SimpleValue();
simpleValue.addColumn(new Column(name));
simpleValue.setTypeName(String.class.getName());
PersistentClass persistentClass = getPersistentClass();
simpleValue.setTable(persistentClass.getTable());
Property property = new Property();
property.setName(name);
property.setValue(simpleValue);
getCustomProperties().addProperty(property);
updateMapping();
}
@Override
public void removeCustomField(String name) {
Iterator propertyIterator = customProperties.getPropertyIterator();
while (propertyIterator.hasNext()) {
Property property = (Property) propertyIterator.next();
if (property.getName().equals(name)) {
propertyIterator.remove();
updateMapping();
return;
}
}
}
private synchronized void updateMapping() {
MappingManager.updateClassMapping(this);
HibernateUtil.getInstance().reset();
}
private PersistentClass getPersistentClass() {
return HibernateUtil.getInstance().getClassMapping(this.entityClass);
}
}




Esta implementación lo que permite es interactuar con la clase en memoria y realizar operaciones de agregar, eliminar y actualizar de un campo especifico a la clase cargada.



Paso 3:



Ahora vamos a crear una clase XMLUtil que permite manipular el XML de mapeo y agregar, eliminar o actualizar segun sea necesario.






package com.system.hibernate;

import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.dom.DOMSource;
import java.io.IOException;
import java.io.FileOutputStream;

public class XMLUtil {

public static void removeChildren(Node node) {
NodeList childNodes = node.getChildNodes();
int length = childNodes.getLength();
for (int i = length - 1; i > -1; i--) {
node.removeChild(childNodes.item(i));
}
}

public static Document loadDocument(String file)
throws ParserConfigurationException, SAXException, IOException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(file);
}

public static void saveDocument(Document dom, String file)
throws TransformerException, IOException {
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, dom.getDoctype().getPublicId());
transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, dom.getDoctype().getSystemId());
DOMSource source = new DOMSource(dom);
StreamResult result = new StreamResult();
FileOutputStream outputStream = new FileOutputStream(file);
result.setOutputStream(outputStream);
transformer.transform(source, result);
outputStream.flush();
outputStream.close();
}
}






Después creamos la clase HibernateUtil para manejar la conexión respectiva.






package com.system.hibernate;

import com.system.model.Contact;
import org.hibernate.*;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.cfg.Configuration;

public class HibernateUtil {

private static HibernateUtil instance;
private Configuration configuration;
private SessionFactory sessionFactory;
private Session session;

public synchronized static HibernateUtil getInstance() {
if (instance == null) {
instance = new HibernateUtil();
}
return instance;
}

private synchronized SessionFactory getSessionFactory() {
if (sessionFactory == null) {
sessionFactory = getConfiguration().buildSessionFactory();
}
return sessionFactory;
}

public synchronized Session getCurrentSession() {
if (session == null) {
session = getSessionFactory().openSession();
session.setFlushMode(FlushMode.COMMIT);
}
return session;
}

private synchronized Configuration getConfiguration() {
if (configuration == null) {
try {
configuration = new Configuration().configure();
configuration.addClass(Contact.class);
} catch (HibernateException e) {
System.out.println("failure");
e.printStackTrace();
}
}
return configuration;
}

public void reset() {
Session session = getCurrentSession();
if (session != null) {
session.flush();
if (session.isOpen()) {
session.close();;
}
}
SessionFactory sf = getSessionFactory();
if (sf != null) {
sf.close();
}
this.configuration = null;
this.sessionFactory = null;
this.session = null;
}

public PersistentClass getClassMapping(Class entityClass) {
return getConfiguration().getClassMapping(entityClass.getName());
}
}




Adicional a este paso necesitamos crear un archivo MappingManager que sera encargado gestionar las operaciones entre la conexión y el archivo XML de mapeo.






package com.system.hibernate;

import com.system.model.Contact;
import org.hibernate.*;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.cfg.Configuration;

public class HibernateUtil {

private static HibernateUtil instance;
private Configuration configuration;
private SessionFactory sessionFactory;
private Session session;

public synchronized static HibernateUtil getInstance() {
if (instance == null) {
instance = new HibernateUtil();
}
return instance;
}

private synchronized SessionFactory getSessionFactory() {
if (sessionFactory == null) {
sessionFactory = getConfiguration().buildSessionFactory();
}
return sessionFactory;
}

public synchronized Session getCurrentSession() {
if (session == null) {
session = getSessionFactory().openSession();
session.setFlushMode(FlushMode.COMMIT);
}
return session;
}

private synchronized Configuration getConfiguration() {
if (configuration == null) {
try {
configuration = new Configuration().configure();
configuration.addClass(Contact.class);
} catch (HibernateException e) {
System.out.println("failure");
e.printStackTrace();
}
}
return configuration;
}

public void reset() {
Session session = getCurrentSession();
if (session != null) {
session.flush();
if (session.isOpen()) {
session.close();;
}
}
SessionFactory sf = getSessionFactory();
if (sf != null) {
sf.close();
}
this.configuration = null;
this.sessionFactory = null;
this.session = null;
}

public PersistentClass getClassMapping(Class entityClass) {
return getConfiguration().getClassMapping(entityClass.getName());
}
}








Paso 5:



Ahora vamos a probar la aplicación y ver como podemos resolver el problema planteado.



Nuestra tabla correspondiente al modelo Contact es:









La clase para hacer la prueba es la siguiente.






package com.system.testing;

import com.system.hibernate.CustomizableEntityManager;
import com.system.hibernate.CustomizableEntityManagerImpl;
import com.system.hibernate.HibernateUtil;
import com.system.model.Contact;
import org.hibernate.Session;
import org.hibernate.Transaction;
import java.io.Serializable;

public class MainTest {

private static final String TEST_FIELD_NAME = "v_city";
private static final String TEST_VALUE = "Paris";

public static void main(String[] args) {
add();
}

/**
* Agrega un registro con los campos establecidos en el modelo
*/
public static void add() {
HibernateUtil.getInstance().getCurrentSession();
Session session = HibernateUtil.getInstance().getCurrentSession();
Transaction tx = session.beginTransaction();
try {
/**
* Instanciamos la clase Contacto
*/
Contact contact = new Contact();
contact.setName("Contact Name 1");
/**
* Guardamos
*/
Serializable id = session.save(contact);
tx.commit();
/**
* Vamos a consultar el registro creado
*/
contact = (Contact) session.get(Contact.class, id);
System.out.println("value = " + contact.getName());

} catch (Exception e) {
tx.rollback();
System.out.println("e = " + e);
}
}

public static void addCampoExtra() {
HibernateUtil.getInstance().getCurrentSession();
CustomizableEntityManager contactEntityManager = new CustomizableEntityManagerImpl(Contact.class);
contactEntityManager.addCustomField(TEST_FIELD_NAME);
Session session = HibernateUtil.getInstance().getCurrentSession();
Transaction tx = session.beginTransaction();
try {
/**
* Instanciamos la clase Contacto
*/
Contact contact = new Contact();
contact.setName("Contact Name 1");

/**
* Agregamos un nuevo campo al modelo *
*/
contact.setValueOfCustomField(TEST_FIELD_NAME, TEST_VALUE);
/**
* Guardamos
*/
Serializable id = session.save(contact);
tx.commit();
/**
* Vamos a consultar el registro creado
*/
contact = (Contact) session.get(Contact.class, id);
Object value = contact.getValueOfCustomField(TEST_FIELD_NAME);
System.out.println("value = " + value);

} catch (Exception e) {
tx.rollback();
System.out.println("e = " + e);
}
}

}






Esta clase de prueba tiene 2 métodos, uno para agregar un registro normal y otro para agregar un campo que no se encuentre en el modelo Contact.



El resultado de ejecutar el método add es:











Ahora ejecutamos el segundo método addExtraCampo y el resultado es:













Este es un resumen práctico de como podemos resolver le problema de tener campos dinámicos en java utilizando Hibernate.



Para implementar las funcionalidades de agregar, modificar y eliminar campos extras de un modelo definido se tiene que implementar mas mecanismos incluido el de serialización y sincronización.





El código del ejemplo se encuentra en GitHub  para que lo puedan probar directamente.













1 comentario: