Bonnes vacances à tous!
Il est arrivé si soudainement que le début du deuxième groupe 
«Java Enterprise Developer» a coïncidé avec le 256e jour de l'année. 
Coïncidence? Je ne pense pas.Eh bien, nous partageons l'avant-dernier intérêt: quelles nouveautés JPA 2.2 a-t-il apporté - résultats de streaming, conversion de date améliorée, nouvelles annotations - quelques exemples d'améliorations utiles.
C'est parti!
L'API Java Persistence (JPA) est une spécification Java EE fondamentale largement utilisée dans l'industrie. Que vous développiez pour la plate-forme Java EE ou pour le framework Java alternatif, JPA est votre choix pour sauvegarder les données. JPA 2.1 a amélioré les spécifications, permettant aux développeurs de résoudre des problèmes tels que la génération automatique de schémas de base de données et un travail efficace avec les procédures stockées dans la base de données. La dernière version, JPA 2.2, améliore les spécifications en fonction de ces changements.
Dans cet article, je vais parler de nouvelles fonctionnalités et donner des exemples qui vous aideront à démarrer avec. À titre d'exemple, j'utilise le projet «Java EE 8 Playground», qui est disponible sur 
GitHub . L'exemple d'application est basé sur la spécification Java EE 8 et utilise les cadres JavaServer Faces (JSF), Enterprise JavaBeans (EJB) et JPA pour la persistance. Vous devez être familier avec JPA pour comprendre de quoi il s'agit.
 Utilisation de JPA 2.2
Utilisation de JPA 2.2JPA version 2.2 fait partie de la plate-forme Java EE 8. Il convient de noter que seuls les serveurs d'applications compatibles Java EE 8 fournissent une spécification prête à l'emploi prête à l'emploi. Au moment d'écrire ces lignes (fin 2017), il y avait pas mal de tels serveurs d'applications. Cependant, l'utilisation de JPA 2.2 avec Java EE7 est facile. Vous devez d'abord télécharger les fichiers JAR appropriés à l'aide de 
Maven Central et les ajouter au projet. Si vous utilisez Maven dans votre projet, ajoutez les coordonnées au fichier Maven POM:
<dependency> <groupId>javax.persistence</groupId> <artifactId>javax.persistence-api</artifactId> <version>2.2</version> </dependency> 
Sélectionnez ensuite l'implémentation JPA que vous souhaitez utiliser. Depuis JPA 2.2, EclipseLink et Hibernate ont des implémentations compatibles. Comme exemples dans cet article, j'utilise 
EclipseLink en ajoutant la dépendance suivante:
 <dependency> <groupId>org.eclipse.persistence</groupId> <artifactId>eclipselink</artifactId> <version>2.7.0 </version> </dependency> 
Si vous utilisez un serveur compatible Java EE 8, tel que GlassFish 5 ou Payara 5, vous devriez pouvoir spécifier la zone «fournie» pour ces dépendances dans le fichier POM. Sinon, spécifiez la zone «compiler» pour les inclure dans l'assemblage du projet.
Prise en charge de la date et de l'heure Java 8L'un des ajouts les plus positifs est peut-être la prise en charge de l'API date et heure Java 8. Depuis la sortie de Java SE 8 en 2014, les développeurs ont utilisé des solutions de contournement pour utiliser l'API Date et Heure avec JPA. Bien que la plupart des solutions de contournement soient assez simples, la nécessité d'ajouter une prise en charge de base pour l'API de date et heure mise à jour est attendue depuis longtemps. La prise en charge JPA pour l'API Date et heure inclut les types suivants:
- java.time.LocalDate
- java.time.LocalTime
- java.time.LocalDateTime
- java.time.OffsetTime
- java.time.OffsetDateTime
Pour une meilleure compréhension, je vais d'abord expliquer comment le support de l'API Date et Heure fonctionne sans JPA 2.2. JPA 2.1 ne peut fonctionner qu'avec des constructions de date plus anciennes telles que 
java.util.Date et 
java.sql.Timestamp . Par conséquent, vous devez utiliser un convertisseur pour convertir la date stockée dans la base de données en une ancienne conception prise en charge par JPA 2.1, puis la convertir en une API de date et heure mise à jour pour une utilisation dans l'application. Un convertisseur de date dans JPA 2.1 capable d'une telle conversion peut ressembler à la liste 1. Le convertisseur qu'il 
LocalDate est utilisé pour convertir entre 
LocalDate et 
java.util.Date .
Listing 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 n'a plus besoin d'écrire un tel convertisseur, car vous utilisez des types date-heure pris en charge. La prise en charge de ces types est intégrée, vous pouvez donc simplement spécifier le type pris en charge dans le champ de classe d'entité sans code supplémentaire. L'extrait de code ci-dessous illustre ce concept. Notez qu'il n'est pas nécessaire d'ajouter d'annotation au code 
@Temporal , car le mappage de type se produit automatiquement.
 public class Job implements Serializable { . . . @Column(name = "WORK_DATE") private LocalDate workDate; . . . } 
