web-dev-qa-db-fra.com

JPA: quel est le modèle approprié pour effectuer une itération sur de grands ensembles de résultats?

Disons que j'ai une table avec des millions de lignes. Avec JPA, quelle est la bonne méthode pour parcourir une requête sur cette table, telle que je n'ai pas toute une liste en mémoire avec des millions d'objets?

Par exemple, je soupçonne que ce qui suit va exploser 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());
}

La pagination (mise en boucle et mise à jour manuelle de setFirstResult()/setMaxResult()) est-elle vraiment la meilleure solution?

Edit : le cas d'utilisation principal que je cible est une sorte de travail par lots. C'est bien s'il faut beaucoup de temps pour courir. 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 en mémoire en même temps.

105
George Armhold

La page 537 de Persistance Java avec Hibernate donne une solution utilisant ScrollableResults, mais, hélas, c'est uniquement pour Hibernate. 

Il semble donc que l’utilisation de setFirstResult/setMaxResults et de l’itération manuelle soit vraiment nécessaire. Voici ma solution 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
George Armhold

J'ai essayé les réponses présentées ici, mais JBoss 5.1 + Connecteur MySQL/J 5.1.15 + Hibernate 3.3.2 ne fonctionnait pas avec celles-ci. Nous venons de migrer de JBoss 4.x vers JBoss 5.1, nous nous en tenons donc à cela pour le moment. Le dernier Hibernate que nous pouvons utiliser est donc 3.3.2.

L'ajout de quelques paramètres supplémentaires a fait le travail, et un code comme celui-ci s'exécute sans OOME:

        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 ne finit jamais ou ne tourne jamais vers OutOfMemoryError.

32
Zds

Vous ne pouvez pas vraiment faire cela dans une JPA directe, cependant, Hibernate prend en charge les sessions sans état et les ensembles de résultats défilables.

Nous traitons régulièrement 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

28
Cyberax

Pour être honnête, je suggérerais de quitter JPA et de rester avec JDBC (mais en utilisant certainement JdbcTemplate support class ou similaire). JPA (et les autres fournisseurs/spécifications ORM) n'est pas conçu pour fonctionner sur de nombreux objets au sein d'une transaction, car ils supposent que tout le contenu chargé doit rester dans le cache de premier niveau (d'où la nécessité de clear() dans JPA).

En outre, je recommande une solution plus basse, car les frais généraux de ORM (la réflexion n’est que la partie visible de l’iceberg) pourraient être si importants, qu’itérés sur une simple option ResultSet, même avec un support léger comme mentionné JdbcTemplate sera beaucoup plus rapide.

JPA n'est tout simplement pas conçu pour effectuer des opérations sur un grand nombre d'entités. Vous pouvez jouer avec flush()/clear() pour éviter OutOfMemoryError, mais réfléchissez-y à nouveau. Vous gagnez très peu en payant le prix d'une énorme consommation de ressources.

17
Tomasz Nurkiewicz

Si vous utilisez EclipseLink I 'en utilisant cette méthode pour obtenir un résultat 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();  
}  

méthode close

static void closeCursor(Iterable<?> list)
{
  if (list.iterator() instanceof Cursor)
    {
      ((Cursor) list.iterator()).close();
    }
}
7
user2008477

Cela dépend du type d'opération que vous devez faire. Pourquoi bouclez-vous plus d'un million de lignes? Mettez-vous à jour quelque chose en mode batch? Allez-vous afficher tous les enregistrements sur un client? Calculez-vous des statistiques sur les entités récupérées?

Si vous envisagez d'afficher un million d'enregistrements sur le client, veuillez reconsidérer votre interface utilisateur. Dans ce cas, la solution appropriée consiste à paginer vos résultats et à utiliser setFirstResult() et setMaxResult().

Si vous avez lancé une mise à jour d'un grand nombre d'enregistrements, il est préférable de garder la mise à jour simple et d'utiliser Query.executeUpdate(). Vous pouvez éventuellement exécuter la mise à jour en mode asynchrone à l'aide d'un bean géré par message ou d'un gestionnaire de travail.

Si vous calculez des statistiques sur les entités extraites, vous pouvez tirer parti des fonctions de regroupement définies par la spécification JPA.

Pour tout autre cas, veuillez être plus précis :)

5
frm

Il n’existe pas de solution "appropriée", ce n’est pas ce que JPA, JDO ou tout autre ORM est censé faire. JDBC simple sera votre meilleure alternative, car vous pouvez le configurer pour ramener un petit nombre de lignes à une heure et les vider comme ils sont utilisés, c’est pourquoi les curseurs côté serveur existent.

Les outils ORM ne sont pas conçus pour le traitement en bloc. Ils sont conçus pour vous permettre de manipuler des objets et d'essayer de rendre le SGBDR dans lequel les données sont stockées aussi transparent que possible. La plupart échouent au moins dans une certaine mesure dans la partie transparente. À cette échelle, il n’ya aucun moyen de traiter des centaines de milliers de lignes (Objects), encore moins des millions avec un ORM et de le faire exécuter dans des délais raisonnables en raison de la surcharge d’instanciation d’objet, pure et simple. 

