JPA: Quel est le modèle approprié pour itérer sur de grands ensembles de résultats?

disons que j'ai un tableau avec des millions de lignes. En utilisant JPA, Quelle est la bonne façon d'itérer une requête contre cette table, de sorte que Je n'ai pas toute une liste en mémoire avec des millions d'objets?

par exemple, je soupçonne que le suivant explosera si la table est grande:

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

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

est-ce que la pagination (boucle et mise à jour manuelle setFirstResult() / setMaxResult() ) est vraiment la meilleure solution?

Edit : le premier cas d'utilisation que je vise est une sorte de travail par lot. C'est bien si il prend du temps pour s'exécuter. Il n'y a pas de client web impliqué; j'ai juste besoin de "faire quelque chose" pour chaque ligne, un (ou un petit N) à la fois. J'essaie juste d'éviter de les avoir tous dans la mémoire en même temps.

102
demandé sur George Armhold 2011-02-21 18:13:30

13 réponses

Page 537 du Java Persistance avec Hibernate donne une solution à l'aide de ScrollableResults , mais hélas c'est uniquement pour la mise en veille prolongée.

il semble donc que l'utilisation de setFirstResult / setMaxResults et itération manuelle est vraiment nécessaire. Voici ma solution en utilisant JPA:

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

alors, utilisez-le comme ceci:

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();
    }
}
52
répondu George Armhold 2015-04-28 09:13:38

j'ai essayé les réponses présentées ici, mais JBoss 5.1 + MySQL Connector/J 5.1.15 + Hibernate 3.3.2 n'a pas fonctionné avec ceux-ci. Nous venons de migrer de JBoss 4.x à JBoss 5.1, donc nous nous en sommes tenus là pour le moment, et donc la dernière hibernation que nous pouvons utiliser est 3.3.2.

ajouter quelques paramètres supplémentaires a fait le travail, et le code comme celui-ci fonctionne sans 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();

les lignes cruciales sont les paramètres de requête entre createQuery et scroll. Sans eux, l'appel" scroll " essaie de tout charger en mémoire et soit ne finit jamais ou court Jusqu'à OutOfMemoryError.

31
répondu Zds 2015-09-07 08:42:27

vous ne pouvez pas vraiment faire cela dans le JPA droit, cependant Hibernate a le soutien pour les sessions apatrides et les ensembles de résultats scrollables.

Nous avons l'habitude processus de des milliards de lignes avec son aide.

voici un lien vers la documentation: http://docs.jboss.org/hibernate/core/3.3/reference/en/html/batch.html#batch-statelesssession

27
répondu Cyberax 2011-10-21 07:54:45

