Spring Data JPA: Was ist gut und was ist schlecht?

Der kleine Sohn kam zu seinem Vater
Und fragte das Baby
- Was ist gut?
und was ist schlecht

Vladimir Mayakovsky


In diesem Artikel geht es um Spring Data JPA, nämlich in dem Unterwasserschwader, den ich auf meinem Weg getroffen habe, und natürlich ein wenig um Leistung.


Die im Artikel beschriebenen Beispiele können in der Testumgebung ausgeführt werden, auf die über Referenz zugegriffen werden kann .


Hinweis für diejenigen, die noch nicht zu Spring Boot 2 gewechselt sind

In Versionen von Spring Data JPA 2. * wurde die Hauptschnittstelle für die Arbeit mit Repositorys, nämlich CrudRepository , von dem JpaRepository geerbt wird, JpaRepository . In Version 1. * sahen die Hauptmethoden folgendermaßen aus:


 public interface CrudRepository<T, ID> { T findOne(ID id); List<T> findAll(Iterable<ID> ids); } 

In neuen Versionen:


 public interface CrudRepository<T, ID> { Optional<T> findById(ID id); List<T> findAllById(Iterable<ID> ids); } 

Also fangen wir an.


Wählen Sie t. * aus t, wobei t.id in (...)


Eine der häufigsten Abfragen ist eine Abfrage der Form "Alle Datensätze auswählen, für die der Schlüssel in den übertragenen Satz fällt". Ich bin sicher, fast alle von Ihnen haben so etwas geschrieben oder gesehen


 @Query("select ba from BankAccount ba where ba.user.id in :ids") List<BankAccount> findByUserIds(@Param("ids") List<Long> ids); @Query("select ba from BankAccount ba where ba.user.id in :ids") List<BankAccount> findByUserIds(@Param("ids") Set<Long> ids); 

Dies sind funktionierende, geeignete Anforderungen, es gibt keine Fang- oder Leistungsprobleme, aber es gibt einen kleinen, völlig unauffälligen Nachteil.


Versuchen Sie, selbst zu denken, bevor Sie den Liner öffnen.

Der Nachteil ist, dass die Schnittstelle zu eng ist, um Schlüssel zu übertragen. "Na und?" - Du sagst. "Nun, die Liste, nun das Set, ich sehe hier kein Problem." Wenn wir uns jedoch die Methoden der Root-Schnittstelle ansehen, die viele Werte Iterable sehen wir überall Iterable :


"Na und? Und ich möchte eine Liste. Warum ist es schlimmer?"
Nicht schlimmer, seien Sie einfach darauf vorbereitet, dass in Ihrer Anwendung ähnlicher Code auf einer höheren Ebene angezeigt wird:


 public List<BankAccount> findByUserId(List<Long> userIds) { Set<Long> ids = new HashSet<>(userIds); return repository.findByUserIds(ids); } // public List<BankAccount> findByUserIds(Set<Long> userIds) { List<Long> ids = new ArrayList<>(userIds); return repository.findByUserIds(ids); } 

Dieser Code macht nichts anderes, als die Sammlungen umzukehren. Es kann sich herausstellen, dass das Argument für die Methode eine Liste ist und die Repository-Methode die Menge akzeptiert (oder umgekehrt), und Sie müssen sie nur neu anordnen, um die Kompilierung zu bestehen. Dies wird natürlich vor dem Hintergrund der Gemeinkosten für die Anfrage selbst kein Problem sein, sondern es geht eher um unnötige Gesten.


Daher Iterable es sich, Iterable zu verwenden:


 @Query("select ba from BankAccount ba where ba.user.id in :ids") List<BankAccount> findByUserIds(@Param("ids") Iterable<Long> ids); 