Utilisez l'outil approprié. Straight JDBC et Stored Procedures ont définitivement leur place en 2011, en particulier en ce qui concerne ce qu’ils savent faire de mieux que ces cadres ORM.

Tirer un million de rien, même dans un simple List<Integer>, ne sera pas très efficace, peu importe la façon dont vous le faites. La bonne façon de faire ce que vous demandez est d'utiliser un simple SELECT id FROM table, défini sur SERVER SIDE (en fonction du fournisseur) et le curseur sur FORWARD_ONLY READ-ONLY, suivi de cela.

Si vous devez réellement traiter des millions d'identifiants en appelant un serveur Web avec chacun d'eux, vous devrez également effectuer un traitement simultané pour que celui-ci s'exécute dans un délai raisonnable. Tirer avec un curseur JDBC et en placer quelques-uns à la fois dans une ConcurrentLinkedQueue et disposer d'un petit groupe de threads (# CPU/Cores + 1) les extraire et les traiter est le seul moyen de terminer votre tâche de manière machine avec toute quantité de RAM "normale", étant donné que vous manquez déjà de mémoire.

Voir cette réponse aussi bien.

4
user177800

Vous pouvez utiliser un autre "truc". Ne chargez que la collection d'identifiants des entités qui vous intéressent. Disons que identifiant est de type long = 8 octets, puis 10 ^ 6 une liste de tels identifiants donne environ 8 Mo. S'il s'agit d'un traitement par lots (une instance à la fois), alors c'est supportable. Ensuite, il suffit de parcourir et faire le travail.

Une autre remarque - vous devriez quand même le faire par morceaux - surtout si vous modifiez des enregistrements, sinon segment de restauration dans la base de données augmentera.

Quand il s’agit de définir la stratégie firstResult/maxRows - ce sera TRES TRES lent pour des résultats loin du sommet.

Prenez également en considération le fait que la base de données fonctionne probablement dans read commited isolation , afin d'éviter les lectures fantômes, puis chargez les entités une par une (ou 10 par 10 ou autre).

3
Marcin Cinik

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

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

Dans ton code tu as 

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

Cela vous permettra d'ignorer JPA pour certaines opérations de traitement par lots volumineuses telles que l'importation/exportation, mais vous aurez toujours accès au gestionnaire d'entités pour d'autres opérations JPA si vous en avez besoin.

1
Archimedes Trajano

J'ai été surpris de voir que l'utilisation de procédures stockées n'était pas plus importante dans les réponses fournies ici. Dans le passé, lorsque je devais faire quelque chose comme cela, je créais une procédure stockée qui traitait les données par petits morceaux, puis dormait un peu, puis continuait. La mise en veille a pour but de ne pas submerger la base de données, qui est vraisemblablement également utilisée pour des types de requêtes plus en temps réel, tels que la connexion à un site Web. Si personne d'autre n'utilise la base de données, vous pouvez laisser le sommeil en veille. Si vous devez vous assurer que vous traitez chaque enregistrement une et une seule fois, vous devez créer une table (ou un champ) supplémentaire pour stocker les enregistrements que vous avez traités afin de garantir la résilience lors des redémarrages.

Les économies de performances réalisées ici sont considérables, voire de plusieurs ordres de grandeur, plus rapidement que tout ce que vous pourriez faire dans JPA/Hibernate/AppServer. Votre serveur de base de données disposera probablement de son propre mécanisme de type curseur côté serveur pour traiter efficacement des ensembles de résultats volumineux. Les économies de performances proviennent de la nécessité de ne pas envoyer les données du serveur de base de données au serveur d'applications, où vous traitez les données, puis de les renvoyer.

L'utilisation de procédures stockées peut présenter des inconvénients considérables, mais si vous possédez cette compétence dans votre boîte à outils personnelle et que vous pouvez l'utiliser dans ce genre de situation, vous pouvez éliminer ce type de problème assez rapidement .

1
Danger

Avec Hibernate, il existe 4 façons différentes d’atteindre ce que vous voulez. Chacun a des compromis de conception, des limitations et des conséquences. Je suggère d'explorer chacun et de décider lequel convient à votre situation.

  1. Utiliser une session sans état avec scroll ()
  2. Utilisez session.clear () après chaque itération. Lorsque d'autres entités doivent être attachées, chargez-les dans une session distincte. effectivement, la première session émule la session sans état, mais conserve toutes les fonctionnalités d'une session avec état jusqu'à ce que les objets soient détachés.
  3. Utilisez iterate () ou list () mais obtenez uniquement les identifiants dans la première requête, puis dans une session distincte à chaque itération, effectuez session.load et fermez la session à la fin de l'itération.
  4. Utilisez Query.iterate () avec EntityManager.detach () aussi nommé Session.evict ();
0
Larry Chu

Utiliser Pagination Concept pour récupérer le résultat 

0
Dead Programmer

Je me suis demandé cela moi-même. Cela semble avoir de l'importance:

  • quelle est la taille de votre jeu de données (lignes)
  • quelle implémentation JPA vous utilisez 
  • quel type de traitement que vous effectuez 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 blocs (donc il se peut qu'il ne soit pas testé). En passant, vous aurez besoin de Google Collections si vous voulez l'utiliser.

0
Adam Gent