Spring Data JPA: ce qui est bon et ce qui est mauvais

Bébé est venu voir son pÚre
Et a demandé au bébé
- Ce qui est bon
et ce qui est mauvais

Vladimir Mayakovsky


Cet article concerne Spring Data JPA, notamment dans le rùteau sous-marin que j'ai rencontré en chemin, et bien sûr un peu sur les performances.


Les exemples dĂ©crits dans l'article peuvent ĂȘtre exĂ©cutĂ©s dans l'environnement de test, accessible par rĂ©fĂ©rence .


Remarque pour ceux qui ne sont pas encore passés à Spring Boot 2

Dans les versions de Spring Data JPA 2. *, l'interface principale pour travailler avec les référentiels, à savoir CrudRepository , dont JpaRepository est hérité, a JpaRepository . Dans les versions 1. * les principales méthodes ressemblaient à ceci:


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

Dans les nouvelles versions:


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

Commençons donc.


sĂ©lectionnez t. * parmi t oĂč t.id dans (...)


L'une des requĂȘtes les plus courantes est une requĂȘte de la forme «sĂ©lectionner tous les enregistrements pour lesquels la clĂ© appartient Ă  l'ensemble transmis». Je suis sĂ»r que presque tous ont Ă©crit ou vu quelque chose comme


 @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); 

Ceux-ci fonctionnent, les demandes appropriées, il n'y a pas de problÚme de capture ou de performance, mais il y a un petit inconvénient complÚtement invisible.


Avant d'ouvrir la doublure, essayez de penser par vous-mĂȘme.

L'inconvénient est que l'interface est trop étroite pour transmettre des clés. "Et alors?" - dites-vous. "Eh bien la liste, bien l'ensemble, je ne vois pas de problÚme ici." Cependant, si nous regardons les méthodes de l'interface racine qui prennent de nombreuses valeurs, alors partout nous voyons Iterable :


"Et alors? Et je veux une liste. Pourquoi est-ce pire?"
Pas pire, préparez-vous simplement à l'apparition d'un code similaire à un niveau supérieur dans votre application:


 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); } 

Ce code ne fait qu'inverser les collections. Il peut s'avĂ©rer que l'argument de la mĂ©thode est une liste, et la mĂ©thode du rĂ©fĂ©rentiel accepte l'ensemble (ou vice versa), et il vous suffit de le rĂ©organiser pour passer la compilation. Bien sĂ»r, cela ne deviendra pas un problĂšme dans le contexte des frais gĂ©nĂ©raux pour la demande elle-mĂȘme, il s'agit davantage de gestes inutiles.


Par conséquent, il est Iterable utiliser Iterable :


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

Z.Y. Si nous parlons d'une méthode de *RepositoryCustom , alors il est logique d'utiliser Collection pour simplifier le calcul de la taille à l'intérieur de l'implémentation:


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

Code supplémentaire: clés non dupliquées


Dans la suite de la derniÚre section, je voudrais attirer l'attention sur une idée fausse commune:


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

Autres manifestations de la mĂȘme erreur:


 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); 

À premiùre vue, rien d'inhabituel, non?


Prenez votre temps, pensez par vous-mĂȘme;)

Les requĂȘtes HQL / JPQL de la forme select t from t where t.field in ... finira par devenir une requĂȘte


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

qui retournera toujours la mĂȘme chose indĂ©pendamment de la prĂ©sence de rĂ©pĂ©titions dans l'argument. Par consĂ©quent, il n'est pas nĂ©cessaire de garantir l'unicitĂ© des clĂ©s. Il existe un cas particulier - Oracle, oĂč le fait d'appuyer sur> 1 000 clĂ©s d'entrĂ©e entraĂźne une erreur. Mais si vous essayez de rĂ©duire le nombre de clĂ©s en excluant les rĂ©pĂ©titions, vous devriez plutĂŽt rĂ©flĂ©chir Ă  la raison de leur apparition. L'erreur est probablement quelque part au-dessus.


Donc, dans un bon code, utilisez Iterable :


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

Samopis


Examinez attentivement ce code et trouvez ici trois défauts et une erreur possible:


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

Pensez un peu plus
  • tout est dĂ©jĂ  implĂ©mentĂ© dans SimpleJpaRepository::findAllById
  • demande inactive lors du passage d'une liste vide (dans SimpleJpaRepository::findAllById il y a une vĂ©rification correspondante)
  • toutes les requĂȘtes dĂ©crites Ă  l'aide de @Query sont vĂ©rifiĂ©es au stade de l'Ă©lĂ©vation du contexte, ce qui prend du temps (contrairement Ă  SimpleJpaRepository::findAllById )
  • si Oracle est utilisĂ©, lorsque la collection de clĂ©s est vide, nous obtenons l'erreur ORA-00936: missing expression (ce qui ne se produira pas lors de l'utilisation de SimpleJpaRepository::findAllById , voir point 2)