Étant donné que les types date-heure pris en charge sont des objets de première classe dans le JPA, ils peuvent être spécifiés sans cérémonies supplémentaires. Dans JPA 2.1 
@Temporal annotation doit être décrite dans tous les champs et propriétés constants des 
java.util.Calendar java.util.Date et 
java.util.Calendar .
Il convient de noter que seule une partie des types de données-heure est prise en charge dans cette version, mais le convertisseur d'attribut peut être facilement généré pour fonctionner avec d'autres types, par exemple, pour convertir 
LocalDateTime en 
ZonedDateTime . Le plus gros problème lors de l'écriture d'un tel convertisseur est de déterminer la meilleure façon de convertir entre différents types. Pour rendre les choses encore plus faciles, des convertisseurs d'attributs peuvent désormais être implémentés. Je vais donner un exemple d'implémentation ci-dessous.
Le code du Listing 2 montre comment convertir l'heure de 
LocalDateTime en 
ZonedDateTime .
Listing 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()); } } 
Plus précisément, cet exemple est très simple car 
ZonedDateTime contient des méthodes faciles à convertir. La conversion se produit en appelant la méthode 
toLocalDateTime() . La conversion inverse peut être effectuée en appelant la méthode 
ZonedDateTimeOf() et en transmettant la valeur 
ZoneId avec 
ZoneId pour utiliser le fuseau horaire.
Convertisseurs d'attributs intégrésLes convertisseurs d'attributs étaient un très bon ajout à JPA 2.1, car ils permettaient aux types d'attributs d'être plus flexibles. La mise à jour JPA 2.2 ajoute une capacité utile pour rendre les convertisseurs d'attributs implémentables. Cela signifie que vous pouvez incorporer des ressources Contexts and Dependency Injection (CDI) directement dans le convertisseur d'attributs. Cette modification est cohérente avec d'autres améliorations CDI dans les spécifications Java EE 8, telles que les convertisseurs JSF avancés, car ils peuvent désormais également utiliser l'injection CDI.
Pour profiter de cette nouvelle fonctionnalité, il vous suffit d'incorporer les ressources CDI dans le convertisseur d'attributs, si nécessaire. Le listing 2 fournit un exemple de convertisseur d'attributs, et maintenant je vais le démonter, expliquant tous les détails importants.
La classe de convertisseur doit implémenter l'interface 
javax.persistence.AttributeConverter transmettant les valeurs X et Y. La valeur X correspond au type de données dans l'objet Java et la valeur Y doit correspondre au type de colonne de base de données. Ensuite, la classe du convertisseur doit être annotée avec 
@Converter . Enfin, la classe doit remplacer les 
convertToDatabaseColumn() et 
convertToEntityAttribute() . L'implémentation dans chacune de ces méthodes doit convertir les valeurs de types spécifiques et y revenir.
Pour appliquer automatiquement le convertisseur chaque fois que le type de données spécifié est utilisé, ajoutez «automatique», comme dans 
@Converter(autoApply=true) . Pour appliquer un convertisseur à un seul attribut, utilisez l'annotation @Converter au niveau de l'attribut, comme illustré ici:
 @Convert(converter=LocalDateConverter.java) private LocalDate workDate; 
