Hibernate: meilleure pratique pour extraire toutes les collections paresseuses


92

Ce que j'ai:

@Entity
public class MyEntity {
  @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
  @JoinColumn(name = "myentiy_id")
  private List<Address> addreses;

  @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
  @JoinColumn(name = "myentiy_id")
  private List<Person> persons;

  //....
}

public void handle() {

   Session session = createNewSession();
   MyEntity entity = (MyEntity) session.get(MyEntity.class, entityId);
   proceed(session); // FLUSH, COMMIT, CLOSE session!

   Utils.objectToJson(entity); //TROUBLES, because it can't convert to json lazy collections
}

Quel problème:

Le problème est que je ne peux pas extraire la collection différée après la fermeture de la session. Mais je ne peux pas non plus ne pas fermer une session dans la méthode continue .

Quelle solution (solution grossière):

a) Avant la fermeture de la session, forcez la mise en veille prolongée à extraire des collections paresseuses

entity.getAddresses().size();
entity.getPersons().size();

....

b) Une manière peut-être plus élégante est d'utiliser l' @Fetch(FetchMode.SUBSELECT)annotation

Question:

Qu'est-ce qu'une meilleure pratique / une manière courante / une manière plus élégante de le faire? Cela signifie convertir mon objet en JSON.

Réponses:


102

Utilisez Hibernate.initialize()dedans @Transactionalpour initialiser les objets paresseux.

 start Transaction 
      Hibernate.initialize(entity.getAddresses());
      Hibernate.initialize(entity.getPersons());
 end Transaction 

Maintenant, en dehors de la transaction, vous pouvez obtenir des objets paresseux.

entity.getAddresses().size();
entity.getPersons().size();

1
Ça a l'air attrayant). Si je comprends bien, si j'utilise @Fetch (FetchMode.SUBSELECT), je ne peux appeler Hibernate.initialize qu'une seule fois pour extraire toutes les collections. Ai-je raison?
VB_

4
Et comment gérez-vous lorsque vous récupérez une collection de MyEntity?
Alexis Dufrenoy

1
Si vous appelez une méthode comme "size ()" sur une collection dans une transaction, elle l'initialisera également de sorte que votre exemple après votre initialisation n'est pas le meilleur. Ceci dit, "Hibernate.initialize (...)" est sémantiquement meilleur que collection.size (), vous avez donc le meilleur conseil.
Tristan le

7

Vous pouvez parcourir les Getters de l'objet Hibernate dans la même transaction pour vous assurer que tous les objets enfants paresseux sont récupérés avec impatience avec la classe d'assistance générique suivante :

HibernateUtil.initializeObject (monObjet, "mon.app.model");

package my.app.util;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;

import org.aspectj.org.eclipse.jdt.core.dom.Modifier;
import org.hibernate.Hibernate;

public class HibernateUtil {

public static byte[] hibernateCollectionPackage = "org.hibernate.collection".getBytes();

public static void initializeObject( Object o, String insidePackageName ) {
    Set<Object> seenObjects = new HashSet<Object>();
    initializeObject( o, seenObjects, insidePackageName.getBytes() );
    seenObjects = null;
}

private static void initializeObject( Object o, Set<Object> seenObjects, byte[] insidePackageName ) {

    seenObjects.add( o );

    Method[] methods = o.getClass().getMethods();
    for ( Method method : methods ) {

        String methodName = method.getName();

        // check Getters exclusively
        if ( methodName.length() < 3 || !"get".equals( methodName.substring( 0, 3 ) ) )
            continue;

        // Getters without parameters
        if ( method.getParameterTypes().length > 0 )
            continue;

        int modifiers = method.getModifiers();

        // Getters that are public
        if ( !Modifier.isPublic( modifiers ) )
            continue;

        // but not static
        if ( Modifier.isStatic( modifiers ) )
            continue;

        try {

            // Check result of the Getter
            Object r = method.invoke( o );

            if ( r == null )
                continue;

            // prevent cycles
            if ( seenObjects.contains( r ) )
                continue;

            // ignore simple types, arrays und anonymous classes
            if ( !isIgnoredType( r.getClass() ) && !r.getClass().isPrimitive() && !r.getClass().isArray() && !r.getClass().isAnonymousClass() ) {

                // ignore classes out of the given package and out of the hibernate collection
                // package
                if ( !isClassInPackage( r.getClass(), insidePackageName ) && !isClassInPackage( r.getClass(), hibernateCollectionPackage ) ) {
                    continue;
                }

                // initialize child object
                Hibernate.initialize( r );

                // traverse over the child object
                initializeObject( r, seenObjects, insidePackageName );
            }

        } catch ( InvocationTargetException e ) {
            e.printStackTrace();
            return;
        } catch ( IllegalArgumentException e ) {
            e.printStackTrace();
            return;
        } catch ( IllegalAccessException e ) {
            e.printStackTrace();
            return;
        }
    }

}

private static final Set<Class<?>> IGNORED_TYPES = getIgnoredTypes();

private static boolean isIgnoredType( Class<?> clazz ) {
    return IGNORED_TYPES.contains( clazz );
}

private static Set<Class<?>> getIgnoredTypes() {
    Set<Class<?>> ret = new HashSet<Class<?>>();
    ret.add( Boolean.class );
    ret.add( Character.class );
    ret.add( Byte.class );
    ret.add( Short.class );
    ret.add( Integer.class );
    ret.add( Long.class );
    ret.add( Float.class );
    ret.add( Double.class );
    ret.add( Void.class );
    ret.add( String.class );
    ret.add( Class.class );
    ret.add( Package.class );
    return ret;
}

private static Boolean isClassInPackage( Class<?> clazz, byte[] insidePackageName ) {

    Package p = clazz.getPackage();
    if ( p == null )
        return null;

    byte[] packageName = p.getName().getBytes();

    int lenP = packageName.length;
    int lenI = insidePackageName.length;

    if ( lenP < lenI )
        return false;

    for ( int i = 0; i < lenI; i++ ) {
        if ( packageName[i] != insidePackageName[i] )
            return false;
    }

    return true;
}
}