Harry potter et clé composée


Jetez un Ɠil Ă  deux exemples et choisissez celui que vous prĂ©fĂ©rez:


Nombre de méthodes fois


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

Méthode numéro deux


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

À premiĂšre vue, il n'y a aucune diffĂ©rence. Essayez maintenant la premiĂšre mĂ©thode et exĂ©cutez un test simple:


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

Dans le journal des requĂȘtes (vous le gardez, non?) Nous verrons ceci:


 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 = ? 

Maintenant, deuxiĂšme exemple


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

Le journal des requĂȘtes est diffĂ©rent:


 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=? 

C'est toute la différence: dans le premier cas, nous recevons toujours 1 demande, dans le second - n demandes.
La raison de ce comportement réside dans SimpleJpaRepository::findAllById :


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

La méthode qui vous convient le mieux est de déterminer en fonction de l'importance du nombre de demandes.


Extra CrudRepository :: enregistrer


Souvent, dans le code, il existe un tel contre-motif:


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

Le lecteur est perplexe: oĂč est l'anti-modĂšle? Ce code semble trĂšs logique: nous obtenons l'entitĂ© - mise Ă  jour - sauvegarde. Tout est comme dans les meilleures maisons de Saint-PĂ©tersbourg. J'ose dire que l'appel de CrudRepository::save est superflu ici.


PremiÚrement: la méthode updateRate transactionnelle, par conséquent, toutes les modifications dans l'entité gérée sont suivies par Hibernate et se transforment en demande lorsque Session::flush exécutée, ce qui se produit dans ce code à la fin de la méthode.


DeuxiĂšmement, CrudRepository::save un coup d'Ɠil Ă  la mĂ©thode CrudRepository::save . Comme vous le savez, tous les rĂ©fĂ©rentiels sont basĂ©s sur SimpleJpaRepository . Voici l'implĂ©mentation de 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); } } 

Il y a une subtilitĂ© dont tout le monde ne se souvient pas: Hibernate fonctionne Ă  travers les Ă©vĂ©nements. En d'autres termes, chaque action utilisateur gĂ©nĂšre un Ă©vĂ©nement qui est mis en file d'attente et traitĂ© en tenant compte d'autres Ă©vĂ©nements dans la mĂȘme file d'attente. Dans ce cas, un appel Ă  EntityManager::merge gĂ©nĂšre un MergeEvent , qui est traitĂ© par dĂ©faut dans la DefaultMergeEventListener::onMerge . Il contient une logique assez ramifiĂ©e mais simple pour chacun des Ă©tats de l'argument entitĂ©. Dans notre cas, l'entitĂ© est obtenue Ă  partir du rĂ©fĂ©rentiel Ă  l'intĂ©rieur de la mĂ©thode transactionnelle et est Ă  l'Ă©tat PERSISTANT (c'est-Ă -dire essentiellement contrĂŽlĂ©e par le framework):


 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); } 

Le diable est dans les dĂ©tails, Ă  savoir dans les mĂ©thodes DefaultMergeEventListener::cascadeOnMerge et DefaultMergeEventListener::copyValues . Écoutons le discours direct de Vlad Mikhalche , l'un des principaux dĂ©veloppeurs d'Hibernate:


Dans l'appel de méthode copyValues, l'état hydraté est à nouveau copié, de sorte qu'un nouveau tableau est créé de maniÚre redondante, ce qui gaspille les cycles CPU. Si l'entité a des associations enfants et que l'opération de fusion est également répercutée des entités parent aux entités enfant, la surcharge est encore plus importante car chaque entité enfant propage un événement MergeEvent et le cycle se poursuit.

En d'autres termes, on fait du travail que vous ne pouvez pas faire. En consĂ©quence, notre code peut ĂȘtre simplifiĂ© tout en amĂ©liorant ses performances:


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

Bien sûr, il n'est pas pratique de garder cela à l'esprit lors du développement et de la relecture du code de quelqu'un d'autre, nous aimerions donc apporter des modifications au niveau de la structure filaire afin que la méthode JpaRepository::save perde ses propriétés nuisibles. Est-ce possible?


Oui peut-ĂȘtre
 // @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; } 

