Schöne Ferien an alle!
Es kam so plötzlich vor, dass der Start der zweiten Gruppe
„Java Enterprise Developer“ mit dem 256. Tag des Jahres zusammenfiel.
Zufall? Ich glaube nicht.Nun, wir teilen das vorletzte Interesse: Welche neuen Dinge brachte JPA 2.2 - Streaming-Ergebnisse, verbesserte Datumskonvertierung, neue Anmerkungen - nur einige Beispiele für nützliche Verbesserungen.
Lass uns gehen!
Die Java Persistence API (JPA) ist eine grundlegende Java EE-Spezifikation, die in der Branche weit verbreitet ist. Unabhängig davon, ob Sie für die Java EE-Plattform oder für das alternative Java-Framework entwickeln, ist JPA Ihre Wahl zum Speichern von Daten. JPA 2.1 hat die Spezifikation verbessert, sodass Entwickler Probleme wie die automatische Generierung von Datenbankschemata und die effiziente Arbeit mit in der Datenbank gespeicherten Prozeduren lösen können. Die neueste Version, JPA 2.2, verbessert die Spezifikation basierend auf diesen Änderungen.
In diesem Artikel werde ich über neue Funktionen sprechen und Beispiele geben, die Ihnen den Einstieg erleichtern. Als Beispiel verwende ich das Projekt „Java EE 8 Playground“, das auf
GitHub verfügbar ist. Die Beispielanwendung basiert auf der Java EE 8-Spezifikation und verwendet für die Persistenz die Frameworks JavaServer Faces (JSF), Enterprise JavaBeans (EJB) und JPA. Sie müssen mit JPA vertraut sein, um zu verstehen, worum es geht.
Verwenden von JPA 2.2JPA Version 2.2 ist Teil der Java EE 8-Plattform. Es ist erwähnenswert, dass nur Java EE 8-kompatible Anwendungsserver eine Spezifikation bereitstellen, die sofort einsatzbereit ist. Zum Zeitpunkt dieses Schreibens (Ende 2017) gab es einige solcher Anwendungsserver. Die Verwendung von JPA 2.2 mit Java EE7 ist jedoch einfach. Zuerst müssen Sie die entsprechenden JAR-Dateien mit
Maven Central herunterladen und dem Projekt hinzufügen. Wenn Sie Maven in Ihrem Projekt verwenden, fügen Sie die Koordinaten zur Maven-POM-Datei hinzu:
<dependency> <groupId>javax.persistence</groupId> <artifactId>javax.persistence-api</artifactId> <version>2.2</version> </dependency>
Wählen Sie dann die JPA-Implementierung aus, die Sie verwenden möchten. Ab JPA 2.2 verfügen sowohl EclipseLink als auch Hibernate über kompatible Implementierungen. Als Beispiele in diesem Artikel verwende ich
EclipseLink, indem ich die folgende Abhängigkeit hinzufüge:
<dependency> <groupId>org.eclipse.persistence</groupId> <artifactId>eclipselink</artifactId> <version>2.7.0 </version> </dependency>
Wenn Sie einen Java EE 8-kompatiblen Server wie GlassFish 5 oder Payara 5 verwenden, sollten Sie in der POM-Datei den Bereich "Bereitgestellt" für diese Abhängigkeiten angeben können. Andernfalls geben Sie den Bereich "Kompilieren" an, um sie in die Projektassembly aufzunehmen.
Java 8 Datums- und UhrzeitunterstützungMöglicherweise ist eine der positivsten Ergänzungen die Unterstützung der Java 8-API für Datum und Uhrzeit. Seit der Veröffentlichung von Java SE 8 im Jahr 2014 haben Entwickler Problemumgehungen verwendet, um die Datums- und Uhrzeit-API mit JPA zu verwenden. Obwohl die meisten Problemumgehungen recht einfach sind, ist die Notwendigkeit, grundlegende Unterstützung für die aktualisierte Datums- und Uhrzeit-API hinzuzufügen, längst überfällig. Die JPA-Unterstützung für die Datums- und Uhrzeit-API umfasst die folgenden Typen:
java.time.LocalDate
java.time.LocalTime
java.time.LocalDateTime
java.time.OffsetTime
java.time.OffsetDateTime
Zum besseren Verständnis werde ich zunächst erläutern, wie die API-Unterstützung für Datum und Uhrzeit ohne JPA 2.2 funktioniert. JPA 2.1 kann nur mit älteren
java.sql.Timestamp
wie
java.util.Date
und
java.sql.Timestamp
. Daher müssen Sie einen Konverter verwenden, um das in der Datenbank gespeicherte Datum in ein altes Design zu konvertieren, das von JPA 2.1 unterstützt wird, und es dann zur Verwendung in der Anwendung in eine aktualisierte Datums- und Uhrzeit-API konvertieren. Ein
LocalDate
in JPA 2.1, der eine solche Konvertierung ermöglicht, sieht möglicherweise wie in Listing 1 aus. Der darin enthaltene Konverter wird zum Konvertieren zwischen
LocalDate
und
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 muss einen solchen Konverter nicht mehr schreiben, da Sie unterstützte Datums- und Uhrzeittypen verwenden. Die Unterstützung für solche Typen ist integriert, sodass Sie den unterstützten Typ einfach ohne zusätzlichen Code im Feld für die Entitätsklasse angeben können. Das folgende Code-Snippet demonstriert dieses Konzept. Beachten Sie, dass dem
@Temporal
Code keine Anmerkungen
@Temporal
müssen, da die
@Temporal
automatisch erfolgt.
public class Job implements Serializable { . . . @Column(name = "WORK_DATE") private LocalDate workDate; . . . }
Da die unterstützten Datums- und Uhrzeittypen in der JPA erstklassige Objekte sind, können sie ohne zusätzliche Zeremonien angegeben werden. In JPA 2.1
@Temporal
Anmerkung in allen konstanten Feldern und Eigenschaften des
java.util.Calendar
java.util.Date
und
java.util.Calendar
.
Es ist erwähnenswert, dass in dieser Version nur ein Teil der
LocalDateTime
ZonedDateTime
. Der Attributkonverter kann jedoch einfach generiert werden, um mit anderen Typen zu arbeiten, z. B. zum Konvertieren von
LocalDateTime
in
ZonedDateTime
. Das größte Problem beim Schreiben eines solchen Konverters besteht darin, zu bestimmen, wie am besten zwischen verschiedenen Typen konvertiert werden kann. Um die Sache noch einfacher zu machen, können jetzt Attributkonverter implementiert werden. Ich werde unten eine Beispielimplementierung geben.
Der Code in Listing 2 zeigt, wie die Zeit von
LocalDateTime
in
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()); } }
Insbesondere ist dieses Beispiel sehr einfach, da
ZonedDateTime
Methoden enthält, die einfach zu konvertieren sind. Die Konvertierung erfolgt durch Aufrufen der
toLocalDateTime()
-Methode. Die inverse Konvertierung kann durchgeführt werden, indem die
ZonedDateTimeOf()
-Methode
ZonedDateTimeOf()
und der
LocalDateTime
Wert zusammen mit
ZoneId
, um die Zeitzone zu verwenden.
Eingebettete AttributkonverterAttributkonverter waren eine sehr schöne Ergänzung zu JPA 2.1, da sie es ermöglichten, Attributtypen flexibler zu gestalten. Das JPA 2.2-Update bietet eine nützliche Möglichkeit, Attributkonverter implementierbar zu machen. Dies bedeutet, dass Sie CDI-Ressourcen (Contexts and Dependency Injection) direkt in den Attributkonverter einbetten können. Diese Änderung steht im Einklang mit anderen CDI-Verbesserungen in den Java EE 8-Spezifikationen, z. B. erweiterten JSF-Konvertern, da diese jetzt auch die CDI-Injektion verwenden können.
Um diese neue Funktion nutzen zu können, binden Sie die CDI-Ressourcen nach Bedarf einfach in den Attributkonverter ein. Listing 2 enthält ein Beispiel für einen Attributkonverter. Jetzt werde ich es auseinander nehmen und alle wichtigen Details erläutern.
Die Konverterklasse muss die Schnittstelle
javax.persistence.AttributeConverter
implementieren und die X- und Y-Werte übergeben. Der X-Wert entspricht dem Datentyp im Java-Objekt und der Y-Wert muss dem Datenbankspaltentyp entsprechen. Dann sollte die Konverterklasse mit
@Converter
kommentiert
@Converter
. Schließlich muss die Klasse die
convertToDatabaseColumn()
und
convertToEntityAttribute()
überschreiben. Die Implementierung in jeder dieser Methoden sollte Werte von bestimmten Typen und zurück zu diesen konvertieren.
Um den Konverter bei jeder Verwendung des angegebenen Datentyps automatisch anzuwenden, fügen Sie wie in
@Converter(autoApply=true)
"
@Converter(autoApply=true)
. Um einen Konverter auf ein einzelnes Attribut anzuwenden, verwenden Sie die Annotation @Converter auf Attributebene, wie hier gezeigt:
@Convert(converter=LocalDateConverter.java) private LocalDate workDate;
Der Konverter kann auch auf Klassenebene angewendet werden:
@Convert(attributeName="workDate", converter = LocalDateConverter.class) public class Job implements Serializable { . . .
Angenommen, ich möchte die im Feld
creditLimit
der
creditLimit
enthaltenen Werte beim
creditLimit
verschlüsseln. Um diesen Prozess zu implementieren, müssen die Werte vor dem Speichern verschlüsselt und nach dem Abrufen aus der Datenbank entschlüsselt werden. Dies kann vom Konverter durchgeführt werden, und mit JPA 2.2 kann ich das Verschlüsselungsobjekt in den Konverter einbetten, um das gewünschte Ergebnis zu erzielen. Listing 3 enthält ein Beispiel.
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)); } ... }
In diesem Code wird der Prozess ausgeführt,
CreditLimitEncryptor
Klasse in den Konverter
CreditLimitEncryptor
und anschließend zur Unterstützung des Prozesses verwendet wird.
Streaming-AbfrageergebnisseJetzt können Sie die Funktionen von Java SE 8-Streams problemlos nutzen, wenn Sie mit Abfrageergebnissen arbeiten. Threads vereinfachen nicht nur das Lesen, Schreiben und Verwalten von Code, sondern verbessern in einigen Situationen auch die Abfrageleistung. Einige Thread-Implementierungen tragen auch dazu bei, eine übermäßig große Anzahl von Datenanforderungen gleichzeitig zu vermeiden, obwohl in einigen Fällen die Verwendung der
ResultSet
Paginierung möglicherweise besser funktioniert als Streams.
Um diese Funktion zu aktivieren, wurde die Methode
getResultStream()
zu den
TypedQuery
Query
und
TypedQuery
hinzugefügt. Diese geringfügige Änderung ermöglicht es JPA, einfach einen Ergebnisstrom anstelle einer Liste zurückzugeben. Wenn Sie also mit einem großen
ResultSet
, ist es sinnvoll, die Leistung zwischen einer neuen Thread-Implementierung und einem scrollbaren
ResultSets
oder einer Paginierung zu vergleichen. Der Grund dafür ist, dass Thread-Implementierungen alle Datensätze auf einmal abrufen, in einer Liste speichern und dann zurückgeben. Eine scrollbare
ResultSet
und Paginierungstechnik ruft Daten stückweise ab, was für große Datenmengen möglicherweise besser ist.
Persistenzanbieter können beschließen, die neue Methode
getResultStream()
verbesserter Implementierung zu überschreiben. Der
ResultSet
enthält bereits eine stream () -Methode, die ein scrollbares
ResultSet
, um die Ergebnisse von Datensätzen zu analysieren, anstatt sie vollständig zurückzugeben. Dadurch kann Hibernate mit sehr großen Datenmengen arbeiten und dies gut tun. Es ist zu erwarten, dass andere Anbieter diese Methode überschreiben, um ähnliche Funktionen bereitzustellen, die für JPA von Vorteil sind.
Neben der Leistung ist die Möglichkeit, Ergebnisse zu streamen, eine nette Ergänzung zu JPA, die eine bequeme Möglichkeit bietet, mit Daten zu arbeiten. Ich werde einige Szenarien demonstrieren, in denen dies nützlich sein kann, aber die Möglichkeiten selbst sind endlos. In beiden Szenarien frage ich die
Job
Entität ab und gebe den Stream zurück. Schauen Sie sich zunächst den folgenden Code an, in dem ich den
Jobs
Stream einfach gegen einen bestimmten
Customer
analysiere, indem ich die Schnittstellenmethode
getResultStream()
von
Query
getResultStream()
. Dann verwende ich diesen Thread, um Details zu
customer
und
work date
Job'a auszugeben.
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)); }
Diese Methode kann leicht geändert werden, sodass mit der
Collectors .toList()
-Methode eine Liste der Ergebnisse wie folgt zurückgegeben wird.
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()); }
In dem folgenden Szenario, das unten gezeigt wird, finde ich eine
List
Aufgaben, die sich auf Pools eines bestimmten Formulars beziehen. In diesem Fall gebe ich alle Aufgaben zurück, die dem als Zeichenfolge übermittelten Formular entsprechen. Ähnlich wie im ersten Beispiel gebe ich zuerst einen Stream von
Jobs
Datensätzen zurück. Dann filtere ich Datensätze basierend auf dem Kundenpoolformular. Wie Sie sehen können, ist der resultierende Code sehr kompakt und leicht zu lesen.
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()); }
Wie bereits erwähnt, ist es wichtig, die Leistung in Szenarien zu berücksichtigen, in denen große Datenmengen zurückgegeben werden. Es gibt Bedingungen, unter denen Threads beim Abfragen von Datenbanken nützlicher sind, aber es gibt auch Bedingungen, unter denen sie zu Leistungseinbußen führen können. Eine gute Faustregel lautet: Wenn Daten im Rahmen einer SQL-Abfrage abgefragt werden können, ist es sinnvoll, genau das zu tun. Manchmal überwiegen die Vorteile der Verwendung einer eleganten Thread-Syntax nicht die beste Leistung, die mit der Standard-SQL-Filterung erzielt werden kann.
Unterstützung für doppelte AnmerkungenMit der Veröffentlichung von Java SE 8 wurden doppelte Anmerkungen möglich, sodass Sie Anmerkungen in der Deklaration wiederverwenden können. In einigen Situationen muss dieselbe Anmerkung für eine Klasse oder ein Feld mehrmals verwendet werden. Beispielsweise kann es für eine bestimmte Entitätsklasse mehr als eine
@SqlResultSetMapping
Annotation geben. In Situationen, in denen die Unterstützung für eine erneute Annotation erforderlich ist, sollte eine Container-Annotation verwendet werden. Doppelte Anmerkungen reduzieren nicht nur die Notwendigkeit, Sammlungen identischer Anmerkungen in Containeranmerkungen zu verpacken, sondern können auch das Lesen von Code erleichtern.
Dies funktioniert wie folgt: Die Implementierung der Annotation-Klasse sollte mit der Meta-Annotation
@Repeatable
gekennzeichnet werden, um anzuzeigen, dass sie mehrmals verwendet werden kann. Die
@Repeatable
nimmt den Typ der Containeranmerkungsklasse an. Beispielsweise ist die Annotationsklasse
NamedQuery
jetzt mit der Annotation
@Repeatable(NamedQueries.class)
. In diesem Fall wird die Container-Annotation noch verwendet, aber Sie müssen nicht darüber nachdenken, wenn Sie dieselbe Annotation für die Deklaration oder Klasse verwenden, da
@Repeatable
dieses Detail abstrahiert.
Wir geben ein Beispiel. Wenn Sie einer Entitätsklasse in JPA 2.1 mehr als eine
@NamedQuery
Annotation hinzufügen möchten, müssen Sie diese in die
@NamedQueries
Annotation
@NamedQueries
, wie in Listing 4 gezeigt.
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 { . . . }
In JPA 2.2 ist jedoch alles anders. Da
@NamedQuery
eine doppelte Annotation ist, kann sie in der Entitätsklasse mehrmals angegeben werden, wie in Listing 5 gezeigt.
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 der doppelten Anmerkungen:
@AssociationOverride
@AttributeOverride
@Convert
@JoinColumn
@MapKeyJoinColumn
@NamedEntityGraphy
@NamedNativeQuery
@NamedQuery
@NamedStoredProcedureQuery
@PersistenceContext
@PersistenceUnit
@PrimaryKeyJoinColumn
@SecondaryTable
@SqlResultSetMapping
FazitDie JPA 2.2-Version enthält einige Änderungen, die enthaltenen Verbesserungen sind jedoch erheblich. Schließlich ist der JPA auf Java SE 8 ausgerichtet, sodass Entwickler Funktionen wie die Datums- und Uhrzeit-API verwenden, Abfrageergebnisse streamen und Anmerkungen wiederholen können. Diese Version verbessert auch die CDI-Konsistenz, indem die Möglichkeit hinzugefügt wird, CDI-Ressourcen in Attributkonverter einzubetten. JPA 2.2 ist jetzt verfügbar und Teil von Java EE 8. Ich denke, Sie werden es gerne verwenden.
DAS ENDE
Wie immer warten wir auf Fragen und Kommentare.