防止Dozer触发Hibernate懒加载

18

我正在使用Spring事务,因此在将POJO转换为DTO时事务仍然处于活动状态。

我想防止Dozer触发懒加载,以便隐藏的SQL查询永远不会发生:所有获取操作必须通过HQL显式完成(以获得最佳的性能控制)。

  1. 这是一种好的做法吗(我找不到任何文档)?

  2. 如何安全地实现它?

在DTO转换之前,我尝试了以下操作:

PlatformTransactionManager tm = (PlatformTransactionManager) SingletonFactoryProvider.getSingletonFactory().getSingleton("transactionManager");
tm.commit(tm.getTransaction(new DefaultTransactionDefinition()));

我不知道事务发生了什么,但是Hibernate会话没有关闭,而且仍然发生了延迟加载。

我尝试过这个:

SessionFactory sf = (SessionFactory) SingletonFactoryProvider.getSingletonFactory().getSingleton("sessionFactory");
sf.getCurrentSession().clear();
sf.getCurrentSession().close();

在应用程序层直接操作会话(session),这是否是一个好的实践方式(在我的项目中称为"facade")?我应该担心哪些负面影响?(我已经看到涉及POJO->DTO转换的测试不能再通过AbstractTransactionnalDatasource Spring测试类来运行,因为这些类会尝试在不再与活动会话关联的事务上触发回滚。)

我还尝试将传播(propagation)设置为NOT_SUPPORTED或REQUIRES_NEW,但它会重用当前的Hibernate session,并且无法防止惰性加载。

6个回答

26

在处理这个问题时,我找到的唯一通用解决方案(经过自定义转换器、事件监听器和代理解析器的研究后)是实现一个自定义字段映射器。我发现这个功能被隐藏在Dozer API中(我不认为它在用户指南中有记录)。

以下是一个简单的示例:

public class MyCustomFieldMapper implements CustomFieldMapper 
{
    public boolean mapField(Object source, Object destination, Object sourceFieldValue, ClassMap classMap, FieldMap fieldMapping) 
    {       
        // Check if field is a Hibernate collection proxy
        if (!(sourceFieldValue instanceof AbstractPersistentCollection)) {
            // Allow dozer to map as normal
            return false;
        }

        // Check if field is already initialized
        if (((AbstractPersistentCollection) sourceFieldValue).wasInitialized()) {
            // Allow dozer to map as normal
            return false;
        }

        // Set destination to null, and tell dozer that the field is mapped
        destination = null;
        return true;
    }   
}
这将会把所有未初始化的PersistentSet对象返回为null。我这样做是为了在将它们传递给客户端时,可以区分一个NULL(未加载)集合和一个空集合。这使我能够在客户端中定义通用行为,以便使用预加载的集合,或者进行另一个服务调用以检索集合(如果需要)。此外,如果您决定在服务层内急切地加载任何集合,则它们将像往常一样被映射。
我使用spring来注入自定义字段映射器:
<bean id="dozerMapper" class="org.dozer.DozerBeanMapper" lazy-init="false">
    <property name="mappingFiles">
        ...
    </property>
    <property name="customFieldMapper" ref="dozerCustomFieldMapper" />
</bean>
<bean id="dozerCustomFieldMapper" class="my.project.MyCustomFieldMapper" />

希望这能帮助那些在寻找此问题解决方案时没有在互联网上找到任何示例的人。