Ces changements ont en effet été opérés en décembre 2017:
https://jira.spring.io/browse/DATAJPA-931
https://github.com/spring-projects/spring-data-jpa/pull/237


Cependant, le lecteur averti a probablement dĂ©jĂ  senti que quelque chose n'allait pas. En effet, ce changement ne cassera rien, mais seulement dans le cas simple oĂč il n'y a pas d'entitĂ©s enfants:


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

Supposons maintenant que son propriétaire soit lié au compte:


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

Il existe une méthode qui vous permet de déconnecter l'utilisateur du compte et de transférer ce dernier au nouvel utilisateur:


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

Que va-t-il se passer maintenant? La vĂ©rification de em.contains(entity) renvoie true, ce qui signifie que em.merge(entity) ne sera pas appelĂ©. Si la clĂ© d'entitĂ© User est créée sur la base de la sĂ©quence (l'un des cas les plus courants), elle ne sera pas créée jusqu'Ă  ce que la transaction soit terminĂ©e (ou que Session::flush appelĂ©e manuellement), c'est-Ă -dire que l'utilisateur sera Ă  l'Ă©tat DÉTACHÉ et son entitĂ© parent ( compte) - Ă  l'Ă©tat PERSISTANT. Dans certains cas, cela peut briser la logique d'application, ce qui s'est produit:


02/03/2018 DATAJPA-931 interrompt la fusion avec RepositoryItemWriter


À cet Ă©gard, la tĂąche Revert optimisations effectuĂ©es pour les entitĂ©s existantes dans CrudRepository :: save a Ă©tĂ© créée et les modifications ont Ă©tĂ© apportĂ©es: Revert DATAJPA-931 .


Blind CrudRepository :: findById


Nous continuons Ă  considĂ©rer le mĂȘme modĂšle de donnĂ©es:


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

L'application a une méthode qui crée un nouveau compte pour l'utilisateur spécifié:


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

Avec la version 2. * l'anti-modÚle indiqué par la flÚche n'est pas si frappant - il est plus clairement visible sur les anciennes versions:


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

Si vous ne voyez pas la faille \ "Ă  l'oeil \", jetez un Ɠil aux requĂȘtes:
 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 (/*
*/) 

La premiÚre demande, nous obtenons l'utilisateur par clé. Ensuite, nous obtenons la clé du compte nouveau-né de la base de données et l'insérons dans le tableau. Et la seule chose que nous prenons à l'utilisateur est la clé, que nous avons déjà comme argument de méthode. D'un autre cÎté, BankAccount contient le champ "utilisateur" et nous ne pouvons pas le laisser vide (en tant que personnes décentes, nous définissons une restriction dans le schéma). Les développeurs expérimentés voient probablement déjà un moyen et manger un poisson et monter à cheval obtenir à la fois l'utilisateur et la demande de ne pas:


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

JpaRepository::getOne retourne un wrapper sur la clĂ© qui a le mĂȘme type que "l'entitĂ©" vivante. Ce code ne donne que deux requĂȘtes:


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

Lorsqu'une entité en cours de création contient de nombreux champs avec une relation plusieurs à un / un à un, cette technique permet d'accélérer l'enregistrement et de réduire la charge sur la base de données.


ExĂ©cution de requĂȘtes HQL


Il s'agit d'un sujet distinct et intĂ©ressant :). Le modĂšle de domaine est le mĂȘme et il existe une telle demande:


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

Considérez le HQL "pur":


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

Lors de son exĂ©cution, la requĂȘte SQL suivante sera créée:


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

