Hibernate的继承特性:优雅背后的小陷进

本文探讨了Hibernate的三个核心特性:同一Session中的实例唯一加载、代理延迟加载和继承机制。当这些特性在特定场景下结合时,可能导致不期望的行为。在订单系统中,查询库存时遇到代理对象问题,通过激活、手工加载等方式解决。解决方案包括断开Session重新加载或提前加载产品以获取真实子类实例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Hibernate有很多特性,这里不多说,只说几个和本次内容相关的特性。

1、同一Session中的同一实例确保只加载一次
具体来说就是在同一个Session中,调用多次session.get(id, Class) / session.load(id, Class),都只发生一次实际的数据库操作(即第一次调用时访问数据库,后续的访问直接从session的缓存中返回了)。这个特性在多个Service/多个方法的时候尤其便捷,可以不用考虑直接调用get或load方法获取数据,而不用担心是否会对数据库造成性能影响。

2、对不需要的实例使用代理延迟加载
Hibernate 3中,对于@ManyToOne,如果没有手动设置@ManyToOne(fetch=FetchType.LAZY),则每次都会自动加载这个One,如果手动配置了,则只是产生一个代理对象(可认为是占位符)。

3、继承机制自动实例化相应的具体子类实例
继承机制在Hibernate中有N种(N>3)配置方式,但不论是哪种配置方式,在进行get / load (id, Class)时,都能够自动返回相应的具体子类实例,相当的智能

以上3个特性都是非常好的也是大家所熟知的,但当这三者结合在一起在某个特殊的情况下,可能会出现你不希望的情况。

先描述一下我所说的特殊情况:

这是一个订单系统的部分实体(Domain Model)定义,其中产品分为具体产品(ConcreteProduct)和套件产品(SuitProduct),都继承自产品(Product)。订单包括一个或多个订单行,每个订单行记录了某个产品(可能是具体产品也可能是套件产品)购买了多少件。注意,套件产品由2种或以上的具体产品构成(为了描述清晰,每种产品的组成数量都视为1)

这是我说的场景,然后描述一下将要进行的操作:

1、获得指定编号的订单:
Order order = session.get(orderId, Order.class);

2、查看此订单有哪些产品被购买了,购买了多少,是否库存足够。
for (OrderLine orderLine : order.getOrderLines) {
    int availQty = getStock(orderLine.getProduct);
    if (orderLine.getOrderedCount > availQty) {
          // 缺货处理
    }
}

/** 查询库存 */
public int getStock(Product product) { ... }

3、对产品可用库存的判定,对于具体产品就是本身的库存数量;对于套件产品,则是套件所有的组成单元即具体产品中库存最小的那个,因此我们的getStock方法需如下:
public int getStock(Product product) {
    if (product instanceof ConcreteProduct) { // 具体产品
         // 从仓库中获得对应此产品的当前库存数
    } else {
        SuitProduct suitProduct = (SuitProduct) product;
        int minAvailQty = Integer.MAX_VALUE;    
        for (ConcreteProduct concreteProduct : suitProduct.getComposedProducts() ) {   // 获得每个组成的具体产品的当前可用库存
            minAvailQty = Math.min(minAvailQty, this.getStock(concreteProduct ));   // 取小值
        }
        return minAvailQty; // 最少的库存就是套件产品的可用库存
    }
}

场景也描述完了,看上面的代码应该看上去能够执行成功,但在实际运行中,这个getStock(Product)方法的参数,是一个Hibernate生成的代理对象,在后面的代码中其instanceof既不是ConcreteProduct也不是SuitProduct。

进行可能问题分析并排错:

1、既然是代理,那我激活
在调用getStock(Product)方法内部,首先执行product.getName(),进行激活。执行结果:无效

2、手工加载
在调用getStock(Product)方法内部,首先执行session.get(productId, Product.class),执行结果:有SQL语句产生,但对象还是Product而不是我们希望的具体子类,结果:无效

再次分析,orderLine.getProduct()返回的是Hibernate的代理对象,是一个Product的Hibernate的实现,在我们前面使用的1、2方法的激活/加载,只是填充了这个Hibernate的实现中的字段值,但没有改变其是Hibernate的一个代理实现的本质,因此instanceof始终无法成功。

明白了这点后,可以有好多种方法来规避这个问题,以下列举我自己试验过的2个:

1、断开session,然后进行product的加载。由于断开Session后,使用get / load(productId, Product.class)就能够完美的使用Hibernate机制获得一个具体的产品子类。执行结果:成功
此方法的缺点:由于断开了session,因此事务、性能都会存在不同程度的影响;

2、在orderLine.getProduct()操作之前,确保已经调用get / load(productId, Product.class)进行了产品的加载。这个可以通过NativeQuery "select product_id from table_order_line where order_id = :orderId",获得产品编号列表,然后循环调用get / load(productId, Product.class),这样在当前session中就有了真实的具体的产品子类存在,后续的orderLine.getProduct()返回的产品就会从当前Session缓存的实例中返回真实的具体产品子类了。执行结果:成功
此方法的确定:如果在一个较大的事务中,此场景位于中间,之前有过其他的操作导致了产生了代理的Product,则此方法执行到这里,仍然会失败,除非在进入处标明需开启一个新事务@Transactional(propagation=Propagation.REQUIRES_NEW)

其他的方式应该还有,由于本人见识有限无法补足(Session.refresh(Obj)这个可能也行,但没试过)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值