pour être honnête, je suggérerais de quitter JPA et de rester avec JDBC (mais certainement en utilisant JdbcTemplate classe de soutien ou de ce genre). JPA (et d'autres fournisseurs/spécifications D'ORMS) n'est pas conçu pour fonctionner sur de nombreux objets d'une même transaction, car ils supposent que tout ce qui est chargé doit rester dans le cache de premier niveau (d'où la nécessité de clear() dans JPA).

aussi je recommande une solution plus basse de niveau parce que le dessus de ORM (réflexion est seulement une pointe d'un iceberg) pourrait être si important , que itérer sur simple ResultSet , même en utilisant un support léger comme mentionné JdbcTemplate sera beaucoup plus rapide.

JPA est tout simplement pas conçu pour effectuer des opérations sur un grand nombre d'entités. Vous pourriez jouer avec flush() / clear() pour éviter OutOfMemoryError , mais considérez ceci une fois de plus. Vous gagnez très peu à payer le prix d'une énorme consommation de ressources.

16
répondu Tomasz Nurkiewicz 2011-02-21 15:34:47

si vous utilisez EclipseLink i 'en utilisant cette méthode pour obtenir un résultat itérable""

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

méthode close

static void closeCursor(Iterable<?> list)
{
  if (list.iterator() instanceof Cursor)
    {
      ((Cursor) list.iterator()).close();
    }
}
7
répondu user2008477 2013-02-12 14:40:05

Cela dépend du type d'opération que vous avez à faire. Pourquoi êtes-vous en boucle de plus d'un million de ligne? Est-ce que vous mettez à jour quelque chose en mode batch? Allez-vous afficher tous les enregistrements à un client? Calculez-vous des statistiques sur les entités récupérées?

si vous voulez afficher un million de dossiers au client, s'il vous plaît reconsidérez votre interface utilisateur. Dans ce cas, la solution appropriée est la pagination de vos résultats et en utilisant setFirstResult() et setMaxResult() .

si vous avez lancé une mise à jour d'un grand nombre d'enregistrements, Vous feriez mieux de garder la mise à jour simple et d'utiliser Query.executeUpdate() . En option, vous pouvez exécuter la mise à jour en mode asynchrone en utilisant un gestionnaire de travail piloté par un Message.

si vous calculez des statistiques sur les entités récupérées, vous pouvez profiter des fonctions de regroupement définies par la spécification JPA.

pour tout autre cas, s'il vous plaît être plus précis :)

5
répondu frm 2011-02-21 15:27:53

il n'y a pas de" bon " quoi faire, ce n'est pas ce que JPA ou JDO ou tout autre ORM est destiné à faire, JDBC droite sera votre meilleure alternative, comme vous pouvez le configurer pour ramener un petit nombre de lignes à la fois et les rincer comme ils sont utilisés, c'est pourquoi les curseurs côté serveur existent.

outils ORM ne sont pas conçus pour le traitement en vrac, ils sont conçus pour vous permettre de manipuler des objets et tenter de faire les RDBMS que les données sont stockées dans be as transparent que possible, la plupart échouent à la partie transparente au moins dans une certaine mesure. À cette échelle, il n'y a aucun moyen de traiter des centaines de milliers de lignes ( objets), encore moins des millions avec n'importe quel ORM et de le faire exécuter dans un délai raisonnable en raison de l'instanciation de l'objet au-dessus, simple et simple.

utilisez l'outil approprié. Les procédures JDBC et stockées ont certainement une place en 2011, en particulier à ce qu'ils sont mieux à faire contre ces Cadres ORM.

tirer un million de quoi que ce soit, même dans un simple List<Integer> ne va pas être très efficace quelle que soit la façon dont vous le faites. La bonne façon de faire ce que vous demandez est un simple SELECT id FROM table , défini à SERVER SIDE ( dépendant du vendeur ) et le curseur à FORWARD_ONLY READ-ONLY et itérer au-dessus de cela.

si vous tirez vraiment des millions d'id à traiter en appelant un serveur web avec chacun d'eux, vous allez devoir faire certains le traitement simultané aussi bien pour ceci exécuter dans n'importe quelle quantité raisonnable de temps. Tirer avec un curseur JDBC et placer quelques uns d'entre eux à la fois dans une ConcurrentLinkedQueue et avoir un petit pool de threads ( # CPU/Core + 1 ) les tirer et les traiter est le seul moyen de compléter votre tâche sur une machine avec n'importe quelle quantité "normale" de RAM, étant donné que vous êtes déjà en cours d'exécution de la mémoire.

Voir aussi réponse .

4
répondu feeling abused and harassed 2017-05-23 12:03:05

Vous pouvez utiliser un autre "truc". Charger uniquement la collecte des identifiants des entités qui vous intéresse. Say identifier est de type long=8bytes, puis 10^6 une liste de ces identificateurs fait environ 8Mb. Si c'est un procédé par lots (une instance à la fois), alors c'est supportable. Puis il suffit d'itérer et faire le travail.

une autre remarque - vous devriez de toute façon faire cela en morceaux - surtout si vous modifiez des enregistrements, sinon segment de retour en base de données augmentera.

quand il s'agit de définir firthresult/maxRows stratégie - il sera très très très lent pour les résultats loin du sommet.

également prendre en considération que la base de données est probablement en fonctionnement dans lire isolement Commité , donc pour éviter le fantôme lit les identificateurs de charge et puis Charger les entités un par un (ou 10 par 10 ou n'importe quoi).

3
répondu Marcin Cinik 2013-10-16 14:05:25

j'ai été surpris de voir que l'utilisation de procédures stockées n'a pas été plus importante dans les réponses ici. Dans le passé, quand j'ai dû faire quelque chose comme ça, je crée une procédure stockée qui traite les données en petits morceaux, puis dort un peu, puis continue. La raison du sommeil est de ne pas submerger la base de données qui est probablement également utilisé pour des types plus en temps réel de requêtes, comme être connecté à un site web. S'il n'y a personne d'autre qui utilise la base de données, alors vous pouvez laisser le sommeil. Si vous avez besoin de vous assurer que vous traitez chaque enregistrement une fois et une seule fois, alors vous aurez besoin de créer une table (ou un champ) supplémentaire pour stocker les enregistrements que vous avez traités afin d'être résilient à travers les redémarrages.

les économies de performance ici sont importantes, peut-être des ordres de grandeur plus rapide que tout ce que vous pourriez faire dans JPA/Hibernate/AppServer land, et votre serveur de base de données aura très probablement son propre curseur côté serveur type de mécanisme pour le traitement efficace de grands ensembles de résultats. Les économies de performance viennent de ne pas avoir à expédier les données du serveur de base de données au serveur d'application, où vous traitez les données, et ensuite l'expédier en arrière.

il y a des inconvénients importants à utiliser des procédures stockées qui peuvent complètement exclure cela pour vous, mais si vous avez cette compétence dans votre boîte à outils personnelle et peut l'utiliser dans ce genre de situation, vous pouvez assommer ce genre de choses assez rapidement.

1
répondu Danger 2013-02-09 22:08:03

pour développer la réponse de @Tomasz Nurkiewicz. Vous avez accès au DataSource qui à son tour peut vous fournir une connexion

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

dans votre code vous avez

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

cela vous permettra de contourner JPA pour certaines opérations de gros lots spécifiques comme import/export, mais vous avez toujours accès au gestionnaire d'entité pour d'autres opérations JPA si vous en avez besoin.

1
répondu Archimedes Trajano 2015-09-30 22:43:23

Utiliser Pagination Concept pour la récupération du résultat

0
répondu Dead Programmer 2011-02-21 15:40:34

je me le suis moi-même demandé. Il semble à la question:

  • comment grand votre jeu de données (lignes)
  • quelle implémentation JPA vous utilisez
  • quel type de traitement vous faites pour chaque ligne.

j'ai écrit un itérateur pour faciliter l'échange des deux approches (findAll vs findEntries).

je vous recommande d'essayer les deux.

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

}

j'ai fini par ne pas utiliser mon itérateur de morceau (il pourrait donc ne pas être que testé). Par ailleurs, vous aurez besoin de Google collections si vous voulez l'utiliser.

0
répondu Adam Gent 2011-02-21 15:42:10

avec hibernation il y a 4 façons différentes d'atteindre ce que vous voulez. Chacune comporte des compromis, des limites et des conséquences en matière de conception. Je suggère d'explorer chacune et de décider laquelle est la bonne pour votre situation.

  1. Use session apatride with scroll ()
  2. session d'Utilisation.clear() après chaque itération. Lorsque d'autres entités doivent être attachées, chargez-les dans une session séparée. en effet, la première session émule les apatrides session, mais en conservant toutes les fonctionnalités d'une session stateful, jusqu'à ce que les objets soient détachés.
  3. utilisez iterate() ou list () mais obtenez seulement des ID dans la première requête, puis dans une session séparée dans chaque itération, faites session.chargez et fermez la session à la fin de l'itération.
  4. Utiliser La Requête.iterate () avec EntityManager.detach () alias Session.evict();
0
répondu Larry Chu 2014-01-30 04:19:19