Merci pour cette réponse. Je sais que cela fait un moment, mais j'essayais de résoudre ce problème et cela a été lent jusqu'à ce que je lis votre code ici. J'ai également ajouté ifs au début de la deuxième méthode initializeObject (object, seenObjects, insidePackageName): if (object instanceof List) { for(Object item : (List<Object>) object) { initializeObject(item, seenObjects, insidePackageName); } return; } else if (object instanceof Set) { for(Object item : (Set<Object>) object) { initializeObject(item, seenObjects, insidePackageName); } return; } Iterate lists sinon ignoré.
Puce

Que faire si SecurityException est levée à o.getClass (). GetMethods () ;?
Oleksii Kyslytsyn

6

Ce n'est pas la meilleure solution, mais voici ce que j'ai obtenu:

1) Annotez le getter que vous souhaitez initialiser avec cette annotation:

@Retention(RetentionPolicy.RUNTIME)
public @interface Lazy {

}

2) Utilisez cette méthode (peut être placée dans une classe générique, ou vous pouvez changer T avec la classe Object) sur un objet après l'avoir lu à partir de la base de données:

    public <T> void forceLoadLazyCollections(T entity) {

    Session session = getSession().openSession();
    Transaction tx = null;
    try {

        tx = session.beginTransaction();
        session.refresh(entity);
        if (entity == null) {
            throw new RuntimeException("Entity is null!");
        }
        for (Method m : entityClass.getMethods()) {

            Lazy annotation = m.getAnnotation(Lazy.class);
            if (annotation != null) {
                m.setAccessible(true);
                logger.debug(" method.invoke(obj, arg1, arg2,...); {} field", m.getName());
                try {
                    Hibernate.initialize(m.invoke(entity));
                }
                catch (Exception e) {
                    logger.warn("initialization exception", e);
                }
            }
        }

    }
    finally {
        session.close();
    }
}

J'utilise session.refresh dans une itération pour charger lazyCollections. et chaque fois que j'exécute mon programme uniquement pour l'une de mes entités, la collection LazyInitializationException et autres est chargée après avoir appelé session.refresh. Comment cela pourrait-il arriver
saba safavi

5

Placez le Utils.objectToJson (entité); appeler avant la clôture de la session.

Ou vous pouvez essayer de définir le mode de récupération et de jouer avec un code comme celui-ci

Session s = ...
DetachedCriteria dc = DetachedCriteria.forClass(MyEntity.class).add(Expression.idEq(id));
dc.setFetchMode("innerTable", FetchMode.EAGER);
Criteria c = dc.getExecutableCriteria(s);
MyEntity a = (MyEntity)c.uniqueResult();

FetchMode.EAGER est obsolète. Le javadoc recommande d'utiliser FetchMode.JOIN, maintenant.
Alexis Dufrenoy

4