Z.Y. Wenn es sich um eine Methode aus *RepositoryCustom , ist es sinnvoll, Collection zu verwenden, um die Berechnung der Größe innerhalb der Implementierung zu vereinfachen:


 public interface BankAccountRepositoryCustom { boolean anyMoneyAvailable(Collection<Long> accountIds); } public class BankAccountRepositoryImpl { @Override public boolean anyMoneyAvailable(Collection<Long> accountIds) { if (ids.isEmpty()) return false; //... } } 

Zusätzlicher Code: nicht doppelte Schlüssel


In Fortsetzung des letzten Abschnitts möchte ich auf ein häufiges Missverständnis aufmerksam machen:


 @Query("select ba from BankAccount ba where ba.user.id in :ids") List<BankAccount> findByUserIds(@Param("ids") Set<Long> ids); 

Andere Manifestationen des gleichen Fehlers:


 Set<Long> ids = new HashSet<>(notUniqueIds); List<BankAccount> accounts = repository.findByUserIds(ids); List<Long> ids = ts.stream().map(T::id).distinct().collect(toList()); List<BankAccount> accounts = repository.findByUserIds(ids); Set<Long> ids = ts.stream().map(T::id).collect(toSet()); List<BankAccount> accounts = repository.findByUserIds(ids); 

Auf den ersten Blick nichts Ungewöhnliches, oder?


Nehmen Sie sich Zeit, denken Sie selbst;)

HQL / JPQL-Abfragen des Formulars select t from t where t.field in ... schließlich zu einer Abfrage wird


 select b.* from BankAccount b where b.user_id in (?, ?, ?, ?, ?, …) 

Dies wird immer das Gleiche zurückgeben, unabhängig davon, ob Wiederholungen im Argument vorhanden sind. Daher ist es nicht erforderlich, die Eindeutigkeit der Schlüssel sicherzustellen. Es gibt einen Sonderfall - Oracle, bei dem das Drücken von> 1000 Eingaben zu einem Fehler führt. Wenn Sie jedoch versuchen, die Anzahl der Schlüssel durch Ausschließen von Wiederholungen zu verringern, sollten Sie lieber über den Grund für deren Auftreten nachdenken. Höchstwahrscheinlich liegt der Fehler irgendwo oben.


Verwenden Iterable also in gutem Code Iterable :


 @Query("select ba from BankAccount ba where ba.user.id in :ids") List<BankAccount> findByUserIds(@Param("ids") Iterable<Long> ids); 

Samopis


Schauen Sie sich diesen Code genau an und finden Sie hier drei Fehler und einen möglichen Fehler:


 @Query("from User u where u.id in :ids") List<User> findAll(@Param("ids") Iterable<Long> ids); 

Denken Sie noch etwas nach
  • Alles ist bereits in SimpleJpaRepository::findAllById
  • Leerlaufanforderung beim Übergeben einer leeren Liste (in SimpleJpaRepository::findAllById gibt es eine entsprechende Prüfung)
  • Alle mit @Query beschriebenen @Query werden in der Phase des @Query überprüft, was SimpleJpaRepository::findAllById Zeit in SimpleJpaRepository::findAllById nimmt (im Gegensatz zu SimpleJpaRepository::findAllById ).
  • Wenn Oracle verwendet wird und die Schlüsselsammlung leer ist, wird der Fehler ORA-00936: missing expression (der bei Verwendung von SimpleJpaRepository::findAllById nicht SimpleJpaRepository::findAllById , siehe Punkt 2).

Harry Potter und zusammengesetzter Schlüssel


Schauen Sie sich zwei Beispiele an und wählen Sie Ihr bevorzugtes aus:


Methodennummer mal


 @Embeddable public class CompositeKey implements Serializable { Long key1; Long key2; } @Entity public class CompositeKeyEntity { @EmbeddedId CompositeKey key; } 

Methode Nummer zwei


 @Embeddable public class CompositeKey implements Serializable { Long key1; Long key2; } @Entity @IdClass(value = CompositeKey.class) public class CompositeKeyEntity { @Id Long key1; @Id Long key2; } 

Auf den ersten Blick gibt es keinen Unterschied. Probieren Sie nun die erste Methode aus und führen Sie einen einfachen Test durch:


 //case for @EmbeddedId @Test public void findAll() { int size = entityWithCompositeKeyRepository.findAllById(compositeKeys).size(); assertEquals(size, 5); } 

Im Abfrageprotokoll (Sie behalten es, richtig?) Werden wir Folgendes sehen:


 select e.key1, e.key2 from CompositeKeyEntity e where e.key1 = ? and e.key2 = ? or e.key1 = ? and e.key2 = ? or e.key1 = ? and e.key2 = ? or e.key1 = ? and e.key2 = ? or e.key1 = ? and e.key2 = ? 

Nun zweites Beispiel


 //case for @Id @Id @Test public void _findAll() { int size = anotherEntityWithCompositeKeyRepository.findAllById(compositeKeys).size(); assertEquals(size, 5); } 

Das Abfrageprotokoll sieht anders aus:


 select e.key1, e.key2 from CompositeKeyEntity e where e.key1=? and e.key2=? select e.key1, e.key2 from CompositeKeyEntity e where e.key1=? and e.key2=? select e.key1, e.key2 from CompositeKeyEntity e where e.key1=? and e.key2=? select e.key1, e.key2 from CompositeKeyEntity e where e.key1=? and e.key2=? select e.key1, e.key2 from CompositeKeyEntity e where e.key1=? and e.key2=? 

Das ist der ganze Unterschied: Im ersten Fall erhalten wir immer 1 Anfrage, im zweiten - n Anfragen.
Der Grund für dieses Verhalten liegt in SimpleJpaRepository::findAllById :


 // ... if (entityInfo.hasCompositeId()) { List<T> results = new ArrayList<>(); for (ID id : ids) { findById(id).ifPresent(results::add); } return results; } // ... 

Welche Methode für Sie am besten geeignet ist, können Sie anhand der Anzahl der Anforderungen bestimmen.


Extra CrudRepository :: speichern


Oft gibt es im Code ein solches Antimuster:


 @Transactional public BankAccount updateRate(Long id, BigDecimal rate) { BankAccount account = repo.findById(id).orElseThrow(NPE::new); account.setRate(rate); return repo.save(account); } 

Der Leser ist ratlos: Wo ist das Antimuster? Dieser Code sieht einfach sehr logisch aus: Wir erhalten die Entität - Update - Speichern. Alles ist wie in den besten Häusern von St. Petersburg. Ich wage zu sagen, dass das Aufrufen von CrudRepository::save hier überflüssig ist.


Erstens: Die updateRate Methode updateRate Transaktionsmethode. Daher werden alle Änderungen in der verwalteten Entität von Hibernate verfolgt und bei der Ausführung von Session::flush in eine Anforderung umgewandelt, die in diesem Code beim updateRate Methode auftritt.


Zweitens werfen CrudRepository::save einen Blick auf die CrudRepository::save Methode. Wie Sie wissen, basieren alle Repositorys auf SimpleJpaRepository . Hier ist die Implementierung von CrudRepository::save :


 @Transactional public <S extends T> S save(S entity) { if (entityInformation.isNew(entity)) { em.persist(entity); return entity; } else { return em.merge(entity); } } 

Es gibt eine Subtilität, an die sich nicht jeder erinnert: Der Ruhezustand funktioniert durch Ereignisse. Mit anderen Worten, jede Benutzeraktion generiert ein Ereignis, das in die Warteschlange gestellt und verarbeitet wird, wobei andere Ereignisse in derselben Warteschlange berücksichtigt werden. In diesem Fall generiert ein Aufruf von EntityManager::merge ein MergeEvent , das standardmäßig in der DefaultMergeEventListener::onMerge . Es enthält eine ziemlich verzweigte, aber einfache Logik für jeden Zustand des Entitätsarguments. In unserem Fall wird die Entität aus dem Repository innerhalb der Transaktionsmethode abgerufen und befindet sich im Status PERSISTENT (d. H. Im Wesentlichen vom Framework gesteuert):


 protected void entityIsPersistent(MergeEvent event, Map copyCache) { LOG.trace("Ignoring persistent instance"); Object entity = event.getEntity(); EventSource source = event.getSession(); EntityPersister persister = source.getEntityPersister(event.getEntityName(), entity); ((MergeContext)copyCache).put(entity, entity, true); this.cascadeOnMerge(source, persister, entity, copyCache); //<---- this.copyValues(persister, entity, entity, source, copyCache); //<---- event.setResult(entity); } 

Der Teufel DefaultMergeEventListener::cascadeOnMerge im Detail, nämlich in den Methoden DefaultMergeEventListener::cascadeOnMerge und DefaultMergeEventListener::copyValues . Hören wir uns die direkte Rede von Vlad Mikhalche an , einem der wichtigsten Entwickler von Hibernate:


Beim Methodenaufruf copyValues ​​wird der hydratisierte Zustand erneut kopiert, sodass redundant ein neues Array erstellt wird, wodurch CPU-Zyklen verschwendet werden. Wenn die Entität untergeordnete Zuordnungen hat und die Zusammenführungsoperation auch von übergeordneten zu untergeordneten Entitäten kaskadiert wird, ist der Overhead sogar noch größer, da jede untergeordnete Entität ein MergeEvent weitergibt und der Zyklus fortgesetzt wird.

Mit anderen Worten, es wird gearbeitet, was Sie nicht tun können. Infolgedessen kann unser Code vereinfacht und gleichzeitig die Leistung verbessert werden:


 @Transactional public BankAccount updateRate(Long id, BigDecimal rate) { BankAccount account = repo.findById(id).orElseThrow(NPE::new); account.setRate(rate); return account; } 

Es ist natürlich unpraktisch, dies beim Entwickeln und Korrekturlesen des Codes eines anderen zu berücksichtigen. JpaRepository::save möchten wir Änderungen auf Drahtgitterebene JpaRepository::save , damit die JpaRepository::save Methode ihre schädlichen Eigenschaften verliert. Ist es möglich?


Ja, vielleicht
 // @Transactional public <S extends T> S save(S entity) { if (entityInformation.isNew(entity)) { em.persist(entity); return entity; } else { return em.merge(entity); } } // @Transactional public <S extends T> S save(S entity) { if (entityInformation.isNew(entity)) { em.persist(entity); return entity; } else if (!em.contains(entity)) { return em.merge(entity); } return entity; } 

Diese Änderungen wurden tatsächlich im Dezember 2017 vorgenommen:
https://jira.spring.io/browse/DATAJPA-931
https://github.com/spring-projects/spring-data-jpa/pull/237


Der anspruchsvolle Leser spürte jedoch wahrscheinlich bereits, dass etwas nicht stimmte. In der Tat wird diese Änderung nichts zerstören, sondern nur in dem einfachen Fall, in dem es keine untergeordneten Entitäten gibt:


 @Entity public class BankAccount { @Id Long id; @Column BigDecimal rate = BigDecimal.ZERO; } 

Angenommen, sein Besitzer ist an das Konto gebunden:


 @Entity public class BankAccount { @Id Long id; @Column BigDecimal rate = BigDecimal.ZERO; @ManyToOne @JoinColumn(name = "user_id") User user; } 

Es gibt eine Methode, mit der Sie den Benutzer vom Konto trennen und diesen auf den neuen Benutzer übertragen können:


 @Transactional public BankAccount changeUser(Long id, User newUser) { BankAccount account = repo.findById(id).orElseThrow(NPE::new); account.setUser(newUser); return repo.save(account); } 

Was wird jetzt passieren? em.contains(entity) wird true zurückgegeben. em.merge(entity) bedeutet, dass em.merge(entity) nicht aufgerufen wird. Wenn der User auf der Grundlage der Sequenz erstellt wird (einer der häufigsten Fälle), wird er erst erstellt, wenn die Transaktion abgeschlossen ist (oder Session::flush manuell aufgerufen wird), d. H. Der Benutzer befindet sich im Status DETACHED und seine übergeordnete Entität ( Konto) - im Zustand PERSISTENT. In einigen Fällen kann dies die Anwendungslogik beschädigen, was passiert ist:


02/03/2018 DATAJPA-931 bricht die Zusammenführung mit RepositoryItemWriter ab


In diesem Zusammenhang wurde die Aufgabe Optimierungen für vorhandene Entitäten in CrudRepository :: save rückgängig machen erstellt und die Änderungen vorgenommen: DATAJPA-931 zurücksetzen .


Blind CrudRepository :: findById


Wir betrachten weiterhin dasselbe Datenmodell:


 @Entity public class User { @Id Long id; // ... } @Entity public class BankAccount { @Id Long id; @ManyToOne @JoinColumn(name = "user_id") User user; } 

Die Anwendung verfügt über eine Methode, mit der ein neues Konto für den angegebenen Benutzer erstellt wird:


 @Transactional public BankAccount newForUser(Long userId) { BankAccount account = new BankAccount(); userRepository.findById(userId).ifPresent(account::setUser); //<---- return accountRepository.save(account); } 

Bei Version 2. * ist das durch den Pfeil gekennzeichnete Antimuster nicht so auffällig - es ist bei älteren Versionen deutlicher zu erkennen:


 @Transactional public BankAccount newForUser(Long userId) { BankAccount account = new BankAccount(); account.setUser(userRepository.findOne(userId)); //<---- return accountRepository.save(account); } 

Wenn Sie den Fehler nicht "mit dem Auge" sehen, sehen Sie sich die folgenden Fragen an:
 select u.id, u.name from user u where u.id = ? call next value for hibernate_sequence insert into bank_account (id, /*…*/ user_id) values (/*…*/) 

Bei der ersten Anfrage erhalten wir den Benutzer per Schlüssel. Als nächstes erhalten wir den Schlüssel für das neugeborene Konto aus der Datenbank und fügen ihn in die Tabelle ein. Und das einzige, was wir dem Benutzer abnehmen, ist der Schlüssel, den wir bereits als Methodenargument haben. Auf der anderen Seite enthält BankAccount das Feld "Benutzer" und wir können es nicht leer lassen (als anständige Leute setzen wir eine Einschränkung im Schema). Erfahrene Entwickler sehen wahrscheinlich schon einen Weg und iss einen Fisch und reite ein Pferd Holen Sie sich sowohl den Benutzer als auch die Anfrage, nicht:


 @Transactional public BankAccount newForUser(Long userId) { BankAccount account = new BankAccount(); account.setUser(userRepository.getOne(userId)); //<---- return accountRepository.save(account); } 

JpaRepository::getOne gibt einen Wrapper über den Schlüssel zurück, der denselben Typ wie die lebende "Entität" hat. Dieser Code gibt nur zwei Anfragen:


 call next value for hibernate_sequence insert into bank_account (id, /*…*/ user_id) values (/*…*/) 

Wenn eine zu erstellende Entität viele Felder mit einer Beziehung von vielen zu eins / eins zu eins enthält, hilft diese Technik, das Speichern zu beschleunigen und die Belastung der Datenbank zu verringern.


Ausführen von HQL-Abfragen


Dies ist ein separates und interessantes Thema :). Das Domain-Modell ist das gleiche und es gibt eine solche Anfrage:


 @Query("select count(ba) " + " from BankAccount ba " + " join ba.user user " + " where user.id = :id") long countUserAccounts(@Param("id") Long id); 

Betrachten Sie die "reine" HQL:


 select count(ba) from BankAccount ba join ba.user user where user.id = :id 

Bei der Ausführung wird die folgende SQL-Abfrage erstellt:


 select count(ba.id) from bank_account ba inner join user u on ba.user_id = u.id where u.id = ? 

Das Problem ist hier selbst bei einem weisen Leben und einem guten Verständnis der SQL-Entwickler nicht sofort ersichtlich: Durch die inner join mit dem Benutzerschlüssel werden Konten mit fehlender Benutzer- user_id von der Auswahl ausgeschlossen (und das Einfügen dieser Konten sollte auf user_id verboten sein), was bedeutet, dass es überhaupt nicht user_id , der user beizutreten müssen. Die Anfrage kann vereinfacht (und beschleunigt) werden:


 select count(ba.id) from bank_account ba where ba.user_id = ? 

Es gibt eine Möglichkeit, dieses Verhalten in c mithilfe von HQL einfach zu erreichen:


 @Query("select count(ba) " + " from BankAccount ba " + " where ba.user.id = :id") long countUserAccounts(@Param("id") Long id); 

Diese Methode erstellt eine "Lite" -Anforderung.


Abfrage vs. Methodenzusammenfassung


Eine der Hauptfunktionen von Spring Data ist die Möglichkeit, eine Abfrage aus dem Methodennamen zu erstellen. Dies ist sehr praktisch, insbesondere in Kombination mit dem intelligenten Add-On von IntelliJ IDEA. Die im vorherigen Beispiel beschriebene Abfrage kann einfach umgeschrieben werden:


 // @Query("select count(ba) " + " from BankAccount ba " + " where ba.user.id = :id") long countUserAccounts(@Param("id") Long id); // long countByUserAccount_Id(Long id); 

Es scheint einfacher und kürzer und lesbarer zu sein, und vor allem müssen Sie die Anfrage selbst nicht ansehen. Ich habe den Namen der Methode gelesen - und es ist bereits klar, was und wie sie auswählt. Aber der Teufel steckt hier im Detail. Die letzte Abfrage für die mit @Query gekennzeichnete Methode @Query wir bereits gesehen. Was wird im zweiten Fall passieren?


Babah!
 select count(ba.id) from bank_account ba left outer join // <--- !!!!!!! user u on ba.user_id = u.id where u.id = ? 

"Was zur Hölle !?" - Der Entwickler wird ausrufen. Immerhin haben wir das schon gesehen Geiger join nicht benötigt.


Der Grund ist prosaisch:



Wenn Sie noch kein Upgrade auf die gepatchten Versionen durchgeführt haben und das Hinzufügen der Tabelle die Abfrage hier und jetzt verlangsamt, verzweifeln Sie nicht: Es gibt zwei Möglichkeiten, um die Schmerzen zu lindern:


  • Ein guter Weg ist, optional = false hinzuzufügen (wenn die Schaltung dies zulässt):


     @Entity public class BankAccount { @Id Long id; @ManyToOne @JoinColumn(name = "user_id", optional = false) User user; } 

  • Die Krückenmethode besteht darin, eine Spalte des gleichen Typs wie der User hinzuzufügen und sie in Abfragen anstelle des user verwenden:


     @Entity public class BankAccount { @Id Long id; @ManyToOne @JoinColumn(name = "user_id") User user; @Column(name = "user_id", insertable = false, updatable = false) Long userId; } 

    Jetzt wird die Anfrage-von-Methode schöner:


     long countByUserId(Long id); 

    gibt


     select count(ba.id) from bank_account ba where ba.user_id = ? 

    Was haben wir erreicht?



Probenahmegrenze


Für unsere Zwecke müssen wir die Auswahl einschränken (z. B. möchten wir Optional von der *RepositoryCustom Methode zurückgeben):


 select ba.* from bank_account ba order by ba.rate limit ? 

Jetzt Java:


 @Override public Optional<BankAccount> findWithHighestRate() { String query = "select b from BankAccount b order by b.rate"; BankAccount account = em .createQuery(query, BankAccount.class) .setFirstResult(0) .setMaxResults(1) .getSingleResult(); return Optional.ofNullable(bankAccount); } 

Der angegebene Code hat eine unangenehme Eigenschaft: Falls die Anforderung eine leere Auswahl zurückgibt, wird eine Ausnahme ausgelöst


 Caused by: javax.persistence.NoResultException: No entity found for query 

In den Projekten, die ich gesehen habe, wurde dies auf zwei Arten gelöst:


  • try-catch mit Variationen von unverblümtem Optonal.empty() Ausnahme und Zurückgeben von Optonal.empty() auf fortgeschrittenere Methoden, z. B. Übergeben eines Lambda mit einer Anforderung an eine Dienstprogrammmethode
  • Aspekt, in dem Repository-Methoden verpackt sind und Optional

Und sehr selten sah ich die richtige Lösung:


 @Override public Optional<BankAccount> findWithHighestRate() { String query = "select b from BankAccount b order by b.rate"; return em.unwrap(Session.class) .createQuery(query, BankAccount.class) .setFirstResult(0) .setMaxResults(1) .uniqueResultOptional(); } 

EntityManager ist Teil des JPA-Standards, während Session zu Hibernate gehört und meiner Meinung nach ein erweitertes Tool ist, das oft vergessen wird.


[Manchmal] schädliche Verbesserung


Wenn Sie ein kleines Feld von einer "dicken" Entität erhalten möchten, gehen wir folgendermaßen vor:


 @Query("select a.available from BankAccount a where a.id = :id") boolean findIfAvailable(@Param("id") Long id); 

Mit der Anforderung können Sie ein Feld vom Typ boolean abrufen, ohne die gesamte Entität zu laden (mit Hinzufügen eines Caches der ersten Ebene, Überprüfen auf Änderungen am Ende der Sitzung und anderer Kosten). Manchmal verbessert dies nicht nur nicht die Leistung, sondern auch umgekehrt - es entstehen unnötige Abfragen von Grund auf. Stellen Sie sich einen Code vor, der einige Überprüfungen durchführt:


 @Override @Transactional public boolean checkAccount(Long id) { BankAccount acc = repository.findById(id).orElseThow(NPE::new); // ... return repository.findIfAvailable(id); } 

Dieser Code macht mindestens 2 Anfragen, obwohl die zweite vermieden werden könnte:


 @Override @Transactional public boolean checkAccount(Long id) { BankAccount acc = repository.findById(id).orElseThow(NPE::new); // ... return repository.findById(id) //    .map(BankAccount::isAvailable) .orElseThrow(IllegalStateException::new); } 

Die Schlussfolgerung ist einfach: Vernachlässigen Sie nicht den Cache der ersten Ebene, im Rahmen einer Transaktion verweist nur das erste JpaRepository::findById auf die Datenbank, JpaRepository::findById Cache der ersten Ebene immer aktiv ist und an eine Sitzung gebunden ist, die normalerweise an die aktuelle Transaktion gebunden ist.


Tests zum Spielen (Link zum Repository finden Sie am Anfang des Artikels):


  • schmaler Schnittstellentest: InterfaceNarrowingTest
  • Testen Sie ein Beispiel mit einem zusammengesetzten Schlüssel: EntityWithCompositeKeyRepositoryTest
  • Testüberschuss CrudRepository::save : ModifierTest.java
  • Blindtest CrudRepository::findById : ChildServiceImplTest
  • unnötiger BankAccountControlRepositoryTest left join Test: BankAccountControlRepositoryTest

Die Kosten für einen zusätzlichen Aufruf von CrudRepository::save können mit RedundantSaveBenchmark berechnet werden. Es wird mit der BenchmarkRunner Klasse gestartet.

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


All Articles