Filho veio ao pai 
E perguntou ao bebê 
- o que é bom 
e o que é ruim 
Vladimir Mayakovsky
Este artigo é sobre o Spring Data JPA, especificamente no rake subaquático que conheci no caminho e, é claro, um pouco sobre desempenho.
Os exemplos descritos no artigo podem ser executados no ambiente de teste, acessível por referência .
Nota para aqueles que ainda não se mudaram para o Spring Boot 2Nas versões do Spring Data JPA 2. *, a interface principal para trabalhar com repositórios, nomeadamente CrudRepository , da qual o JpaRepository é herdado, JpaRepository . Nas versões 1. *, os principais métodos eram assim:
 public interface CrudRepository<T, ID> { T findOne(ID id); List<T> findAll(Iterable<ID> ids); } 
Em novas versões:
 public interface CrudRepository<T, ID> { Optional<T> findById(ID id); List<T> findAllById(Iterable<ID> ids); } 
 Então, vamos começar.
selecione t. * de t onde t.id (...)
Uma das consultas mais comuns é uma consulta no formato "selecione todos os registros para os quais a chave se enquadra no conjunto transmitido". Tenho certeza que quase todos vocês escreveram ou viram algo como
 @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); 
Esses pedidos estão funcionando, são adequados, não há problemas de captura ou desempenho, mas há uma pequena desvantagem completamente discreta.
Antes de abrir o revestimento, tente pensar por si mesmo.A desvantagem é que a interface é muito estreita para transmitir chaves. "Então o que?" você diz. "Bem, a lista, bem o conjunto, não vejo nenhum problema aqui." No entanto, se observarmos os métodos da interface raiz que recebem muitos valores, em todos os lugares vemos Iterable :
"E daí? E eu quero uma lista. Por que é pior?"
Não é pior, apenas esteja preparado para a aparência de código semelhante em um nível mais alto no seu aplicativo:
 public List<BankAccount> findByUserId(List<Long> userIds) { Set<Long> ids = new HashSet<>(userIds); return repository.findByUserIds(ids); }  
Esse código não faz nada além de reverter as coleções. Pode acontecer que o argumento para o método seja uma lista, e o método do repositório aceite o conjunto (ou vice-versa), e você apenas precisará reordená-lo para passar na compilação. Obviamente, isso não se tornará um problema no contexto dos custos indiretos da própria solicitação, trata-se mais de gestos desnecessários.
Portanto, é uma boa prática usar o Iterable :
 @Query("select ba from BankAccount ba where ba.user.id in :ids") List<BankAccount> findByUserIds(@Param("ids") Iterable<Long> ids); 
Z.Y. Se estamos falando de um método de *RepositoryCustom , faz sentido usar Collection para simplificar o cálculo do tamanho dentro da implementação:
 public interface BankAccountRepositoryCustom { boolean anyMoneyAvailable(Collection<Long> accountIds); } public class BankAccountRepositoryImpl { @Override public boolean anyMoneyAvailable(Collection<Long> accountIds) { if (ids.isEmpty()) return false;  
 Código extra: chaves não duplicadas
Na continuação da última seção, quero chamar a atenção para um equívoco comum:
 @Query("select ba from BankAccount ba where ba.user.id in :ids") List<BankAccount> findByUserIds(@Param("ids") Set<Long> ids); 
Outras manifestações do mesmo erro:
 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); 
À primeira vista, nada de anormal, certo?
Leve o seu tempo, pense por si mesmo;)As consultas HQL / JPQL do formulário select t from t where t.field in ... acabará por se transformar em uma consulta
 select b.* from BankAccount b where b.user_id in (?, ?, ?, ?, ?, …) 
que sempre retornará a mesma coisa, independentemente da presença de repetições no argumento. Portanto, para garantir a exclusividade das chaves não é necessário. Há um caso especial - Oracle, em que pressionar mais de 1000 chaves leva a um erro. Mas se você está tentando reduzir o número de chaves eliminando repetições, deve pensar na razão de sua ocorrência. Provavelmente o erro está em algum lugar acima.
Portanto, em bom código, use Iterable :
 @Query("select ba from BankAccount ba where ba.user.id in :ids") List<BankAccount> findByUserIds(@Param("ids") Iterable<Long> ids); 
 Samopis
Dê uma olhada neste código e encontre aqui três falhas e um possível erro:
 @Query("from User u where u.id in :ids") List<User> findAll(@Param("ids") Iterable<Long> ids); 