谢谢,这太棒了,我甚至可以确认它没有在其他地方记录:http://www.google.fr/search?q=CustomFieldMapper+PersistentSet - Tristan
1
此外,在最近的Dozer版本(5.3.0)中,还有另一种方法可以实现它(http://sourceforge.net/tracker/?func=detail&aid=2993122&group_id=133517&atid=727370)。 - Tristan

10

这个流行版本的变体确保捕获PersistentBags、PersistentSets等所有内容...

public class LazyLoadSensitiveMapper implements CustomFieldMapper {

public boolean mapField(Object source, Object destination, Object sourceFieldValue, ClassMap classMap, FieldMap fieldMapping) {
    //if field is initialized, Dozer will continue mapping

    // Check if field is derived from Persistent Collection
    if (!(sourceFieldValue instanceof AbstractPersistentCollection)) {
        // Allow dozer to map as normal
        return false;
    }

    // Check if field is already initialized
    if (((AbstractPersistentCollection) sourceFieldValue).wasInitialized()) {
        // Allow dozer to map as normal
        return false;
    }

    return true;
}

5

我没有成功地实现上述方法(可能是因为版本不同)。然而,以下方法可以正常工作。

public class HibernateInitializedFieldMapper implements CustomFieldMapper {
    public boolean mapField(Object source, Object destination, Object sourceFieldValue, ClassMap classMap, FieldMap fieldMapping) {
        //if field is initialized, Dozer will continue mapping
        return !Hibernate.isInitialized(sourceFieldValue));
    }
}

4
你是否考虑过完全禁用惰性加载呢?
它似乎并不适合你所想使用的模式:
“我想防止Dozer触发惰性加载,以便隐藏SQL查询从未发生:所有获取都必须通过HQL明确完成(以获得对性能最好的控制)。”
这意味着你永远不会想使用惰性加载。
Dozer和你传递给它的基于Hibernate的bean互相毫不知情;Dozer只知道它正在访问bean中的属性,而基于Hibernate的bean正在响应调用get()以懒加载集合,就像你自己访问这些属性时一样。
在你的bean中引入任何让Dozer认识Hibernate代理或反之亦然的技巧,会破坏你应用程序的层次结构,这是我的看法。
如果你不希望在意料之外的时刻触发任何“隐藏的SQL查询”,那么只需禁用惰性加载即可。

当然,如果可能的话,我想要“禁用延迟加载”,但是如何做到呢?我的意思是,“default-lazy=false”意味着所有关联都将被急切地获取,不是吗? - Tristan
是的,或者你可以在类或属性上指定 lazy="false",这将导致急切获取。你必须使用急切获取或延迟加载之一;你不能使用 Hibernate 映射一个属性/集合,并让它只有在某些时候才被 Hibernate 加载。 - matt b
好的,我的要求是:当调用getter方法时,Hibernate不会自动加载我的集合。只有在HQL查询中明确指定“join”(例如“from Person join fetch Orders”)时,Hibernate才会加载我的集合。你能确认Hibernate不支持这种行为吗?(是否有某种原因...或者其他流行的框架可以适应这种行为?) - Tristan
Hibernate的哲学是在您请求时将对象及所有集合(可能是懒加载)、关联等返回给您。您所描述的更接近于SQL层,您可以对获取和不获取的内容进行一些控制。您最好看看其他数据访问库,如iBatis SqlMaps,它允许您将自己指定的查询结果映射到对象上。 - matt b
抱歉,Matt,有一个更好的答案(请看下面)。 - Tristan

0

使用CustomFieldMapper可能不是一个好主意,因为它会为源类的每个字段调用一次,但我们只关心延迟关联映射(子对象列表),所以我们可以在实体对象的getter方法中设置null值。

public Set<childObject> getChild() {
if(Hibernate.isInitialized(child){
    return childObject;
}else
 return null;
}

这实际上是在所有适用的集合上禁用了惰性加载,而不仅仅是针对VO映射。如果您有任何需要此集合的惰性加载的应用程序内部逻辑,它将只得到null。这个问题特别是关于禁用VO映射的惰性加载,而不是全部禁用。 - JamieB

0

这个mapper的简短版本将会是:

return sourceFieldValue instanceof AbstractPersistentCollection && 
!( (AbstractPersistentCollection) sourceFieldValue ).wasInitialized();

1
我不确定重构代码以使其更难读真的能为这个答案增加价值。 - JamieB

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