Convertidor de tipo de atributo con JavaPersistence API
Java nos permite crear muchos tipos de datos. Pero cuando queremos guardarlo 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 convertir de tipos para ayudarnos con este problema.
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 ve que se ejecute la prueba, y que se cierre la conexión al terminar. Por eso, necesitamos estos métodos.
Anteriormente hice un post sobre Base de datos relacionales Java, donde menciono a HSQLDB y Apache Derby. Entre los dos, prefiero el primero.
Primero, vayamos con lo más fácil:
Creamos nuestra clase
Este Converter dice: Yo convierto tu data Java Boolean a String para que lo guardes en la base de datos (método
Ahora, agregaremos la siguiente anotación en la entidad para que considere ese converter
Ahora bien, crearemos la inserción de objetos:
Al ejecutarlo, tendremos el siguiente resultado:
Como se puede ver, el primer listo 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.
Crearemos nuestro Converter
La clase
Notemos la línea 14, donde se define la anotación
Ahora bien, como hemos declarado dos atributos de tipo
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
Agregamos los valores en la entidad en nuestra clase de prueba.
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
Y cambiamos la anotación en la entidad:
Y al ejecutarlo...
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 convertir 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 ve 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 archivopom.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.Anteriormente hice un post sobre Base de datos relacionales Java, donde menciono a HSQLDB y Apache Derby. Entre los dos, prefiero el primero.
<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 interfazjavax.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 comenzaríamos 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 Producto implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @Convert(converter = BooleanStringConverter.class) private String descripcion; private boolean existe; //....
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 listo 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"))); } }
Fecha
es un simple JavaBean que tiene tres atributos de tipo String: dia, mes y año.Notemos la línea 14, 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
En JPA 2.0 nos traía una interesante novedad: si teníamos un atributo que era una lista, podíamos usar la anotación
Veamos, agregaremos una lista en nuestra entidad:
@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 ListaNombresConverter implements AttributeConverter<List<String>, String> { @Override public String convertToDatabaseColumn(List<String> attribute) { if (attribute == null) { return null; } StringBuilder sb = new StringBuilder(); attribute.stream().forEach((attr) -> { sb.append(attr).append(','); }); return sb.toString(); } @Override public List<String> convertToEntityAttribute(String dbData) { if (dbData == null) { return null; } String[] splits = dbData.split(","); List<String> lista = new ArrayList<>(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...
Código fuente
El código fuente lo puedes descargar desde aquí:
Y también puedes examinarlo desde aquí: https://bitbucket.org/apuntesdejava/app-demo-jpa-converter/src/tip
En Oracle Tecnology Network he publicado una documentación más completa:
Como convertir el tipo de atributo con #Java Persistence API (JPA), por Diego Silva: https://t.co/nPRrCcRVPi
— Oracle OTN (@oracleotnla) 26 de febrero de 2016
Bibliografía
- JavaDoc de Java EE 7:
http://docs.oracle.com/javaee/7/api/javax/persistence/Convert.html
Pensé que estaría en el Tutorial Java EE 7, pero no lo encontré.
Comentarios
Publicar un comentario
Si quieres hacer una pregunta más específica, hazla en los foros que tenemos habilitados en Google Groups
Ah! solo se permiten comentarios de usuarios registrados. Si tienes OpenID, bienvenido! Puedes obtener su OpenID, aquí: http://openid.net/