Le convertisseur peut également être appliqué au niveau de la classe:
 @Convert(attributeName="workDate", converter = LocalDateConverter.class) public class Job implements Serializable { . . . 
Supposons que je veuille chiffrer les valeurs contenues dans le champ 
creditLimit de l'entité 
Customer lors de son enregistrement. Pour implémenter ce processus, les valeurs doivent être chiffrées avant d'être enregistrées et déchiffrées après avoir été récupérées de la base de données. Cela peut être fait par le convertisseur et, en utilisant JPA 2.2, je peux intégrer l'objet de chiffrement dans le convertisseur pour obtenir le résultat souhaité. Le listing 3 en donne un exemple.
Listing 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)); } ... } 
Dans ce code, le processus est effectué en 
CreditLimitEncryptor classe 
CreditLimitEncryptor dans le convertisseur, puis en l'utilisant pour aider le processus.
Résultats des requêtes de streamingDésormais, vous pouvez facilement profiter pleinement des fonctionnalités des flux Java SE 8 lorsque vous travaillez avec des résultats de requête. Les threads simplifient non seulement la lecture, l'écriture et la maintenance du code, mais aident également à améliorer les performances des requêtes dans certaines situations. Certaines implémentations de threads permettent également d'éviter un nombre excessivement élevé de demandes de données simultanées, bien que dans certains cas, l'utilisation de la pagination 
ResultSet puisse fonctionner mieux que les flux.
Pour activer cette fonction, la méthode 
getResultStream() a été ajoutée aux 
TypedQuery Query et 
TypedQuery . Cette modification mineure permet à JPA de renvoyer simplement un flux de résultats au lieu d'une liste. Ainsi, si vous travaillez avec un 
ResultSet volumineux, il est logique de comparer les performances entre une nouvelle implémentation de thread et un 
ResultSets ou une pagination déroulante. La raison en est que les implémentations de threads récupèrent tous les enregistrements à la fois, les stockent dans une liste, puis les renvoient. Un 
ResultSet scrollable et une technique de pagination récupèrent les données au coup par coup, ce qui pourrait être mieux pour les grands ensembles de données.
Les fournisseurs de persistance peuvent décider de remplacer la nouvelle méthode 
getResultStream() une implémentation améliorée. Hibernate inclut déjà une méthode stream () qui utilise un 
ResultSet déroulant pour analyser les résultats des enregistrements au lieu de les renvoyer complètement. Cela permet à Hibernate de travailler avec de très grands ensembles de données et de bien le faire. On peut s'attendre à ce que d'autres fournisseurs remplacent cette méthode pour fournir des fonctionnalités similaires qui sont avantageuses pour JPA.
En plus des performances, la possibilité de diffuser les résultats est un ajout intéressant à JPA, qui fournit un moyen pratique de travailler avec les données. Je vais vous montrer quelques scénarios où cela peut être utile, mais les possibilités elles-mêmes sont infinies. Dans les deux scénarios, je recherche l'entité 
Job et renvoie le flux. Tout d'abord, regardez le code suivant, où j'analyse simplement le flux de 
Jobs rapport à un 
Customer spécifique en appelant la méthode d'interface 
Query getResultStream() . Ensuite, j'utilise ce fil pour afficher les détails concernant le 
customer et 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)); } 
Cette méthode peut être légèrement modifiée afin qu'elle renvoie une liste de résultats à l'aide de la méthode 
Collectors .toList() comme suit.
 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()); } 