Pense um pouco mais- tudo já está implementado no SimpleJpaRepository::findAllById
- solicitação inativa ao passar uma lista vazia (em SimpleJpaRepository::findAllByIdhá uma verificação correspondente)
- todas as consultas descritas usando @Querysão verificadas no estágio de aumentar o contexto, o que leva tempo (diferente deSimpleJpaRepository::findAllById)
- se o Oracle for usado, quando a coleção de chaves estiver vazia, obteremos o erro ORA-00936: missing expression(o que não acontecerá ao usarSimpleJpaRepository::findAllById, consulte o ponto 2)
 Harry potter e chave composta
Dê uma olhada em dois exemplos e escolha o seu preferido:
Número do método vezes
 @Embeddable public class CompositeKey implements Serializable { Long key1; Long key2; } @Entity public class CompositeKeyEntity { @EmbeddedId CompositeKey key; } 
Método número dois
 @Embeddable public class CompositeKey implements Serializable { Long key1; Long key2; } @Entity @IdClass(value = CompositeKey.class) public class CompositeKeyEntity { @Id Long key1; @Id Long key2; } 
À primeira vista, não há diferença. Agora tente o primeiro método e execute um teste simples:
 
No log de consulta (você mantém, certo?) Veremos isso:
 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 = ? 
Agora segundo exemplo
 
O log de consulta parece diferente:
 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=? 
Essa é toda a diferença: no primeiro caso, sempre recebemos 1 solicitação, no segundo - n pedidos.
O motivo desse comportamento está em SimpleJpaRepository::findAllById :
 
Qual método é o melhor para você determinar com base na importância do número de solicitações.
CrudRepository extra :: salvar
Freqüentemente no código existe um antipadrão:
 @Transactional public BankAccount updateRate(Long id, BigDecimal rate) { BankAccount account = repo.findById(id).orElseThrow(NPE::new); account.setRate(rate); return repo.save(account); } 
O leitor está perplexo: onde está o antipadrão? Este código parece muito lógico: obtemos a entidade - atualização - salvamento. Tudo é como nas melhores casas de São Petersburgo. Ouso dizer que chamar CrudRepository::save é supérfluo aqui.
Primeiro: o método updateRate transacional; portanto, todas as alterações na entidade gerenciada são rastreadas pelo Hibernate e se transformam em uma solicitação quando o Session::flush executado, o que nesse código ocorre quando o método termina.
Em segundo lugar, CrudRepository::save dar uma olhada no método CrudRepository::save . Como você sabe, todos os repositórios são baseados no SimpleJpaRepository . Aqui está a implementação do 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); } } 
Há uma sutileza que nem todos se lembram: o Hibernate funciona através de eventos. Em outras palavras, cada ação do usuário gera um evento na fila e processado, levando em consideração outros eventos na mesma fila. Nesse caso, uma chamada para EntityManager::merge gera um MergeEvent , que é processado por padrão no DefaultMergeEventListener::onMerge . Ele contém uma lógica bastante ramificada, mas simples, para cada um dos estados do argumento da entidade. Em nosso caso, a entidade é obtida do repositório dentro do método transacional e está no estado PERSISTENT (ou seja, essencialmente controlado pela estrutura):
 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);  
O diabo está nos detalhes, nomeadamente nos métodos DefaultMergeEventListener::cascadeOnMerge e DefaultMergeEventListener::copyValues . Vamos ouvir o discurso direto de Vlad Mikhalche , um dos principais desenvolvedores do Hibernate:
Na chamada do método copyValues, o estado hidratado é copiado novamente, para que uma nova matriz seja criada de forma redundante, desperdiçando assim ciclos de CPU. Se a entidade tiver associações filhas e a operação de mesclagem também for cascateada de pai para filho, a sobrecarga será ainda maior porque cada entidade filho propagará um MergeEvent e o ciclo continuará.
Em outras palavras, está sendo feito um trabalho que você não pode fazer. Como resultado, nosso código pode ser simplificado enquanto aprimora seu desempenho:
 @Transactional public BankAccount updateRate(Long id, BigDecimal rate) { BankAccount account = repo.findById(id).orElseThrow(NPE::new); account.setRate(rate); return account; } 
Obviamente, é inconveniente ter isso em mente ao desenvolver e revisar o código de outra pessoa, portanto, gostaríamos de fazer alterações no nível da estrutura de arame para que o método JpaRepository::save perca suas propriedades nocivas. Isso é possível?
No entanto, o leitor sofisticado provavelmente já sentiu que algo estava errado. De fato, essa mudança não quebrará nada, mas apenas no caso simples quando não houver entidades filhas:
 @Entity public class BankAccount { @Id Long id; @Column BigDecimal rate = BigDecimal.ZERO; } 
