Felices vacaciones a todos!
Sucedió tan repentinamente que el inicio del segundo grupo
"Java Enterprise Developer" coincidió con el día 256 del año.
¿Coincidencia? No lo creoBueno, compartimos el penúltimo interés: qué cosas nuevas trajo JPA 2.2: resultados de transmisión, conversión de fecha mejorada, nuevas anotaciones, solo algunos ejemplos de mejoras útiles.
Vamos!
La Java Persistence API (JPA) es una especificación fundamental de Java EE que se usa ampliamente en la industria. Independientemente de si está desarrollando para la plataforma Java EE o para el marco de Java alternativo, JPA es su elección para guardar datos. JPA 2.1 mejoró la especificación, permitiendo a los desarrolladores resolver problemas como la generación automática de esquemas de bases de datos y un trabajo eficiente con procedimientos almacenados en la base de datos. La última versión, JPA 2.2, mejora la especificación basada en estos cambios.
En este artículo hablaré sobre nuevas funcionalidades y daré ejemplos que lo ayudarán a comenzar a usarlo. Como muestra, uso el proyecto "Java EE 8 Playground", que está disponible en
GitHub . La aplicación de muestra se basa en la especificación Java EE 8 y utiliza los marcos JavaServer Faces (JSF), Enterprise JavaBeans (EJB) y JPA para la persistencia. Debe estar familiarizado con JPA para comprender de qué se trata.
Usando JPA 2.2JPA versión 2.2 es parte de la plataforma Java EE 8. Vale la pena señalar que solo los servidores de aplicaciones compatibles con Java EE 8 proporcionan una especificación que está lista para usar de inmediato. Al momento de escribir esto (finales de 2017), había bastantes servidores de aplicaciones de este tipo. Sin embargo, usar JPA 2.2 con Java EE7 es fácil. Primero necesita descargar los archivos JAR apropiados usando
Maven Central y agregarlos al proyecto. Si está utilizando Maven en su proyecto, agregue las coordenadas al archivo POM de Maven:
<dependency> <groupId>javax.persistence</groupId> <artifactId>javax.persistence-api</artifactId> <version>2.2</version> </dependency>
Luego, seleccione la implementación JPA que desea usar. Comenzando con JPA 2.2, tanto EclipseLink como Hibernate tienen implementaciones compatibles. Como ejemplos en este artículo, uso
EclipseLink agregando la siguiente dependencia:
<dependency> <groupId>org.eclipse.persistence</groupId> <artifactId>eclipselink</artifactId> <version>2.7.0 </version> </dependency>
Si está utilizando un servidor compatible con Java EE 8, como GlassFish 5 o Payara 5, debería poder especificar el área "proporcionada" para estas dependencias en el archivo POM. De lo contrario, especifique el área de "compilación" para incluirlos en el ensamblaje del proyecto.
Soporte de fecha y hora Java 8Quizás una de las adiciones más positivas es la compatibilidad con Java 8 Date and Time API. Desde el lanzamiento de Java SE 8 en 2014, los desarrolladores han utilizado soluciones alternativas para usar la API de fecha y hora con JPA. Aunque la mayoría de las soluciones son bastante sencillas, la necesidad de agregar soporte básico para la API actualizada de fecha y hora está muy atrasada. El soporte de JPA para la API de fecha y hora incluye los siguientes tipos:
java.time.LocalDate
java.time.LocalTime
java.time.LocalDateTime
java.time.OffsetTime
java.time.OffsetDateTime
Para una mejor comprensión, primero explicaré cómo funciona el soporte de API de fecha y hora sin JPA 2.2. JPA 2.1 solo puede funcionar con construcciones de fecha anteriores como
java.util.Date
y
java.sql.Timestamp
. Por lo tanto, debe usar un convertidor para convertir la fecha almacenada en la base de datos en un diseño antiguo que sea compatible con JPA 2.1, y luego convertirlo en una API de fecha y hora actualizada para usar en la aplicación. Un convertidor de fecha en JPA 2.1 capaz de tal conversión puede parecerse al Listado 1. El convertidor en él se usa para convertir entre
LocalDate
y
java.util.Date
.
Listado 1 @Converter(autoApply = true) public class LocalDateTimeConverter implements AttributeConverter<LocalDate, Date> { @Override public Date convertToDatabaseColumn(LocalDate entityValue) { LocalTime time = LocalTime.now(); Instant instant = time.atDate(entityValue) .atZone(ZoneId.systemDefault()) .toInstant(); return Date.from(instant); } @Override public LocalDate convertToEntityAttribute(Date databaseValue){ Instant instant = Instant.ofEpochMilli(databaseValue.getTime()); return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()).toLocalDate(); } }
JPA 2.2 ya no necesita escribir dicho convertidor, ya que está utilizando tipos de fecha y hora compatibles. El soporte para tales tipos está integrado, por lo que puede simplemente especificar el tipo admitido en el campo de clase de entidad sin código adicional. El fragmento de código siguiente muestra este concepto. Tenga en cuenta que no es necesario agregar anotaciones al código
@Temporal
, porque la asignación de tipos se produce automáticamente.
public class Job implements Serializable { . . . @Column(name = "WORK_DATE") private LocalDate workDate; . . . }
Dado que los tipos de fecha y hora admitidos son objetos de primera clase en el JPA, se pueden especificar sin ceremonias adicionales. En JPA 2.1
@Temporal
anotación debe describirse en todos los campos y propiedades constantes del
java.util.Calendar
java.util.Date
y
java.util.Calendar
.
Vale la pena señalar que solo algunos de los tipos de fecha y hora son compatibles con esta versión, pero el convertidor de atributos se puede generar fácilmente para trabajar con otros tipos, por ejemplo, para convertir
LocalDateTime
a
ZonedDateTime
. El mayor problema al escribir un convertidor de este tipo es determinar la mejor manera de convertir entre diferentes tipos. Para facilitar aún más las cosas, ahora se pueden implementar convertidores de atributos. Daré un ejemplo de implementación a continuación.
El código en el Listado 2 muestra cómo convertir el tiempo de
LocalDateTime
a
ZonedDateTime
.
Listado 2 @Converter public class LocalToZonedConverter implements AttributeConverter<ZonedDateTime, LocalDateTime> { @Override public LocalDateTime convertToDatabaseColumn(ZonedDateTime entityValue) { return entityValue.toLocalDateTime(); } @Override public ZonedDateTime convertToEntityAttribute(LocalDateTime databaseValue) { return ZonedDateTime.of(databaseValue, ZoneId.systemDefault()); } }
Específicamente, este ejemplo es muy sencillo porque
ZonedDateTime
contiene métodos que son fáciles de convertir. La conversión se produce llamando al método
toLocalDateTime()
. La conversión inversa se puede hacer llamando al método
ZonedDateTimeOf()
y pasando el valor
LocalDateTime
junto con
ZoneId
para usar la zona horaria.
Convertidores de atributos integradosLos convertidores de atributos fueron una muy buena adición a JPA 2.1, ya que permitieron que los tipos de atributos fueran más flexibles. La actualización JPA 2.2 agrega una capacidad útil para hacer que los convertidores de atributos sean implementables. Esto significa que puede incrustar recursos de Contextos e Inyección de Dependencias (CDI) directamente en el convertidor de atributos. Esta modificación es coherente con otras mejoras de CDI en las especificaciones Java EE 8, como los convertidores JSF avanzados, ya que ahora también pueden usar la inyección de CDI.
Para aprovechar esta nueva característica, simplemente incruste los recursos CDI en el convertidor de atributos, según sea necesario. El Listado 2 proporciona un ejemplo de un convertidor de atributos, y ahora lo desarmaré, explicando todos los detalles importantes.
La clase del convertidor debe implementar la interfaz
javax.persistence.AttributeConverter
, pasando los valores X e Y. El valor X corresponde al tipo de datos en el objeto Java, y el valor Y debe corresponder al tipo de la columna de la base de datos. Entonces, la clase del convertidor debe ser anotada con
@Converter
. Finalmente, la clase debe anular los
convertToDatabaseColumn()
y
convertToEntityAttribute()
. La implementación en cada uno de estos métodos debe convertir los valores de tipos específicos y volver a ellos.
Para aplicar automáticamente el convertidor cada vez que se usa el tipo de datos especificado, agregue "automático", como en
@Converter(autoApply=true)
. Para aplicar un convertidor a un solo atributo, use la anotación @Converter en el nivel de atributo, como se muestra aquí:
@Convert(converter=LocalDateConverter.java) private LocalDate workDate;
El convertidor también se puede aplicar a nivel de clase:
@Convert(attributeName="workDate", converter = LocalDateConverter.class) public class Job implements Serializable { . . .
Supongamos que quiero cifrar los valores contenidos en el campo
creditLimit
de la entidad
Customer
cuando se guarda. Para implementar este proceso, los valores deben cifrarse antes de guardarse y descifrarse después de recuperarse de la base de datos. El convertidor puede hacer esto y, utilizando JPA 2.2, puedo incrustar el objeto de cifrado en el convertidor para lograr el resultado deseado. El Listado 3 proporciona un ejemplo.
Listado 3 @Converter public class CreditLimitConverter implements AttributeConverter<BigDecimal, BigDecimal> { @Inject CreditLimitEncryptor encryptor; @Override public BigDecimal convertToDatabaseColumn (BigDecimal entityValue) { String encryptedFormat = encryptor.base64encode(entityValue.toString()); return BigDecimal.valueOf(Long.valueOf(encryptedFormat)); } ... }
En este código, el proceso se realiza
CreditLimitEncryptor
clase
CreditLimitEncryptor
en el convertidor y luego usándolo para ayudar al proceso.
Transmisión de resultados de consultasAhora puede aprovechar al máximo las funciones de secuencias de Java SE 8 cuando trabaje con resultados de consultas. Los subprocesos no solo simplifican la lectura, la escritura y el mantenimiento del código, sino que también ayudan a mejorar el rendimiento de las consultas en algunas situaciones. Algunas implementaciones de hilos también ayudan a evitar un número simultáneo excesivamente grande de solicitudes de datos, aunque en algunos casos el uso de la paginación
ResultSet
puede funcionar mejor que las transmisiones.
Para habilitar esta función, se ha agregado el método
getResultStream()
a las
TypedQuery
Query
y
TypedQuery
. Este cambio menor permite que JPA simplemente devuelva una secuencia de resultados en lugar de una lista. Por lo tanto, si está trabajando con un
ResultSet
grande, tiene sentido comparar el rendimiento entre una nueva implementación de subproceso y un
ResultSets
desplazable o paginación. La razón es que las implementaciones de subprocesos recuperan todos los registros a la vez, los almacenan en una lista y luego los devuelven. Un
ResultSet
desplazable y una técnica de paginación recuperan datos poco a poco, lo que podría ser mejor para grandes conjuntos de datos.
Los proveedores de persistencia pueden decidir anular el nuevo método
getResultStream()
una implementación mejorada. Hibernate ya incluye un método stream () que utiliza un
ResultSet
desplazable para analizar los resultados de los registros en lugar de devolverlos por completo. Esto le permite a Hibernate trabajar con conjuntos de datos muy grandes y hacerlo bien. Se puede esperar que otros proveedores anulen este método para proporcionar características similares que sean beneficiosas para JPA.
Además del rendimiento, la capacidad de transmitir resultados es una buena adición a JPA, que proporciona una forma conveniente de trabajar con datos. Demostraré un par de escenarios donde esto puede ser útil, pero las posibilidades en sí mismas son infinitas. En ambos escenarios, consulto la entidad
Job
y devuelvo la secuencia. Primero, mire el siguiente código, donde simplemente analizo la secuencia de
Jobs
contra un
Customer
específico llamando al método de interfaz
Query
getResultStream()
. Luego, uso este hilo para mostrar detalles sobre el
customer
y la
work date
Job'a.
public void findByCustomer(PoolCustomer customer){ Stream<Job> jobList = em.createQuery("select object(o) from Job o " + "where o.customer = :customer") .setParameter("customer", customer) .getResultStream(); jobList.map(j -> j.getCustomerId() + " ordered job " + j.getId() + " - Starting " + j.getWorkDate()) .forEach(jm -> System.out.println(jm)); }
Este método puede modificarse ligeramente para que devuelva una lista de resultados utilizando el
Collectors .toList()
siguiente manera.
public List<Job> findByCustomer(PoolCustomer customer){ Stream<Job> jobList = em.createQuery( "select object(o) from Job o " + "where o.customerId = :customer") .setParameter("customer", customer) .getResultStream(); return jobList.collect(Collectors.toList()); }
En el siguiente escenario, que se muestra a continuación, encuentro una
List
tareas relacionadas con grupos de una forma específica. En este caso, devuelvo todas las tareas que coinciden con el formulario enviado como una cadena. Similar al primer ejemplo, primero devuelvo una secuencia de registros de
Jobs
. Luego, filtro los registros basados en el formulario de grupo de clientes. Como puede ver, el código resultante es muy compacto y fácil de leer.
public List<Job> findByCustPoolShape(String poolShape){ Stream<Job> jobstream = em.createQuery( "select object(o) from Job o") .getResultStream(); return jobstream.filter( c -> poolShape.equals(c.getCustomerId().getPoolId().getShape())) .collect(Collectors.toList()); }
Como mencioné anteriormente, es importante recordar el rendimiento en escenarios donde se devuelven grandes cantidades de datos. Existen condiciones en las que los subprocesos son más útiles para consultar bases de datos, pero también existen aquellas en las que pueden causar una degradación del rendimiento. Una buena regla general es que si los datos pueden consultarse como parte de una consulta SQL, tiene sentido hacerlo. A veces, los beneficios del uso de la elegante sintaxis de subprocesos no superan el mejor rendimiento que se puede lograr con el filtrado SQL estándar.
Soporte de anotaciones duplicadasCuando se lanzó Java SE 8, se hicieron posibles anotaciones duplicadas, lo que le permite reutilizar las anotaciones en la declaración. Algunas situaciones requieren el uso de la misma anotación en una clase o campo varias veces. Por ejemplo, puede haber más de una anotación
@SqlResultSetMapping
para una clase de entidad dada. En situaciones donde se requiere soporte para la re-anotación, se debe usar la anotación de contenedor. Las anotaciones duplicadas no solo reducen el requisito de envolver colecciones de anotaciones idénticas en anotaciones de contenedor, sino que también pueden hacer que el código sea más fácil de leer.
Esto funciona de la siguiente manera: la implementación de la clase de anotación debe marcarse con la
@Repeatable
para indicar que se puede usar más de una vez. La
@Repeatable
toma el tipo de la clase de anotación del contenedor. Por ejemplo, la
NamedQuery
anotación
NamedQuery
ahora
NamedQuery
marcada con la
@Repeatable(NamedQueries.class)
. En este caso, la anotación del contenedor todavía está en uso, pero no tiene que pensar en ello cuando usa la misma anotación en la declaración o clase, porque
@Repeatable
este detalle.
Damos un ejemplo. Si desea agregar más de una anotación
@NamedQuery
a una clase de entidad en JPA 2.1, debe encapsularlas dentro de la anotación
@NamedQueries
, como se muestra en el Listado 4.
Listado 4 @Entity @Table(name = "CUSTOMER") @XmlRootElement @NamedQueries({ @NamedQuery(name = "Customer.findAll", query = "SELECT c FROM Customer c") , @NamedQuery(name = "Customer.findByCustomerId", query = "SELECT c FROM Customer c " + "WHERE c.customerId = :customerId") , @NamedQuery(name = "Customer.findByName", query = "SELECT c FROM Customer c " + "WHERE c.name = :name") . . .)}) public class Customer implements Serializable { . . . }
Sin embargo, en JPA 2.2, todo es diferente. Como
@NamedQuery
es una anotación duplicada, se puede especificar en la clase de entidad más de una vez, como se muestra en el Listado 5.
Listado 5 @Entity @Table(name = "CUSTOMER") @XmlRootElement @NamedQuery(name = "Customer.findAll", query = "SELECT c FROM Customer c") @NamedQuery(name = "Customer.findByCustomerId", query = "SELECT c FROM Customer c " + "WHERE c.customerId = :customerId") @NamedQuery(name = "Customer.findByName", query = "SELECT c FROM Customer c " + "WHERE c.name = :name") . . . public class Customer implements Serializable { . . . }
Lista de anotaciones duplicadas:
@AssociationOverride
@AttributeOverride
@Convert
@JoinColumn
@MapKeyJoinColumn
@NamedEntityGraphy
@NamedNativeQuery
@NamedQuery
@NamedStoredProcedureQuery
@PersistenceContext
@PersistenceUnit
@PrimaryKeyJoinColumn
@SecondaryTable
@SqlResultSetMapping
ConclusiónLa versión 2.2 de JPA tiene algunos cambios, pero las mejoras incluidas son significativas. Finalmente, el JPA está alineado con Java SE 8, lo que permite a los desarrolladores utilizar funciones como la API de fecha y hora, la transmisión de resultados de consultas y la repetición de anotaciones. Esta versión también mejora la consistencia de CDI al agregar la capacidad de incrustar recursos de CDI en convertidores de atributos. JPA 2.2 ya está disponible y es parte de Java EE 8, creo que le gustaría usarlo.
El fin
Como siempre, estamos esperando preguntas y comentarios.