Dans le scénario suivant, illustré ci-dessous, je trouve une 
List tâches liées aux pools d'un formulaire spécifique. Dans ce cas, je renvoie toutes les tâches correspondant au formulaire soumis sous forme de chaîne. Semblable au premier exemple, je renvoie d'abord un flux d'enregistrements 
Jobs . Ensuite, je filtre les enregistrements en fonction du formulaire de pool de clients. Comme vous pouvez le voir, le code résultant est très compact et facile à lire.
 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()); } 
Comme je l'ai mentionné précédemment, il est important de se souvenir des performances dans les scénarios où de grandes quantités de données sont retournées. Il existe des conditions dans lesquelles les threads sont plus utiles pour interroger des bases de données, mais il y a aussi ceux où ils peuvent entraîner une dégradation des performances. Une bonne règle de base est que si les données peuvent être interrogées dans le cadre d'une requête SQL, il est logique de le faire. Parfois, les avantages de l'utilisation d'une syntaxe de thread élégante ne l'emportent pas sur les meilleures performances pouvant être obtenues à l'aide du filtrage SQL standard.
Prise en charge des annotations en doubleLorsque Java SE 8 a été publié, des annotations en double sont devenues possibles, vous permettant de réutiliser des annotations dans la déclaration. Certaines situations nécessitent l'utilisation de la même annotation sur une classe ou un champ plusieurs fois. Par exemple, il peut y avoir plus d'une annotation 
@SqlResultSetMapping pour une classe d'entité donnée. Dans les situations où la prise en charge de la ré-annotation est requise, l'annotation de conteneur doit être utilisée. Les annotations en double réduisent non seulement la nécessité d'encapsuler des collections d'annotations identiques dans des annotations de conteneur, mais peuvent également faciliter la lecture du code.
Cela fonctionne comme suit: l'implémentation de la classe d'annotation doit être marquée avec la méta-annotation 
@Repeatable pour indiquer qu'elle peut être utilisée plusieurs fois. La méta annotation 
@Repeatable prend le type de la classe d'annotation de conteneur. Par exemple, la 
NamedQuery annotation 
NamedQuery désormais marquée avec l' 
@Repeatable(NamedQueries.class) . Dans ce cas, l'annotation de conteneur est toujours utilisée, mais vous n'avez pas à y penser lorsque vous utilisez la même annotation sur la déclaration ou la classe, car 
@Repeatable résume ce détail.
Nous donnons un exemple. Si vous souhaitez ajouter plusieurs annotations 
@NamedQuery à une classe d'entité dans JPA 2.1, vous devez les encapsuler dans l'annotation 
@NamedQueries , comme indiqué dans le Listing 4.
Listing 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 { . . . } 
Cependant, dans JPA 2.2, tout est différent. Étant donné que 
@NamedQuery est une annotation en double, elle peut être spécifiée dans la classe d'entité plusieurs fois, comme indiqué dans le listing 5.
Listing 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 { . . . } 
Liste des annotations en double:
- @AssociationOverride
- @AttributeOverride
- @Convert
- @JoinColumn
- @MapKeyJoinColumn
- @NamedEntityGraphy
- @NamedNativeQuery
- @NamedQuery
- @NamedStoredProcedureQuery
- @PersistenceContext
- @PersistenceUnit
- @PrimaryKeyJoinColumn
- @SecondaryTable
- @SqlResultSetMapping
ConclusionLa version JPA 2.2 a quelques changements, mais les améliorations incluses sont importantes. Enfin, le JPA est aligné sur Java SE 8, permettant aux développeurs d'utiliser des fonctionnalités telles que l'API Date et heure, les résultats de requête en streaming et les annotations répétées. Cette version améliore également la cohérence CDI en ajoutant la possibilité d'incorporer des ressources CDI dans les convertisseurs d'attributs. JPA 2.2 est maintenant disponible et fait partie de Java EE 8, je pense que vous aimerez l'utiliser.
LA FIN
Comme toujours, nous attendons vos questions et commentaires.