Convertidor de tipo de atributo con Java Persistence API

Por Diego Silva
Publicado en Febrero 2016

Java nos permite crear muchos tipos de datos. Pero cuando queremos guardarlos en la base de datos, necesitamos hacer una conversión. Y de manera inversa, cuando queremos obtener un valor de la base de datos, necesitamos convertirlo a nuestro tipo de valor especial.

Menudo trabajo. Optamos o por hacer un convertidor de datos a nivel de DAO, o no usamos nuestra estructura de datos especial.

¿Y si usamos JPA? Calma, calma. La versión JPA 2.1 (que viene incluido en Java EE 7 - JSR 338) tiene un convertidor de tipos para ayudarnos con este problema.

Clase de prueba

Consideraremos que tenemos una clase de prueba que tendrá dos métodos importantes: Uno lista los objetos usando JPA, y otro lista los registros usando JDBC. Además, necesitaremos de un método que haga la persistencia del objeto.

private void listadoObjetos() { 
System.out.println("--- Listado de objetos  ---"); 
TypedQuery<Producto> query = em.createQuery("Select a from Producto a", Producto.class); 
List<Producto> lista = query.getResultList(); 
lista.stream().forEach((p0) -> { 
System.out.println(p0); 
}); 
System.out.println("--- Fin de listado objetos---"); 
} 

private void listadoJDBC() { 
System.out.println("--- Listado según JDBC Nativo ---"); 
try (Connection conn = DriverManager.getConnection("jdbc:hsqldb:file:data/jpademo;ifexists=true", "jpa", "jpa")) { 
String sql = "SELECT * from producto"; 
PreparedStatement stmt = conn.prepareStatement(sql); 
ResultSet rs = stmt.executeQuery(); 
ResultSetMetaData meta = rs.getMetaData(); 
String[] columnNames = new String[meta.getColumnCount()]; 
for(int i=0;i<columnNames.length;i++) 
columnNames[i]=meta.getColumnName(i+1); 
while (rs.next()) { 
for (int i = 0; i < columnNames.length; i++) { 
System.out.print('\t'+columnNames[i]+':'+rs.getString(i+1)); 
} 
System.out.println(); 
} 

} catch (SQLException ex) { 
LOG.log(Level.SEVERE, null, ex); 
} 

System.out.println("--- Fin Listado según JDBC Nativo ---"); 
} 

private void listar() { 
listadoObjetos(); 
listadoJDBC(); 

} 

/** 
* Método que crea la persistencia en el JPA 
* @param object 
*/ 
public void persist(Object object) { 
em.getTransaction().begin(); 
try { 
em.persist(object); 
em.getTransaction().commit(); 
} catch (Exception e) { 
LOG.log(Level.SEVERE, "Error guardando el objeto", e); 
em.getTransaction().rollback(); 
} 
} 

Estos métodos se ejecutarán para ver el contenido después de cada inserción de registros, a fin de comparar entre los objetos creados por el API y los registros obtenidos de la base de datos.

Además, necesitaremos preparar el EntityManager cada vez que se ejecute la prueba, y que se cierre la conexión al terminar. Por eso, necesitamos estos métodos.

@Before 
public void setUp(){ 
EntityManagerFactory emf = Persistence.createEntityManagerFactory("demojpaPU"); 
em = emf.createEntityManager(); 
} 

@After 
public void tearDown(){ 
em.close(); 
} 

 

JPA 2.1

La implementación de JPA que funciona mejor es la de Hibernate, y no la de Eclipselink. Por tanto, necesitaremos que pongamos esta dependencia en nuestro archivo pom.xml:

<dependency> 
<groupId>org.hibernate</groupId> 
<artifactId>hibernate-entitymanager</artifactId> 
<version>4.3.11.Final</version> 
</dependency>

Base de datos

