JPA: qual é o padrão adequado para iterar em grandes conjuntos de resultados?

114

Digamos que eu tenha uma tabela com milhões de linhas. Usando JPA, qual é a maneira correta de iterar em uma consulta nessa tabela, de forma que eu não tenha uma lista inteira na memória com milhões de objetos?

Por exemplo, suspeito que o seguinte explodirá se a mesa for grande:

List<Model> models = entityManager().createQuery("from Model m", Model.class).getResultList();

for (Model model : models)
{
     System.out.println(model.getId());
}

A paginação (em loop e atualização manual setFirstResult()/ setMaxResult()) é realmente a melhor solução?

Editar : o principal caso de uso que estou visando é uma espécie de trabalho em lote. Tudo bem se demorar muito para ser executado. Não há cliente da web envolvido; Eu só preciso "fazer algo" para cada linha, uma (ou um pequeno N) de cada vez. Estou apenas tentando evitar tê-los todos na memória ao mesmo tempo.

George Armhold
fonte
Qual banco de dados e driver JDBC você está usando?

Respostas:

55

A página 537 do Java Persistence with Hibernate dá uma solução usando ScrollableResults, mas infelizmente é apenas para Hibernate.

Portanto, parece que usar setFirstResult/ setMaxResultse iteração manual é realmente necessário. Esta é minha solução usando JPA:

private List<Model> getAllModelsIterable(int offset, int max)
{
    return entityManager.createQuery("from Model m", Model.class).setFirstResult(offset).setMaxResults(max).getResultList();
}

então, use-o assim:

private void iterateAll()
{
    int offset = 0;

    List<Model> models;
    while ((models = Model.getAllModelsIterable(offset, 100)).size() > 0)
    {
        entityManager.getTransaction().begin();
        for (Model model : models)
        {
            log.info("do something with model: " + model.getId());
        }

        entityManager.flush();
        entityManager.clear();
        em.getTransaction().commit();
        offset += models.size();
    }
}
George Armhold
fonte
33
Acho que o exemplo não é seguro se houver novas inserções durante o processo em lote. O usuário deve fazer o pedido com base em uma coluna onde tenha certeza de que os dados recém-inseridos estarão no final da lista de resultados.
Balazs Zsoldos
quando a página atual é a última página e tem menos de 100 elementos, a verificação size() == 100irá pular uma consulta adicional que retorna uma lista vazia
cdalxndr
38

Tentei as respostas apresentadas aqui, mas JBoss 5.1 + MySQL Connector / J 5.1.15 + Hibernate 3.3.2 não funcionou com eles. Acabamos de migrar do JBoss 4.x para o JBoss 5.1, portanto, continuamos com ele por enquanto e, portanto, o Hibernate mais recente que podemos usar é o 3.3.2.

Adicionar alguns parâmetros extras funcionou, e um código como este é executado sem OOMEs:

        StatelessSession session = ((Session) entityManager.getDelegate()).getSessionFactory().openStatelessSession();

        Query query = session
                .createQuery("SELECT a FROM Address a WHERE .... ORDER BY a.id");
        query.setFetchSize(Integer.valueOf(1000));
        query.setReadOnly(true);
        query.setLockMode("a", LockMode.NONE);
        ScrollableResults results = query.scroll(ScrollMode.FORWARD_ONLY);
        while (results.next()) {
            Address addr = (Address) results.get(0);
            // Do stuff
        }
        results.close();
        session.close();

As linhas cruciais são os parâmetros de consulta entre createQuery e scroll. Sem eles, a chamada "scroll" tenta carregar tudo na memória e nunca termina ou executa para OutOfMemoryError.

