【问题标题】:Hibernate: best practice to pull all lazy collectionsHibernate:拉取所有惰性集合的最佳实践
【发布时间】:2025-11-28 05:55:01
【问题描述】:

我有什么:

@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) 在会话关闭之前,强制休眠拉取惰性集合

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

....

b) 也许更优雅的方法是使用@Fetch(FetchMode.SUBSELECT) 注释

问题:

什么是最佳实践/常见方式/更优雅的方式?意味着将我的对象转换为 JSON。

【问题讨论】:

    标签: java hibernate lazy-loading


    【解决方案1】:

    @Transactional 中使用Hibernate.initialize() 来初始化惰性对象。

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

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

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

    【讨论】:

    • 看起来很吸引人)。据我了解,如果我将使用@Fetch(FetchMode.SUBSELECT),那么我只能调用一次 Hibernate.initialize 来提取所有集合。我说的对吗?
    • 当您检索 MyEntity 的集合时,您如何管理?
    • 如果您在事务中对集合调用任何方法,如“size()”,它也会对其进行初始化,因此您在初始化后的示例不是最好的。这就是说,“Hibernate.initialize(...)”在语义上比 collection.size() 更好,所以你有最好的建议。
    【解决方案2】:

    您可以在同一个事务中遍历 Hibernate 对象的 Getter,以确保使用以下 generic 辅助类快速获取所有惰性子对象:

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

    【讨论】:

    • 感谢您的回答。我知道这已经有一段时间了,但我试图解决这个问题,直到我在这里阅读你的代码之前进展缓慢。我还在第二个方法的开头添加了 ifs initializeObject(object, seenObjects, insidePackageName): if (object instanceof List) { for(Object item : (List&lt;Object&gt;) object) { initializeObject(item, seenObjects, insidePackageName); } return; } else if (object instanceof Set) { for(Object item : (Set&lt;Object&gt;) object) { initializeObject(item, seenObjects, insidePackageName); } return; } Iterate lists else ignores.
    • 如果在 o.getClass().getMethods(); 处抛出 SecurityException 怎么办?
    【解决方案3】:

    不是最好的解决方案,但这是我得到的:

    1) 用这个注解来注解你想要初始化的getter:

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

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

        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。每次我只为我的一个实体运行程序时,我都会在调用 session.refresh 后加载 LazyInitializationException 和其他集合。怎么会这样
    【解决方案4】:

    放置 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。
    【解决方案5】:

    Hibernate 4.1.6 引入了一个新特性来处理这些惰性关联问题。当您在 hibernate.properties 或 hibernate.cfg.xml 中启用 hibernate.enable_lazy_load_no_trans 属性时,您将不再有 LazyInitializationException。

    更多请参考:https://*.com/a/11913404/286588

    【讨论】:

    【解决方案6】:

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

    1. 加入获取一个集合
    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);
    

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

    【讨论】:

    • 嗨弗拉德,如果我调用 Hibernate#initialize(entity.getSubSet()) 如果 getSubSet 返回 Collections.unmodifyableSet(this.subSet),它是否有效。我试过了,但没有。底层集合是“PersistentSet”。打电话给#size()
    • 但也许问题是我后来调用 contains 而我的 equals 使用直接字段访问而不是 getter..
    • 如果您按照我的回答中提供的步骤操作,它会起作用。
    【解决方案7】:

    这可能还没有达到最佳实践,但我通常在集合上调用 SIZE 以在同一事务中加载子项,就像您建议的那样。它很干净,不受子元素结构的任何更改的影响,并且生成的 SQL 开销很低。

    【讨论】:

      【解决方案8】:

      如果您使用 jpa 存储库, 设置 properties.put("hibernate.enable_lazy_load_no_trans",true);到 jpaPropertymap

      【讨论】:

        【解决方案9】:

        对于 JPA-Hibernate 中的惰性集合存在某种误解。首先让我们明确一点 为什么尝试读取惰性集合会引发异常,而不仅仅是返回 NULL 以进行转换或进一步用例?.

        这是因为数据库中的空字段,尤其是连接列中的空字段具有意义,而不仅仅是像编程语言那样的未呈现状态。 当您尝试将惰性集合解释为 Null 值时,这意味着(在 Datastore 端)这些实体之间没有关系,这不是真的。所以抛出异常是某种最佳实践,你必须处理它而不是 Hibernate。

        所以如上所述,我建议:

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

        也如其他答案中所述,有很多方法(急切获取、加入等)或库和方法可以做到这一点,但在处理和解决问题之前,您必须建立对正在发生的事情的看法。

        【讨论】:

          【解决方案10】:

          您可以对您的实体使用@NamedEntityGraph 注释来创建一个可加载的查询,以设置您要在查询中加载哪些集合。

          这种方法的主要优点是,只有当您选择使用此图时,hibernate 才会进行一次查询来检索实体及其集合,如下所示:

          实体配置

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

          用法

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

          【讨论】:

            【解决方案11】:

            尝试使用Gson库将对象转换为Json

            servlet 示例:

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

            【讨论】: