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::findAllById
há uma verificação correspondente) - todas as consultas descritas usando
@Query
são verificadas no estágio de aumentar o contexto, o que leva tempo (diferente de SimpleJpaRepository::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 usar SimpleJpaRepository::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 User
e 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ção Optonal.empty()
exceção e retornar Optonal.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::findById
ChildServiceImplTest
: ChildServiceImplTest
- teste de
left join
desnecessário: BankAccountControlRepositoryTest
O custo de uma chamada extra para CrudRepository::save
pode ser calculado usando RedundantSaveBenchmark
. É lançado usando a classe BenchmarkRunner
.