Zds
fonte
2
Olá Zds, seu caso de uso de escanear milhões de linhas é certamente comum para mim, e OBRIGADO por postar o código final. No meu caso, estou colocando registros no Solr, para indexá-los para pesquisa de texto completo. E, devido às regras de negócios que não irei abordar, preciso ir via Hibernate, em vez de usar apenas os módulos integrados do JDBC ou do Solr.
Mark Bennett
Feliz por ajudar :-). Também estamos lidando com grandes conjuntos de dados, neste caso permitindo ao usuário consultar todos os nomes de ruas dentro da mesma cidade / condado, ou às vezes até mesmo estado, então a criação de índices requer a leitura de muitos dados.
Zds de
Aparece com o MySQL, você realmente tem que passar por todos esses obstáculos: stackoverflow.com/a/20900045/32453 (outros bancos de dados podem ser menos rigorosos, imagino ...)
rogerdpack
32

Você realmente não pode fazer isso em JPA direto, entretanto o Hibernate tem suporte para sessões sem estado e conjuntos de resultados roláveis.

Rotineiramente processamos bilhões de linhas com sua ajuda.

Aqui está um link para a documentação: http://docs.jboss.org/hibernate/core/3.3/reference/en/html/batch.html#batch-statelesssession

Cyberax
fonte
17
Obrigado. É bom saber que alguém está fazendo bilhões de linhas através do Hibernate. Algumas pessoas aqui estão dizendo que é impossível. :-)
George Armhold
2
É possível adicionar um exemplo aqui também? Presumo que seja semelhante ao exemplo do Zds?
rogerdpack
19

Para ser honesto, eu sugeriria deixar o JPA e ficar com o JDBC (mas certamente usando a JdbcTemplateclasse de suporte ou algo semelhante). O JPA (e outros provedores / especificações ORM) não foi projetado para operar em muitos objetos dentro de uma transação, pois eles assumem que tudo carregado deve permanecer no cache de primeiro nível (daí a necessidade de clear()no JPA).

Também estou recomendando uma solução de nível mais baixo porque a sobrecarga do ORM (a reflexão é apenas a ponta de um iceberg) pode ser tão significativa, que iterar sobre o plano ResultSet, mesmo usando algum suporte leve como mencionado JdbcTemplate, será muito mais rápido.

O JPA simplesmente não foi projetado para executar operações em uma grande quantidade de entidades. Você pode brincar com flush()/ clear()para evitar OutOfMemoryError, mas considere isso mais uma vez. Você ganha muito pouco pagando o preço do enorme consumo de recursos.

Tomasz Nurkiewicz
fonte
A vantagem do JPA não é apenas agnóstico de banco de dados, mas a possibilidade de nem mesmo usar um banco de dados tradicional (NoSQL). Não é muito difícil limpar / limpar de vez em quando e geralmente as operações em lote são feitas com pouca frequência.
Adam Gent
1
Oi Thomasz. Tenho muitos motivos para reclamar do JPA / Hibernate, mas, respeitosamente, realmente duvido que eles "não sejam projetados para operar em muitos objetos". Suspeito que só preciso aprender o padrão adequado para este caso de uso.
George Armhold
4
Bem, só consigo pensar em dois padrões: paginações (mencionadas várias vezes) e flush()/ clear(). O primeiro é IMHO não projetado para fins de processamento em lote, enquanto usando a sequência de flush () / clear () cheira a abstração com vazamento .
Tomasz Nurkiewicz
Sim, foi uma combinação de paginação e flush / clear, como você mencionou. Obrigado!
George Armhold
7

Se você usar EclipseLink I 'usando este método para obter o resultado como Iterable

private static <T> Iterable<T> getResult(TypedQuery<T> query)
{
  //eclipseLink
  if(query instanceof JpaQuery) {
    JpaQuery<T> jQuery = (JpaQuery<T>) query;
    jQuery.setHint(QueryHints.RESULT_SET_TYPE, ResultSetType.ForwardOnly)
       .setHint(QueryHints.SCROLLABLE_CURSOR, true);

    final Cursor cursor = jQuery.getResultCursor();
    return new Iterable<T>()
    {     
      @SuppressWarnings("unchecked")
      @Override
      public Iterator<T> iterator()
      {
        return cursor;
      }
    }; 
   }
  return query.getResultList();  
}  

fechar Método

