Nouveautés de JPA 2.2

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

JPA 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 8

L'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és

Les 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 streaming

Dé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 double

Lorsque 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

Conclusion

La 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.

Source: https://habr.com/ru/post/fr423195/


All Articles