Avec Hibernate 4.1.6, une nouvelle fonctionnalité est introduite pour gérer ces problèmes d'association paresseux. Lorsque vous activez la propriété hibernate.enable_lazy_load_no_trans dans hibernate.properties ou dans hibernate.cfg.xml, vous n'aurez plus d'exception LazyInitializationException.

Pour en savoir plus, consultez: https://stackoverflow.com/a/11913404/286588


3
C'est en fait un anti-modèle. Pour plus d'informations: vladmihalcea.com
...

3

Lorsque vous devez récupérer plusieurs collections, vous devez:

  1. REJOIGNEZ FETCH une collection
  2. Utilisez le Hibernate.initializepour les collections restantes.

Donc, dans votre cas, vous avez besoin d'une première requête JPQL comme celle-ci:

MyEntity entity = session.createQuery("select e from MyEntity e join fetch e.addreses where e.id 
= :id", MyEntity.class)
.setParameter("id", entityId)
.getSingleResult();

Hibernate.initialize(entity.persons);

De cette façon, vous pouvez atteindre votre objectif avec 2 requêtes SQL et éviter un produit cartésien.


Salut Vlad, ça marche si j'appelle Hibernate#initialize(entity.getSubSet())si getSubSet revient Collections.unmodifyableSet(this.subSet). J'ai essayé et non. La collection sous-jacente est «PersistentSet». Même histoire avec l'appel#size()
Vadim Kirilchuk

Mais peut-être que le problème est que j'appelle plus tard contient et que mes égaux utilisent l'accès direct au champ et non les getters ..
Vadim Kirilchuk

Cela fonctionne si vous suivez les étapes fournies dans ma réponse.
Vlad Mihalcea le

2

Ce n'est probablement pas une bonne pratique, mais j'appelle généralement un SIZEsur la collection pour charger les enfants dans la même transaction, comme vous l'avez suggéré. Il est propre, à l'abri de tout changement dans la structure des éléments enfants et génère du SQL avec une faible surcharge.


0

Essayez d'utiliser la Gsonbibliothèque pour convertir des objets en Json

Exemple avec des servlets:

  List<Party> parties = bean.getPartiesByIncidentId(incidentId);
        String json = "";
        try {
            json = new Gson().toJson(parties);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(json);

0

si vous utilisez le référentiel jpa, définissez properties.put ("hibernate.enable_lazy_load_no_trans", true); à jpaPropertymap


0

Vous pouvez utiliser l' @NamedEntityGraphannotation de votre entité pour créer une requête chargeable définissant les collections que vous souhaitez charger sur votre requête.

Le principal avantage de ce choix est que hibernate effectue une seule requête pour récupérer l'entité et ses collections et uniquement lorsque vous choisissez d'utiliser ce graphe, comme ceci:

Configuration d'entité

@Entity
@NamedEntityGraph(name = "graph.myEntity.addresesAndPersons", 
attributeNodes = {
    @NamedAttributeNode(value = "addreses"),
    @NamedAttributeNode(value = "persons"
})

Usage

public MyEntity findNamedGraph(Object id, String namedGraph) {
        EntityGraph<MyEntity> graph = em.getEntityGraph(namedGraph);

        Map<String, Object> properties = new HashMap<>();
        properties.put("javax.persistence.loadgraph", graph);

        return em.find(MyEntity.class, id, properties);
    }

0

Il y a une sorte de malentendu sur les collections paresseuses dans JPA-Hibernate. Tout d'abord, voyons clairement que pourquoi essayer de lire une collection paresseuse lève des exceptions et pas simplement renvoie NULL pour la conversion ou d'autres cas d'utilisation? .

C'est parce que les champs Null dans les bases de données, en particulier dans les colonnes jointes, ont une signification et pas simplement un état non présenté, comme les langages de programmation. lorsque vous essayez d'interpréter une collection différée à la valeur Null, cela signifie (côté magasin de données) qu'il n'y a pas de relations entre ces entités et ce n'est pas vrai. donc lancer une exception est une sorte de meilleure pratique et vous devez vous en occuper et non avec Hibernate.

Donc, comme mentionné ci-dessus, je recommande de:

  1. Détachez l'objet souhaité avant de le modifier ou d'utiliser une session sans état pour l'interrogation
  2. Manipulez les champs paresseux aux valeurs souhaitées (zéro, nul, etc.)

également comme décrit dans d'autres réponses, il existe de nombreuses approches (recherche impatiente, participation, etc.) ou bibliothèques et méthodes pour le faire, mais vous devez configurer votre vision de ce qui se passe avant de traiter le problème et de le résoudre.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.