Para este ejemplo, estoy usando una base de datos incrustable llamada HSQLDB (http://hsqldb.org/) que, a mi parecer, es la más práctica para hacer demos.

<dependency> 
<groupId>org.hsqldb</groupId> 
<artifactId>hsqldb</artifactId> 
<version>2.3.3</version>      
</dependency>

Converter básico

Para crear nuestro convertidor necesitamos crear una clase que implemente la interfaz javax.persistence.AttributeConverter y que tenga la anotación @javax.persistence.Converter

Primero, vayamos con lo más fácil:

Tenemos nuestra entidad (una clase JavaBean común y silvestre) con un atributo de tipo boolean y necesitamos que se guarde como tipo Texto. (Dije que comencemos con lo más fácil)

Entonces, aquí tenemos nuestra clase con nuestra propiedad boolean:

@Entity 
public class Producto implements Serializable { 

private static final long serialVersionUID = 1L; 
@Id 
@GeneratedValue(strategy = GenerationType.AUTO) 
private Long id; 

private String descripcion; 

private boolean existe; 
//....


Creamos nuestra clase BooleanStringConverter.java así:

package com.apuntesdejava.app.demo.jpa.converter.helper; 

import javax.persistence.AttributeConverter; 
import javax.persistence.Converter; 

/** 
* 
* @author diego.silva@apuntesdejava.com 
*/ 
@Converter 
public class BooleanStringConverter implements AttributeConverter<Boolean, String> { 

//Dos tipos de valores 

static final String EXISTE = "Existe"; 
static final String NO_EXISTE = "No Existe"; 

/** 
* Convierte el tipo del atributo en un valor válido para una columna de la 
* tabla 
* 
* @param attribute el valor a convertir 
* @return el valor convertido 
*/ 
@Override 
public String convertToDatabaseColumn(Boolean attribute) { 
return attribute ? EXISTE : NO_EXISTE; 
} 

/** 
* Convierte el tipo de dato que viene de la base de datos al tipo de 
* nuestra entidad 
* 
* @param dbData El valor de la base de datos 
* @return El valor devuelto 
*/ 
@Override 
public Boolean convertToEntityAttribute(String dbData) { 
//devuelve TRUE si el valor de la columna es EXISTE 
return dbData.equals(EXISTE); 
} 

}

Este Converter dice: Yo convierto tu data Java Boolean a String para que lo guardes en la base de datos (método convertToDatabaseColumn); y cuando leo de la base de datos la cadena lo convertiré en objeto Java Boolean. (método convertToEntitytAttribyte)

Ahora, agregaremos la siguiente anotación en la entidad para que considere ese converter

@Entity 
public class Productoimplements Serializable{ 

private staticfinal longserialVersionUID = 1L; 
@Id 
@GeneratedValue(strategy = GenerationType.AUTO) 
private Longid; 

@Convert(converter= BooleanStringConverter.class) 
private Stringdescripcion; 

private booleanexiste; 
//.... 

Ahora bien, crearemos la inserción de objetos:

@Test 
public void createInstance() { 

Producto p = new Producto(); 
p.setExiste(true); 
p.setDescripcion("Teclado"); 

persist(p); 

Producto p1 = new Producto(); 
p1.setDescripcion("Monitor"); 
persist(p1); 

listar(); 

}
 

Al ejecutarlo, tendremos el siguiente resultado:

Como se puede ver, el primer listado muestra el valor del atributo convertido en Boolean, pero en el segundo listado aparece como fue guardado en la base de datos, como un VARCHAR.

Converter automático

Ahora bien, también podemos hacer que un converter se auto aplique a todos los tipos de atributo de entidad que tengan el valor que corresponde. Por ejemplo, aumentemos dos propiedades más a nuestra entidad:

@Entity 
 
public class Producto implements Serializable { 
 
private static final long serialVersionUID = 1L; 
@Id 
@GeneratedValue(strategy = GenerationType.AUTO) 
private Long id; 
 
private String descripcion; 
 
@Convert(converter = BooleanStringConverter.class) 
private boolean existe; 
 
private Fecha fechaRenovacion; 
 
private Fecha fechaActualizacion; 
//... 

Crearemos nuestro Converter AttributeConverter

@Converter(autoApply = true) 
public class FechaConverter 
implements AttributeConverter<Fecha, java.sql.Date> { 

@Override 
public Date convertToDatabaseColumn(Fecha attribute) { 
LocalDate localDate = LocalDate.parse(attribute.getDia() 
+ '/' + attribute.getMes() 
+ '/' + attribute.getAnio(), 
DateTimeFormatter.ofPattern("dd/MM/yyyy")); 
Date date = Date.valueOf(localDate); 

return date; 

} 

@Override 
public Fecha convertToEntityAttribute(Date dbData) { 
LocalDate localDate = dbData.toLocalDate(); 
return new Fecha(localDate.format(DateTimeFormatter.ofPattern("dd")), 
localDate.format(DateTimeFormatter.ofPattern("MM")), 
localDate.format(DateTimeFormatter.ofPattern("YYYY"))); 
} 

} 


La clase Fecha es un simple JavaBean que tiene tres atributos de tipo String: día, mes y año.

Notemos la línea marcada, donde se define la anotación @Converter tiene el atributo autoApply = true. Esto quiere decir, que cuando vea en cualquier entidad el tipo Fecha declarado en un atributo, se le aplicará este converter.

Ahora bien, como hemos declarado dos atributos de tipo Fecha en nuestra entidad, a los se aplicará este Converter. Pero, si queremos que uno de ellos no se aplique, le agregamos una notación

@Entity 

public class Producto implements Serializable { 

private static final long serialVersionUID = 1L; 
@Id 
@GeneratedValue(strategy = GenerationType.AUTO) 
private Long id; 

private String descripcion; 

@Convert(converter = BooleanStringConverter.class) 
private boolean existe; 

private Fecha fechaRenovacion; 

@Convert(disableConversion = true) 
private Fecha fechaActualizacion; 
//....


Lo ejecutamos, y este es nuestro resultado:

Se puede ver que, como objeto, obtienen bien los valores convertidos, pero en la base de datos, el campo fechaActualizacion que tiene la notación disabledConversion=true es guardado como objeto serializado. Pero el otro campo, fechaRenovacion que no tiene ninguna anotación, tiene el siguiente resultado:

Lista de objetos

JPA 2.0 nos traía una interesante novedad: si teníamos un atributo que era una lista, podíamos usar la anotación @javax.persistence.ElementCollection El JPA se encargará de crear una tabla adicional relacionada a la tabla de la entidad.

Veamos, agregaremos una lista en nuestra entidad:

@Entity 

public class Producto implements Serializable { 

private static final long serialVersionUID = 1L; 
@Id 
@GeneratedValue(strategy = GenerationType.AUTO) 
private Long id; 

private String descripcion; 

@Convert(converter = BooleanStringConverter.class) 
private boolean existe; 

@ElementCollection 
private List<String> otrosNombres; 
//Se autoaplicará el FechaConverter 
private Fecha fechaRenovacion; 

@Convert(disableConversion = true) 
private Fecha fechaActualizacion; 
//... 

Agregamos los valores en la entidad en nuestra clase de prueba.

@Test 
public void createInstance() { 
Fecha f1 = new Fecha("27", "03", "1976"), 
f2 = new Fecha("23", "08", "2020"); 

Producto p = new Producto(); 
p.setExiste(true); 
p.setDescripcion("Teclado"); 
p.setFechaRenovacion(f1); 
p.setFechaActualizacion(f2); 
p.setOtrosNombres(new ArrayList<>(Arrays.asList(new String[]{"Keyboard", "Dispositivo de entrada"}))); 

persist(p); 

Producto p1 = new Producto(); 
p1.setFechaRenovacion(f2); 
p1.setFechaActualizacion(f1); 
p1.setDescripcion("Monitor"); 
persist(p1); 

listar(); 

} 

Y el resultado es:

Y las tablas creadas en la base de datos son:

Así es como funciona en JPA 2.0, pero si queremos que no cree otra tabla, sino que la lista en Java se convierta en un campo, aquí es donde entra el Converter.

Creamos nuestro converter ListaNombresConverter con el siguiente contenido:

@Converter 
public class ListaNombresConverterimplements AttributeConverter<List<String>, String> { 

@Override 
public StringconvertToDatabaseColumn(List<String>attribute) { 
if (attribute== null) { 
returnnull; 
} 
StringBuilder sb = new StringBuilder(); 
attribute.stream().forEach((attr) -> { 
sb.append(attr).append(','); 
}); 
return sb.toString(); 
} 

@Override 
public List<String> convertToEntityAttribute(StringdbData) { 
if (dbData== null) { 
returnnull; 
} 
String[]splits = dbData.split(","); 
List<String>lista = newArrayList<>(Arrays.asList(splits)); 

return lista; 
} 

} 

Y cambiamos la anotación en la entidad:

@Entity 

public class Producto implements Serializable { 

private static final long serialVersionUID = 1L; 
@Id 
@GeneratedValue(strategy = GenerationType.AUTO) 
private Long id; 

private String descripcion; 

@Convert(converter = BooleanStringConverter.class) 
private boolean existe; 

@Convert(converter = ListaNombresConverter.class) 
private List<String> otrosNombres; 
//Se autoaplicará el FechaConverter 
private Fecha fechaRenovacion; 

@Convert(disableConversion = true) 
private Fecha fechaActualizacion; 
//...

Y al ejecutarlo...

Mapas

También podemos usar atributos de mapas para guardar valores en la clase, y queda automáticamente registrado en la tabla. Aquí veamos los casos

Mapa de valores básicos

Agreguemos este atributo en nuestra entidad, con sus respectivos métodos set y get

//...  
@Convert(converter = MapSobrenombreConverter.class) 
private Map<String, String> nombres; 
//...  

En nuestro Test, será de lo más simple posible:

//… 
//nombres adicionales 
Map<String,String> nombres=new HashMap<>(); 
nombres.put("singular", "Monitor"); 
nombres.put("plural", "Monitores"); 
p1.setNombres(nombres); 
//... 

 

Como necesitamos guardarlo como cadena (que es un tipo bastante manejable en una base de datos) lo guardaremos pero en formato Json; de esta manera aprovechamos el API JSON de Java. Aquí va la clase.

package com.apuntesdejava.app.demo.jpa.converter.helper; 
import java.io.StringReader; 
import java.util.HashMap; 
import java.util.Map; 
import javax.json.Json; 
import javax.json.JsonObject; 
import javax.json.JsonObjectBuilder; 
import javax.json.JsonReader; 
import javax.persistence.AttributeConverter; 
/** 
* Tambien podemos usar mapas para valores simples. Lo haremos un poco más 
* divertido: lo convertiremos en JSon 
* 
* @author diego.silva@apuntesdejava.com 
*/ 
public class MapSobrenombreConverter implements AttributeConverter<Map<String, String>, String> { 
    @Override 
public String convertToDatabaseColumn(Map<String, String> attribute) { 
//creamos nuestro builder de json 
JsonObjectBuilder builder = Json.createObjectBuilder(); 
//recorremos el mapa del atributo de la entidad recibida 
attribute.keySet().stream().forEach((key) -> { 
//obtengo el valor.. 
String value = attribute.get(key); 
//.. y lo guardo en el json 
builder.add(key, value); 
}); 
//finalmente, lo devuelvo como cadena.. y directo a la base de datos 
return builder.build().toString(); 
} 
    @Override 
public Map<String, String> convertToEntityAttribute(String dbData) { 
//preparo mi mapa de valores 
Map<String, String> attr = new HashMap<>(); 
//Preparo el reader de la cadena dbData 
JsonReader reader = Json.createReader(new StringReader(dbData)); 
//Leo la cadena y lo convierto en Json 
JsonObject obj = reader.readObject(); 
//Y recorro la lista de cada atributo del json... 
obj.entrySet().stream().forEach((entry) -> { 
//... para guardarlo en el mapa 
attr.put(entry.getKey(), entry.getValue().toString()); 
}); 
return attr; 
} 
} 

 Al ejecutarlo, este será nuestro resultado:

Mapa de valores complejos

Esto es una variación de la opción anterior. Supongamos una clase adicional Direccion.java

public class Direccion { 
private String calle; 
private String numero; 
private String ciudad; 
private String codigoPostal; 
    public Direccion() { 
} 
    public Direccion(String calle, String numero, String ciudad, String codigoPostal) { 
this.calle = calle; 
this.numero = numero; 
this.ciudad = ciudad; 
this.codigoPostal = codigoPostal; 
} 
// … getters y setters… y toString() 
}

Existiría una DireccionConverter.java. Todo lo transformaremos a JSON que, de por sí, es un formato estándar para guardar estructuras complejas.

package com.apuntesdejava.app.demo.jpa.converter.helper; 
import com.apuntesdejava.app.demo.jpa.converter.entity.Direccion; 
import java.io.StringReader; 
import java.util.HashMap; 
import java.util.Map; 
import javax.json.Json; 
import javax.json.JsonObject; 
import javax.json.JsonObjectBuilder; 
import javax.json.JsonReader; 
import javax.persistence.AttributeConverter; 
/** 
* Usaremos un mapa de objetos, y también lo pondremos en JSON para guardarlo en 
* la base de datos 
* 
* @author diego.silva@apuntesdejava.com 
*/ 
public class DireccionConverter implements AttributeConverter<Map<String, Direccion>, String> { 
    @Override 
public String convertToDatabaseColumn(Map<String, Direccion> attribute) { 
//creamos nuestro builder de json 
JsonObjectBuilder builder = Json.createObjectBuilder(); 
//recorremos el mapa del atributo de la entidad recibida 
if (attribute != null) { 
//obtengo todos los atributos de Direccion            
//y lo guardo en el JSON 
            attribute.keySet().stream().forEach((key) -> { 
//obtengo un objeto 
Direccion direccion = attribute.get(key); 
//.. y lo guardo en el json por cada atributo de Direccion 
JsonObjectBuilder item = Json.createObjectBuilder(); 
item.add("calle", direccion.getCalle()); 
item.add("ciudad", direccion.getCiudad()); 
item.add("codigoPostal", direccion.getCodigoPostal()); 
item.add("numero", direccion.getNumero()); 
                builder.add(key, item); 
}); 
} 
//finalmente, lo devuelvo como cadena.. y directo a la base de datos 
return builder.build().toString(); 
} 
    @Override 
public Map<String, Direccion> convertToEntityAttribute(String dbData) { 
if (dbData == null) { 
return null; 
} 
//preparo mi mapa de valores 
Map<String, Direccion> attr = new HashMap<>(); 
//Preparo el reader de la cadena dbData 
JsonReader reader = Json.createReader(new StringReader(dbData)); 
//Leo la cadena y lo convierto en Json 
JsonObject obj = reader.readObject(); 
//Y recorro la lista de cada atributo del json... 
obj.entrySet().stream().forEach((entry) -> { 
//... para guardarlo en el mapa 
JsonObject item = (JsonObject) entry.getValue(); 
     Direccion dir = new Direccion(item.getString("calle"), 
item.getString("numero"), 
item.getString("ciudad"), 
item.getString("codigoPostal")); 
attr.put(entry.getKey(), dir); 
}); 
return attr; 
} 
} 

En nuestro test agregamos algunos registros.

//… 
Map direcciones1 = new HashMap<>(); 
direcciones1.put("casa", new Direccion("Av. Siempreviva", "742", "Springfield", "1122323")); 
direcciones1.put("trabajo", new Direccion("Reactor nuclear", "0", "Springfield", "4567")); 
p.setDirecciones(direcciones1); 

persist(p); 
//… 

Y el resultado es:

En Java:

Y en JDBC:

 

Código fuente

El código fuente utilizado para este ejemplo está disponible (vía mercurial) en esta dirección.
https://bitbucket.org/apuntesdejava/app-demo-jpa-converter/

Además, lo pueden descargar desde aquí: https://bitbucket.org/apuntesdejava/app-demo-jpa-converter/get/7f7fa0e5338b.zip

Bibliografía

  1. JavaDoc de Java EE 7:
    http://docs.oracle.com/javaee/7/api/javax/persistence/Convert.html

 


Diego Silva. En Noviembre de 2013 obtuvo el Oracle Certified Associate Java SE 7 Programmer. Ha Trabajado para el Estado peruano, desarrollando y llevando a producción sistemas de información en vía web. Actualmente trabaja en una empresa privada desarrollando aplicaciones Java Web con Oracle ADF y con la plataforma Liferay.

Este artículo ha sido revisado por el equipo de productos Oracle y se encuentra en cumplimiento de las normas y prácticas para el uso de los productos Oracle.