Le problĂšme ici n'est pas immĂ©diatement Ă©vident, mĂȘme par une vie sage et des dĂ©veloppeurs SQL bien compris: inner join par clĂ© utilisateur exclura les comptes avec user_id manquant de l'Ă©chantillon (et dans le bon sens, l'insertion de ceux-ci devrait ĂȘtre interdite au niveau du schĂ©ma), ce qui signifie qu'il n'est pas user_id de rejoindre la table user du tout besoin de. La demande peut ĂȘtre simplifiĂ©e (et accĂ©lĂ©rĂ©e):


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

Il existe un moyen d'obtenir facilement ce comportement en c Ă  l'aide de HQL:


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

Cette mĂ©thode crĂ©e une requĂȘte «allĂ©gĂ©e».


RĂ©sumĂ© de la requĂȘte et de la mĂ©thode


L'une des principales caractĂ©ristiques de Spring Data est la possibilitĂ© de crĂ©er une requĂȘte Ă  partir du nom de la mĂ©thode, ce qui est trĂšs pratique, en particulier en combinaison avec le module complĂ©mentaire intelligent d'IntelliJ IDEA. La requĂȘte dĂ©crite dans l'exemple prĂ©cĂ©dent peut ĂȘtre facilement réécrite:


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

Il semble ĂȘtre plus simple, plus court et plus lisible, et surtout - vous n'avez pas besoin de regarder la demande elle-mĂȘme. J'ai lu le nom de la mĂ©thode - et c'est dĂ©jĂ  clair ce qu'elle choisit et comment. Mais le diable est ici dans les dĂ©tails. Nous avons dĂ©jĂ  vu la derniĂšre requĂȘte pour la mĂ©thode marquĂ©e avec @Query . Que se passera-t-il dans le deuxiĂšme cas?


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

"Que diable!?" - s'exclamera le développeur. AprÚs tout, nous avons déjà vu que violoniste join pas nécessaire.


La raison est prosaĂŻque:



Si vous n'avez pas encore mis à niveau vers les versions corrigées et que rejoindre la table ralentit la demande ici et maintenant, alors ne désespérez pas: il y a deux façons de soulager la douleur:


  • une bonne façon est d'ajouter optional = false (si le circuit le permet):


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

  • La maniĂšre la plus simple consiste Ă  ajouter une colonne du mĂȘme type que la clĂ© d'entitĂ© User et Ă  l'utiliser dans des requĂȘtes au lieu du champ user :


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

    Maintenant, la méthode request-from-method sera plus agréable:


     long countByUserId(Long id); 

    donne


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

    qu'avons-nous réalisé.



Limite d'échantillonnage


Pour nos besoins, nous devons limiter la sélection (par exemple, nous voulons renvoyer Optional partir de la méthode *RepositoryCustom ):


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

Maintenant 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); } 

Le code spĂ©cifiĂ© a une caractĂ©ristique dĂ©sagrĂ©able: dans le cas oĂč la demande retourne une sĂ©lection vide, une exception sera levĂ©e


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

Dans les projets que j'ai vus, cela a été résolu de deux maniÚres principales:


  • try-catch avec des variantes de la Optonal.empty() exception et le retour d' Optonal.empty() Ă  des mĂ©thodes plus avancĂ©es, comme passer un lambda avec une requĂȘte Ă  une mĂ©thode utilitaire
  • aspect dans lequel les mĂ©thodes de rĂ©fĂ©rentiel sont renvoyĂ©es renvoyĂ©es Optional

Et trĂšs rarement, j'ai vu la bonne solution:


 @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 fait partie de la norme JPA, tandis que Session appartient à Hibernate et est à mon humble avis un outil plus avancé, qui est souvent oublié.


[Parfois] amélioration néfaste


Lorsque vous avez besoin d'obtenir un petit champ à partir d'une entité "épaisse", nous faisons ceci:


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

La demande vous permet d'obtenir un champ de type boolean sans charger l'entitĂ© entiĂšre (avec l'ajout d'un cache de premier niveau, la vĂ©rification des modifications Ă  la fin de la session et d'autres dĂ©penses). Parfois, cela non seulement n'amĂ©liore pas les performances, mais vice versa - il crĂ©e des requĂȘtes inutiles Ă  partir de zĂ©ro. Imaginez un code qui effectue quelques vĂ©rifications:


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

Ce code fait au moins 2 requĂȘtes, bien que la seconde puisse ĂȘtre Ă©vitĂ©e:


 @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); } 

La conclusion est simple: ne négligez pas le cache du premier niveau, dans le cadre d'une transaction, seul le premier JpaRepository::findById fait référence à la base de données, JpaRepository::findById cache du premier niveau est toujours JpaRepository::findById et lié à une session, qui est généralement liée à la transaction en cours.


Tests avec lesquels jouer (le lien vers le référentiel est donné au début de l'article):


  • test d'interface Ă©troite: InterfaceNarrowingTest
  • tester un exemple avec une clĂ© composite: EntityWithCompositeKeyRepositoryTest
  • tester l'excĂšs de CrudRepository::save : ModifierTest.java
  • test aveugle CrudRepository::findById : ChildServiceImplTest
  • test de left join inutile: BankAccountControlRepositoryTest

Le coĂ»t d'un appel supplĂ©mentaire Ă  CrudRepository::save peut ĂȘtre calculĂ© Ă  l'aide de RedundantSaveBenchmark . Il est lancĂ© Ă  l'aide de la classe BenchmarkRunner .

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


All Articles