Agora, suponha que seu proprietário esteja vinculado à conta:
 @Entity public class BankAccount { @Id Long id; @Column BigDecimal rate = BigDecimal.ZERO; @ManyToOne @JoinColumn(name = "user_id") User user; } 
Existe um método que permite desconectar o usuário da conta e transferi-lo para o novo usuário:
 @Transactional public BankAccount changeUser(Long id, User newUser) { BankAccount account = repo.findById(id).orElseThrow(NPE::new); account.setUser(newUser); return repo.save(account); } 
O que vai acontecer agora? A verificação de em.contains(entity) retornará true, o que significa que em.merge(entity) não será chamado. Se a chave da entidade User for criada com base na sequência (um dos casos mais comuns), ela não será criada até que a transação seja concluída (ou Session::flush chamada manualmente), ou seja, o usuário estará no estado DETACHED e sua entidade pai ( conta) - no estado PERSISTENTE. Em alguns casos, isso pode quebrar a lógica do aplicativo, e foi o que aconteceu:
02/03/2018 DATAJPA-931 interrompe a fusão com o RepositoryItemWriter
Nesse sentido, a tarefa Reverter otimizações feitas para entidades existentes no CrudRepository :: save foi criada e as alterações foram feitas: Reverter DATAJPA-931 .
Blind CrudRepository :: findById
Continuamos a considerar o mesmo modelo de dados:
 @Entity public class User { @Id Long id;  
O aplicativo possui um método que cria uma nova conta para o usuário especificado:
 @Transactional public BankAccount newForUser(Long userId) { BankAccount account = new BankAccount(); userRepository.findById(userId).ifPresent(account::setUser);  
Com a versão 2. * o antipadrão indicado pela seta não é tão impressionante - é mais claramente visto nas versões mais antigas:
 @Transactional public BankAccount newForUser(Long userId) { BankAccount account = new BankAccount(); account.setUser(userRepository.findOne(userId));  
Se você não vê a falha "a olho nu", dê uma olhada nas consultas: 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 () 
 A primeira solicitação, obtemos o usuário por chave. Em seguida, obtemos a chave da conta do recém-nascido no banco de dados e a inserimos na tabela. E a única coisa que extraímos do usuário é a chave, que já temos como argumento de método. Por outro lado, BankAccount contém o campo "user" e não podemos deixá-lo vazio (como pessoas decentes, definimos uma restrição no esquema). Desenvolvedores experientes provavelmente já veem uma maneira e comer um peixe, e andar a cavalo faça com que o usuário e a solicitação não:
 @Transactional public BankAccount newForUser(Long userId) { BankAccount account = new BankAccount(); account.setUser(userRepository.getOne(userId));  
JpaRepository::getOne retorna um wrapper sobre a chave que tem o mesmo tipo da "entidade" viva. Este código fornece apenas duas solicitações:
 call next value for hibernate_sequence insert into bank_account (id,  user_id) values () 
Quando uma entidade que está sendo criada contém muitos campos com um relacionamento muitos-para-um / um-para-um, essa técnica ajuda a acelerar a economia e reduzir a carga no banco de dados.
Executando consultas HQL
Este é um tópico separado e interessante :). O modelo de domínio é o mesmo e existe uma solicitação:
 @Query("select count(ba) " + " from BankAccount ba " + " join ba.user user " + " where user.id = :id") long countUserAccounts(@Param("id") Long id); 
Considere o HQL "puro":
 select count(ba) from BankAccount ba join ba.user user where user.id = :id 
Quando é executada, a seguinte consulta SQL será criada:
 select count(ba.id) from bank_account ba inner join user u on ba.user_id = u.id where u.id = ? 
O problema aqui não é imediatamente evidente, mesmo com uma vida sábia e um bom entendimento dos desenvolvedores SQL: inner join por chave de usuário excluirá as contas com user_id ausente da seleção (e, de uma maneira boa, a inserção delas deve ser proibida no nível do esquema), o que significa que não é user_id ingressar na tabela do user . preciso. A solicitação pode ser simplificada (e acelerada):
 select count(ba.id) from bank_account ba where ba.user_id = ? 
Existe uma maneira de obter esse comportamento facilmente em c usando o HQL:
 @Query("select count(ba) " + " from BankAccount ba " + " where ba.user.id = :id") long countUserAccounts(@Param("id") Long id); 
Este método cria uma solicitação "Lite".
Resumo de consulta vs. método
Um dos principais recursos do Spring Data é a capacidade de criar uma consulta a partir do nome do método, o que é muito conveniente, especialmente em combinação com o complemento inteligente do IntelliJ IDEA. A consulta descrita no exemplo anterior pode ser facilmente reescrita:
 
Parece ser mais simples, mais curto, mais legível e mais importante - você não precisa examinar a própria solicitação. Li o nome do método - e já está claro o que ele escolhe e como. Mas o diabo está aqui nos detalhes. A consulta final para o método marcado com @Query já vimos. O que acontecerá no segundo caso?
Babah! select count(ba.id) from bank_account ba left outer join // < 
 "Que diabos!?" - o desenvolvedor exclama. Afinal, já vimos isso violinista join não join necessária.
O motivo é prosaico:
Se você ainda não fez o upgrade para as versões corrigidas e a união da tabela diminui a solicitação aqui e agora, não se desespere: existem duas maneiras de aliviar a dor:
- uma boa maneira é adicionar - optional = false(se o circuito permitir):
 
 -  - @Entity public class BankAccount { @Id Long id; @ManyToOne @JoinColumn(name = "user_id", optional = false) User user; }
 
 
- A maneira mais importante é adicionar uma coluna do mesmo tipo que a chave da entidade - Usere usá-la em consultas em vez do campo do- 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; }
 
 - Agora, o request-from-method será melhor: 
 -  - long countByUserId(Long id);
 
 - dá 
 -  - select count(ba.id) from bank_account ba where ba.user_id = ?
 
 - o que conseguimos? 
 
Limite de amostragem
Para nossos propósitos, precisamos limitar a seleção (por exemplo, queremos retornar Optional do método *RepositoryCustom ):
 select ba.* from bank_account ba order by ba.rate limit ? 
Agora 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); } 
O código especificado possui um recurso desagradável: no caso em que a solicitação retornou uma seleção vazia, uma exceção será lançada
 Caused by: javax.persistence.NoResultException: No entity found for query 
