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)这个可能也行,但没试过)