Hibernate: 拉取所有延迟集合的最佳做法

117

我所拥有的:

@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
}

问题:

问题是当会话关闭后,我无法提取懒加载的集合。但我也不能在proceed方法中不关闭会话。

解决方案(粗略解决方案):

a) 在会话关闭之前,强制Hibernate提取懒加载的集合。

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

b) 更加优雅的方式可能是使用@Fetch(FetchMode.SUBSELECT)注解。

问题:

将我的对象转换为JSON,有什么最佳实践/常见方法/更加优雅的方式吗?

12个回答

130

@Transactional中使用Hibernate.initialize()来初始化延迟加载的对象。

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

现在,在事务之外,您可以获取惰性对象。

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

1
看起来很有吸引力。如果我使用@Fetch(FetchMode.SUBSELECT),那么我只需要调用一次Hibernate.initialize就可以拉取所有集合,是吗? - VB_
4
当您检索MyEntity集合时,您如何处理? - Alexis Dufrenoy
1
如果您在事务中调用集合上的任何方法,例如“size()”,它也会初始化该集合,因此您的示例在初始化后并不是最佳的。这意味着,“Hibernate.initialize(...)”比collection.size()更语义化,因此您得到了最好的建议。 - Tristan

8
不是最好的解决方案,但这是我找到的方法: 1)使用此注释对您想要初始化的getter进行注释:
@Retention(RetentionPolicy.RUNTIME)
public @interface Lazy {

}

2) 从数据库读取对象后,可以在泛型类中使用此方法(也可以将T更改为Object类):

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

我在迭代中使用session.refresh来加载lazyCollections。每次运行程序时,只有一个实体会导致LazyInitializationException,其他集合在调用session.refresh后加载。这是怎么发生的? - saba safavi

8

您可以使用以下通用帮助类,在同一事务中遍历Hibernate对象的Getters,以确保所有懒加载子对象都被主动获取:

HibernateUtil.initializeObject(myObject, "my.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;
}
}

谢谢您的回答。我知道已经过了一段时间,但在尝试解决这个问题时一直进展缓慢,直到我在这里阅读了您的代码。我还在第二个方法initializeObject(object,seenObjects,insidePackageName)的开头添加了if语句: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; }迭代列表,否则将被忽略。 - Chip
1
如果在 o.getClass().getMethods(); 抛出 SecurityException,该怎么办? - Oleksii Kyslytsyn
为什么要比较字节而不是字符串? - renanleandrof

6

在会话关闭之前,将Utils.objectToJson(entity);的调用放置在前面。

或者您可以尝试设置获取模式,并按如下方式操作代码

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已经被弃用。现在javadoc建议使用FetchMode.JOIN。 - Alexis Dufrenoy

5

当需要获取多个集合时,您需要:

  1. JOIN FETCH 一次集合
  2. 对于其余的集合使用Hibernate.initialize

所以,在您的情况下,您需要像这样的第一个JPQL查询:

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

这样,您可以通过两个SQL查询实现目标,并避免笛卡尔积。

嗨,弗拉德,如果我调用Hibernate#initialize(entity.getSubSet()),如果getSubSet返回Collections.unmodifyableSet(this.subSet),它是否有效?我尝试了一下,但没有成功。底层集合是'PersistentSet'。调用#size()也是同样的情况。 - Vadim Kirilchuk
但是也许问题在于我后来调用了contains方法,而我的equals方法使用的是直接字段访问而不是getter方法。 - Vadim Kirilchuk
1
如果您按照我的答案提供的步骤操作,它就能正常工作。 - Vlad Mihalcea

4

在Hibernate 4.1.6中引入了一项新功能来处理那些延迟关联问题。当您在hibernate.properties或hibernate.cfg.xml中启用hibernate.enable_lazy_load_no_trans属性时,您将不再遇到LazyInitializationException异常。

更多信息请参见:https://dev59.com/l3RB5IYBdhLWcg3wl4IQ#11913404


6
这实际上是一种反模式。了解更多信息,请参考:https://vladmihalcea.com/the-hibernate-enable_lazy_load_no_trans-anti-pattern/ - Ph03n1x

3

可能不是最佳实践,但我通常会在同一事务中调用集合的 SIZE 方法来加载子项,就像你建议的那样。这样做简洁明了,对子元素结构的任何更改都不受影响,并且生成的 SQL 开销较小。


0

如果您正在使用JPA Repository,将properties.put("hibernate.enable_lazy_load_no_trans",true)设置到jpaPropertymap中。


0

关于JPA-Hibernate中的惰性集合,存在一些误解。首先让我们澄清一下:为什么尝试读取惰性集合会抛出异常而不是简单地返回NULL以进行转换或进一步使用?

这是因为数据库中的空字段,特别是在连接列中,具有意义,而不仅仅是不存在的状态,就像编程语言一样。当您尝试将惰性集合解释为Null值时,它意味着(在数据存储端)这些实体之间没有关系,这是不正确的。因此,抛出异常是一种最佳实践,您必须处理它而不是Hibernate。

因此,如上所述,我建议:

  1. 在修改对象或使用无状态会话进行查询之前分离所需对象
  2. 将惰性字段操作为所需值(零、null等)

另外,正如其他答案中所描述的那样,有很多方法(急切获取、连接等)或库和方法可以做到这一点,但在处理问题并解决问题之前,您必须设置自己的视图。


0
一种使用更多或更少标准JPA的方法是添加。
@NamedEntityGraph(includeAllAttributes = true)

对于您的实体,它将提供一个命名实体图,该实体图与您的实体同名(因为未提供名称)。

然后,假设您已从现有方法中获取了entity,则可以将以下代码应用于它。

// this is needed to ensure the existing 
// object that you have retrieved is disconnected
// from the entity manager, otherwise the find
// method will return the same object.
entityManager.detach(entity); 

// Locate the entity graph based on the class name of the entity.
EntityGraph<?> entityGraph =
  entityManager.getEntityGraph(
    entityManager
      .getMetamodel()
      .entity(entity.getClass()).getName());

var fullyLoadedEntity = entityManager
  .find(
    entity.getClass(), 
    entity.getId(), 
    Map.of(
      // SpecHints is hibernate specific the value is
      // "jakarta.persistence.loadgraph"
      SpecHints.HINT_SPEC_LOAD_GRAPH, entityGraph,
      // This is needed if you're using L2 cache otherwise it will
      // use the cached copy and it may not be complete.
      // "jakarta.persistence.cache.retrieveMode"
      SpecHints.HINT_SPEC_CACHE_RETRIEVE_MODE, CacheRetrieveMode.BYPASS
));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接