static void closeCursor(Iterable<?> list)
{
  if (list.iterator() instanceof Cursor)
    {
      ((Cursor) list.iterator()).close();
    }
}
user2008477
fonte
6
Belo objeto jQuery
usr-local-ΕΨΗΕΛΩΝ
Eu tentei seu código, mas ainda obtenho OOM - parece que todos os objetos T (e todos os objetos de tabela unidos referidos de T) nunca são GC. A criação de perfil mostra que eles são referenciados a partir da "tabela" em org.eclipse.persistence.internal.sessions.RepeatableWriteUnitOfWork junto com org.eclipse.persistence.internal.identitymaps.CacheKey. Eu olhei no cache e minhas configurações são todas padrão (Desativar seletivo, Fraco com subcache flexível, Tamanho do cache 100, Eliminar invalidar). Analisarei as sessões de desativação e verei se isso ajuda. BTW, eu simplesmente itero sobre o cursor de retorno usando "para (T o: resultados)".
Edi Bice
Badum tssssssss
dctremblay
5

Depende do tipo de operação que você precisa fazer. Por que você está repetindo um milhão de linhas? Você está atualizando algo no modo em lote? Você vai exibir todos os registros para um cliente? Você está computando algumas estatísticas sobre as entidades recuperadas?

Se você vai exibir um milhão de registros para o cliente, reconsidere sua interface de usuário. Nesse caso, a solução apropriada é paginar seus resultados e usar setFirstResult()e setMaxResult().

Se você lançou uma atualização de uma grande quantidade de registros, é melhor manter a atualização simples e de uso Query.executeUpdate(). Opcionalmente, você pode executar a atualização no modo assíncrono usando um Message-Driven Bean oa Work Manager.

Se estiver computando algumas estatísticas sobre as entidades recuperadas, você pode tirar vantagem das funções de agrupamento definidas pela especificação JPA.

Para qualquer outro caso, seja mais específico :)

frm
fonte
Simplesmente, preciso fazer algo "para cada" linha. Certamente, este é um caso de uso comum. No caso específico em que estou trabalhando agora, preciso consultar um serviço da web externo que está totalmente fora do meu banco de dados, usando um id (o PK) de cada linha. Os resultados não são exibidos em nenhum navegador da Web do cliente, portanto, não há interface de usuário digna de menção. Em outras palavras, é um trabalho em lote.
George Armhold
Se você "precisa" do id de impressão para cada linha, não há outra maneira de obter cada linha, obter o id e imprimir. A melhor solução depende do que você precisa fazer.
Dainius
@Caffeine Coma, se você só precisa do id de cada linha, a maior melhoria provavelmente viria apenas de buscar aquela coluna, SELECT m.id FROM Model me então iterar em um List <Integer>.
Jörn Horstmann
1
@ Jörn Horstmann- se houver milhões de linhas, isso realmente importa? Meu ponto é que um ArrayList com milhões de objetos (embora pequenos) não será bom para o heap da JVM.
George Armhold
@Dainius: minha pergunta é realmente: "como posso iterar em cada linha, sem ter todo o ArrayList na memória?" Em outras palavras, eu gostaria de uma interface para extrair N de cada vez, onde N é significativamente menor que 1 milhão. :-)
George Armhold
5

Não há "apropriado" o que fazer isso, isso não é o que JPA ou JDO ou qualquer outro ORM se destina a fazer, JDBC direto será sua melhor alternativa, pois você pode configurá-lo para trazer de volta um pequeno número de linhas em uma vez e esvazie-os à medida que são usados, é por isso que existem cursores do lado do servidor.

As ferramentas ORM não são projetadas para processamento em massa, elas são projetadas para permitir que você manipule objetos e tente fazer com que o RDBMS em que os dados são armazenados seja o mais transparente possível, a maioria falha na parte transparente pelo menos em algum grau. Nessa escala, não há como processar centenas de milhares de linhas (Objetos), muito menos milhões com qualquer ORM e executá-lo em qualquer período de tempo razoável por causa do overhead de instanciação do objeto, puro e simples.

Use a ferramenta apropriada. JDBC direto e procedimentos armazenados definitivamente têm um lugar em 2011, especialmente no que eles fazem melhor em comparação com essas estruturas ORM.