Nos projetos que vi, isso foi resolvido de duas maneiras principais:
- try-catch com variações de Optonal.empty()sem exceçãoOptonal.empty()exceção e retornarOptonal.empty()para maneiras mais avançadas, como passar um lambda com uma solicitação para um método utilitário
- aspecto no qual os métodos de repositório retornam Optional
E muito raramente, vi a solução certa:
 @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 faz parte do padrão JPA, enquanto a Session pertence ao Hibernate e é IMHO uma ferramenta mais avançada, que geralmente é esquecida.
[Às vezes] melhoria prejudicial
Quando você precisa obter um pequeno campo de uma entidade "grossa", fazemos o seguinte:
 @Query("select a.available from BankAccount a where a.id = :id") boolean findIfAvailable(@Param("id") Long id); 
A solicitação permite que você obtenha um campo do tipo boolean sem carregar toda a entidade (com a adição de um cache de primeiro nível, verificando alterações no final da sessão e outras despesas). Às vezes, isso não apenas não melhora o desempenho, mas vice-versa - ele cria consultas desnecessárias do zero. Imagine um código que execute algumas verificações:
 @Override @Transactional public boolean checkAccount(Long id) { BankAccount acc = repository.findById(id).orElseThow(NPE::new);  
Esse código faz pelo menos 2 solicitações, embora a segunda possa ser evitada:
 @Override @Transactional public boolean checkAccount(Long id) { BankAccount acc = repository.findById(id).orElseThow(NPE::new);  
A conclusão é simples: não negligencie o cache do primeiro nível, dentro da estrutura de uma transação, apenas o primeiro JpaRepository::findById se refere ao banco de dados, JpaRepository::findById cache do primeiro nível está sempre JpaRepository::findById e está vinculado a uma sessão, que geralmente está vinculada à transação atual.
Testes para jogar (o link para o repositório é fornecido no início do artigo):
- teste de interface estreita: InterfaceNarrowingTest
- teste para um exemplo com uma chave composta: EntityWithCompositeKeyRepositoryTest
- testar excesso CrudRepository::save:ModifierTest.java
- teste cego CrudRepository::findByIdChildServiceImplTest:ChildServiceImplTest
- teste de left joindesnecessário:BankAccountControlRepositoryTest
O custo de uma chamada extra para CrudRepository::save pode ser calculado usando RedundantSaveBenchmark . É lançado usando a classe BenchmarkRunner .