Puxar um milhão de qualquer coisa, mesmo de uma forma simples, List<Integer>não será muito eficiente, independentemente de como você o faz. A maneira correta de fazer o que você está pedindo é simples SELECT id FROM table, defina como SERVER SIDE(dependente do fornecedor) e coloque o cursor em FORWARD_ONLY READ-ONLYe itere sobre isso.

Se você realmente está puxando milhões de ids para processar chamando algum servidor da web com cada um, você terá que fazer algum processamento simultâneo para que isso seja executado em um período de tempo razoável. Puxar com um cursor JDBC e colocar alguns deles por vez em um ConcurrentLinkedQueue e ter um pequeno pool de threads (# CPU / Cores + 1) puxar e processá-los é a única maneira de completar sua tarefa em uma máquina com qualquer " "normal" de RAM, visto que você já está ficando sem memória.

Veja esta resposta também.

Comunidade
fonte
1
Então você está dizendo que nenhuma empresa precisa visitar todas as linhas de sua mesa de usuários? Seus programadores simplesmente jogam o Hibernate pela janela quando chega a hora de fazer isso? " Não há nenhuma maneira de processar centenas de milhares de linhas " - na minha pergunta que eu apontou setFirstResult / setMaxResult, tão claramente não é um caminho. Estou perguntando se existe um melhor.
George Armhold
"Pegar um milhão de qualquer coisa, mesmo em um simples List <Integer> não vai ser muito eficiente, independentemente de como você faz isso." Esse é exatamente o meu ponto. Estou perguntando como não criar a lista gigante, mas sim como iterar em um conjunto de resultados.
George Armhold
Use uma instrução select JDBC simples e direta com FORWARD_ONLY READ_ONLY com um cursor SERVER_SIDE como sugeri em minha resposta. Como fazer o JDBC usar um cursor SERVER_SIDE depende do driver do banco de dados.
1
Eu concordo totalmente com a resposta. A melhor solução depende do problema. Se o problema for carregar algumas entidades facilmente, o JPA é bom. Se o problema é usar grandes quantidades de dados de forma eficiente, o JDBC direto é melhor.
extrano
4
A varredura de milhões de registros é comum por vários motivos, por exemplo, indexá-los em um mecanismo de pesquisa. E embora eu concorde que JDBC é normalmente uma rota mais direta, às vezes você entra em um projeto que já tem uma lógica de negócios muito complexa agrupada em uma camada do Hibernate. Se você o ignorar e for para o JDBC, irá ignorar a lógica de negócios, que às vezes não é trivial para reimplementar e manter. Quando as pessoas postam perguntas sobre casos de uso atípicos, elas geralmente sabem que é um pouco estranho, mas podem estar herdando algo em vez de construir do zero e talvez não consigam revelar detalhes.
Mark Bennett
4

Você pode usar outro "truque". Carregue apenas a coleção de identificadores das entidades nas quais você está interessado. Digamos que o identificador seja do tipo long = 8 bytes, então 10 ^ 6 uma lista de tais identificadores totaliza cerca de 8 MB. Se for um processo em lote (uma instância por vez), é suportável. Em seguida, apenas itere e faça o trabalho.

Outra observação - você deve fazer isso em partes - especialmente se modificar os registros, caso contrário, o segmento de rollback no banco de dados aumentará.

Quando se trata de definir a estratégia firstResult / maxRows - será MUITO, MUITO lento para resultados distantes do topo.

Também leve em consideração que o banco de dados provavelmente está operando em isolamento de leitura confirmada , para evitar leituras fantasmas, carregue os identificadores e carregue as entidades uma a uma (ou 10 por 10 ou o que for).

Marcin Cinik
fonte
Olá @Marcin, você ou qualquer outra pessoa pode fornecer um link para um exemplo de código aplicando esta abordagem em partes e id-first stepwise, de preferência usando fluxos Java8?
Krevelen
2

Fiquei surpreso ao ver que o uso de procedimentos armazenados não foi mais proeminente nas respostas aqui. No passado, quando eu tinha que fazer algo assim, eu crio um procedimento armazenado que processa dados em pequenos pedaços, depois dorme um pouco e depois continua. O motivo para dormir é não sobrecarregar o banco de dados, que provavelmente também está sendo usado para tipos de consultas em tempo real, como a conexão a um site. Se não houver mais ninguém usando o banco de dados, você pode deixar de dormir. Se você precisar garantir que processa cada registro uma vez e apenas uma vez, precisará criar uma tabela (ou campo) adicional para armazenar quais registros você processou a fim de ser resiliente nas reinicializações.

As economias de desempenho aqui são significativas, possivelmente ordens de magnitude mais rápidas do que qualquer coisa que você pudesse fazer em JPA / Hibernate / AppServer, e seu servidor de banco de dados provavelmente terá seu próprio tipo de mecanismo de cursor do lado do servidor para processar grandes conjuntos de resultados com eficiência. A economia de desempenho vem de não ter que enviar os dados do servidor de banco de dados para o servidor de aplicativos, onde você processa os dados e depois os envia de volta.

Existem algumas desvantagens significativas em usar procedimentos armazenados que podem descartar completamente isso para você, mas se você tiver essa habilidade em sua caixa de ferramentas pessoal e puder usá-la neste tipo de situação, você pode eliminar esses tipos de coisas rapidamente .

Perigo
fonte
1
-2 downvotes - o próximo downvoter poderia defender seu downvote?
Perigo
1
Eu pensei a mesma coisa enquanto lia isso. A pergunta indica um trabalho em lote de alto volume sem IU. Supondo que você não precise de recursos específicos do servidor de aplicativos, por que usar um servidor de aplicativos? O procedimento armazenado seria muito mais eficiente.
jdessey
@jdessey Dependendo da situação, digamos que temos um recurso de importação onde, na importação, ele deve fazer algo com alguma outra parte do sistema, por exemplo, adicionar linhas a outra tabela com base em algumas regras de negócios que já foram codificadas como EJB. Então, executar em um servidor de aplicativos faria mais sentido, a menos que você consiga fazer o EJB funcionar em um modo integrado.
Archimedes Trajano
1

Para expandir a resposta de @Tomasz Nurkiewicz. Você tem acesso ao DataSourceque, por sua vez, pode fornecer uma conexão

@Resource(name = "myDataSource",
    lookup = "java:comp/DefaultDataSource")
private DataSource myDataSource;

Em seu código você tem

try (Connection connection = myDataSource.getConnection()) {
    // raw jdbc operations
}

Isso permitirá que você ignore o JPA para algumas operações de lote grandes específicas, como importação / exportação, no entanto, você ainda terá acesso ao gerenciador de entidade para outras operações JPA, se precisar.

Arquimedes Trajano
fonte
0

Use o Paginationconceito para recuperar o resultado

Programador Morto
fonte
4
A paginação é muito boa para GUI's. Mas, para processar grandes quantidades de dados, o ScrollableResultSet foi inventado há muito tempo. Simplesmente não está no JPA.
extraneon
0

Eu mesmo me perguntei isso. Parece importar:

  • quão grande é o seu conjunto de dados (linhas)
  • qual implementação JPA você está usando
  • que tipo de processamento você está fazendo para cada linha.

Eu escrevi um Iterator para facilitar a troca de ambas as abordagens (findAll vs findEntries).

Eu recomendo que você tente ambos.

Long count = entityManager().createQuery("select count(o) from Model o", Long.class).getSingleResult();
ChunkIterator<Model> it1 = new ChunkIterator<Model>(count, 2) {

    @Override
    public Iterator<Model> getChunk(long index, long chunkSize) {
        //Do your setFirst and setMax here and return an iterator.
    }

};

Iterator<Model> it2 = List<Model> models = entityManager().createQuery("from Model m", Model.class).getResultList().iterator();


public static abstract class ChunkIterator<T> 
    extends AbstractIterator<T> implements Iterable<T>{
    private Iterator<T> chunk;
    private Long count;
    private long index = 0;
    private long chunkSize = 100;

    public ChunkIterator(Long count, long chunkSize) {
        super();
        this.count = count;
        this.chunkSize = chunkSize;
    }

    public abstract Iterator<T> getChunk(long index, long chunkSize);

    @Override
    public Iterator<T> iterator() {
        return this;
    }

    @Override
    protected T computeNext() {
        if (count == 0) return endOfData();
        if (chunk != null && chunk.hasNext() == false && index >= count) 
            return endOfData();
        if (chunk == null || chunk.hasNext() == false) {
            chunk = getChunk(index, chunkSize);
            index += chunkSize;
        }
        if (chunk == null || chunk.hasNext() == false) 
            return endOfData();
        return chunk.next();
    }

}

Acabei não usando meu iterador de chunk (então pode não ter sido testado). A propósito, você precisará de coleções do Google se quiser usá-lo.

Adam Gent
fonte
Em relação a "que tipo de processamento você está fazendo para cada linha" - se o número de linhas estiver na casa dos milhões, suspeito que mesmo um objeto simples com apenas uma coluna de id causará problemas. Também pensei em escrever meu próprio Iterador que envolvesse setFirstResult / setMaxResult, mas percebi que esse deve ser um problema comum (e, espero, resolvido!).
George Armhold
@Caffeine Coma Eu postei meu Iterador, você provavelmente poderia fazer mais alguma adaptação do JPA a ele. Me diga se isso ajuda. Acabei não usando (fiz um findAll).
Adam Gent
0

Com a hibernação, existem 4 maneiras diferentes de conseguir o que você deseja. Cada um tem compensações, limitações e consequências de design. Sugiro explorar cada um e decidir qual é o certo para a sua situação.

  1. Use a sessão sem estado com scroll ()
  2. Use session.clear () após cada iteração. Quando outras entidades precisarem ser conectadas, carregue-as em uma sessão separada. efetivamente, a primeira sessão está emulando a sessão sem estado, mas mantendo todos os recursos de uma sessão com estado, até que os objetos sejam desconectados.
  3. Use iterate () ou list (), mas obtenha apenas ids na primeira consulta, então em uma sessão separada em cada iteração, faça session.load e feche a sessão no final da iteração.
  4. Use Query.iterate () com EntityManager.detach () também conhecido como Session.evict ();
Larry Chu
fonte
0

Aqui está um exemplo JPA simples e direto (em Kotlin) que mostra como você pode paginar sobre um conjunto de resultados arbitrariamente grande, lendo pedaços de 100 itens por vez, sem usar um cursor (cada cursor consome recursos no banco de dados). Ele usa paginação de conjunto de chaves.

Consulte https://use-the-index-luke.com/no-offset para o conceito de paginação de conjunto de chaves e https://www.citusdata.com/blog/2016/03/30/five-ways-to- paginar / para uma comparação de diferentes maneiras de paginar junto com suas desvantagens.

/*
create table my_table(
  id int primary key, -- index will be created
  my_column varchar
)
*/

fun keysetPaginationExample() {
    var lastId = Integer.MIN_VALUE
    do {

        val someItems =
        myRepository.findTop100ByMyTableIdAfterOrderByMyTableId(lastId)

        if (someItems.isEmpty()) break

        lastId = someItems.last().myTableId

        for (item in someItems) {
          process(item)
        }

    } while (true)
}
Elifarley
fonte
0

Um exemplo com JPA e NativeQuery buscando toda vez que o tamanho Elements usando offsets

public List<X> getXByFetching(int fetchSize) {
        int totalX = getTotalRows(Entity);
        List<X> result = new ArrayList<>();
        for (int offset = 0; offset < totalX; offset = offset + fetchSize) {
            EntityManager entityManager = getEntityManager();
            String sql = getSqlSelect(Entity) + " OFFSET " + offset + " ROWS";
            Query query = entityManager.createNativeQuery(sql, X.class);
            query.setMaxResults(fetchSize);
            result.addAll(query.getResultList());
            entityManager.flush();
            entityManager.clear();
        return result;
    }
harryssuperman
fonte