Hibernate中OneToOne延时加载的问题-现象和原因
2016-08-16 13:57
405 查看
关于OneToOne的延迟加载(fetch = FetchType.LAZY),我觉得是非常需要注意的一个问题。它不只是存在于Hibernate中,其他的ORM persistence framekwork也有同样的问题,至少我用过的eclipseLink也是一样的。这个问题之所以重要,是因为它在背后偷偷的降低了查询数据库的效率,但又极难发现,特别是当你进入一个老项目,发现软件的运行效率极低时,你就应该检查一下已有代码中是否合理定义了OneToOne的关联。
在Java Persistence with Hibernate的13章有比较清楚的解释,这里举个简单的例子:
假设我们在数据库里定义了Car这种数据类型,每辆Car都关联了Wheel的Object。简单的就是Car和Wheel之间是一种OneToMany的关联。
如果我们需要从数据库里面取出所有的cars。那么典型的ORM框架会做如下的步骤:
首先,取出所有的Cars:
然后针对每一辆Car:
换句话说,ORM用了一条select语句来取出所有的Car,然后用了额外的N条select语句来取出和Car相关联的对象,这是相当损耗效率的一种行为。哪怕我们自己写一条:
将select的执行次数由N+1变为2,然后在内存中自己查找car和wheel,也比执行N+1要强很多。
特别是有的时候,当我们在查询Car的时候,未必需要用到Wheel的数据,这时更是一种额外的开销。
而这里要讨论的OneToOne就是上面所提到的关联实体。
我们先来简单创建两个Entity(Car和SteelingWheel),并在两个Entity之间创建1对1的关联。
Car:
SteelingWheel:
用Spring Boot和Spring JPA Repository写一个简单的测试文件,我们在数据库中创建1000个Car和SteelingWheel的record。然后读取,看看延迟加载是否生效。
先读取SteelingWheel:
加个断点,看看:
我们可以看到只有一条select语句,hibernate采用了延迟加载,对应的关联实体Car的记录没有被取出。
同时在QueryResult中,steelingWheel下面的car是一个javassit的代理类。这个代理类的作用在延迟加载的文章中有解释。
总结一下:
在双向(Bidirectional)的OneToOne关系中,在Owner这一方作查询时,延迟加载是没有问题。
问题出在另一端。
我们调用List cars = carRepository.findAll(); 看看会发生什么。
Hibernate在select car之后,针对cars的每一条record,又执行了
一次:
一共1000次。运行的时间和资源开销大幅上升。从断点处我们也可以看到,car下面的steelingwheel已经从数据库中抓取:
虽然我们在car的定义中明确指定了fetch = FETCHTYPE.LAZY,但在实际的运行过程中它是失效的。如果不注意这个细节,那么在缺乏足够细致的压力测试的情况下,我们就很难在产品发布之前发现这个问题,因为只有在大量数据下这两者才能有明显差别。
那么原因是什么?
在Car表里由于没有关系字段,因此仅从Car表角度看无法知道拥有该Car的SteelingWheel是哪个(除非在Car表建立SteelingWheel表的外键steelingWHeel_id,但由于冗余了关系字段,这样做也会导致写数据库的效率急剧降低,并且增加了不必要的存储空间),而从SteelingWheel角度不一样,由于含有car_id字段可能清楚知道该SteelingWheel拥有一辆Car。
正是由于上面的原因,因此当从Car获取SteelingWheel时,hibernate为了确定SteelingWheel表中到底有没有该Car,因此发了一条sql:select * from SteelingWheel where card_id = ?,参数既是该Car的id,以此来维护Card与SteelingWheel的关系。
还有一种解释,但需要理解hibernate的延迟加载的机制:代理。
2,延迟加载原理
hibernate使用了代理(Proxy),对实体的调用会被代理接受和处理,hibernate可以设置这个代理被调用到的时候去加载数据,从而实现延迟加载。那么对于一个映射对象,要么它有值,要么它是null,对于null值建立代理是没多大作用的,而且也不能对null建立动态代理。那就是说hibernate在对延迟加载建立代理的时候要考虑这个映射的对象是否是null。如果是null不需要建立代理,直接把映射的值设置成null,如果映射的对象不为null,那么hibernate就建立代理对象。
简而言之,为null就不能延迟加载,为代理对象才能延迟加载。所以必须通过select语句确定是否为null。
N+1 Select
我们先简单的回顾一下ORM里面常见的N+1 Select的问题:在Java Persistence with Hibernate的13章有比较清楚的解释,这里举个简单的例子:
假设我们在数据库里定义了Car这种数据类型,每辆Car都关联了Wheel的Object。简单的就是Car和Wheel之间是一种OneToMany的关联。
如果我们需要从数据库里面取出所有的cars。那么典型的ORM框架会做如下的步骤:
首先,取出所有的Cars:
SELECT * FROM Cars;
然后针对每一辆Car:
SELECT * FROM Wheel WHERE CarId = ?
换句话说,ORM用了一条select语句来取出所有的Car,然后用了额外的N条select语句来取出和Car相关联的对象,这是相当损耗效率的一种行为。哪怕我们自己写一条:
SELECT * FROM Wheel
将select的执行次数由N+1变为2,然后在内存中自己查找car和wheel,也比执行N+1要强很多。
特别是有的时候,当我们在查询Car的时候,未必需要用到Wheel的数据,这时更是一种额外的开销。
延迟加载
所以,ORM框架为我们提供了一种机制,延迟加载,只有确定要使用关联对象的时候我们才把它从数据库里面读取出来。Hibernate 的延迟加载本质上就是代理模式的应用,当程序通过 Hibernate 装载一个实体时,默认情况下,Hibernate 并不会立即抓取它的集合属性、关联实体所以对应的记录,而是通过生成一个代理来表示这些集合属性、关联实体,这就是代理模式应用带来的优势。而这里要讨论的OneToOne就是上面所提到的关联实体。
OneToOne无法延迟加载
那OneToOne的延迟加载有什么问题呢?我们先来简单创建两个Entity(Car和SteelingWheel),并在两个Entity之间创建1对1的关联。
Car:
@Entity @Table( name = "car" ) public class Car { @Id @Column( name = "id" ) @GeneratedValue( strategy = GenerationType.AUTO ) Long id; @Column( name = "brand" ) String brand; /*关联SteelingWheel,同时指定延迟加载,当然hibernate是默对关联实体进行延迟加载的*/ @OneToOne( cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "car" ) SteelingWheel steelingWheel; public Long getId() { return id; } public void setId( Long id ) { this.id = id; } public String getBrand() { return brand; } public void setBrand( String brand ) { this.brand = brand; } public SteelingWheel getSteelingWheel() { return steelingWheel; } public void setSteelingWheel( SteelingWheel steelingWheel ) { this.steelingWheel = steelingWheel; } }
SteelingWheel:
@Entity @Table( name = "steeling_wheel" ) public class SteelingWheel { @Id @Column( name = "id" ) @GeneratedValue( strategy = GenerationType.AUTO ) Long id; @Column( name = "type" ) String type; /*关联Car,并且在表中加入car_id作为外键,同时指定延迟加载,当然hibernate是默对关联实体进行延迟加载的*/ @OneToOne( cascade = CascadeType.ALL, fetch = FetchType.LAZY ) @JoinColumn( name = "car_id", nullable = false ) Car car; public Car getCar() { return car; } public void setCar( Car car ) { this.car = car; } public Long getId() { return id; } public void setId( Long id ) { this.id = id; } public String getType() { return type; } public void setType( String type ) { this.type = type; } }
用Spring Boot和Spring JPA Repository写一个简单的测试文件,我们在数据库中创建1000个Car和SteelingWheel的record。然后读取,看看延迟加载是否生效。
先读取SteelingWheel:
@SpringBootTest public class JpaDemoApplicationTests { @Autowired private CarRepository carRepository; @Autowired private SteelingWheelRepository steelingWheelRepository; @Test public void contextLoads() { } @Test public void testOperationContext() throws Exception { for( int i = 0; i < 1000; i++ ) { SteelingWheel steelingWheel = new SteelingWheel(); steelingWheel.setType( "wood" ); Car benz = new Car(); benz.setBrand( "BENZ" ); benz.setSteelingWheel( steelingWheel ); steelingWheel.setCar( benz ); steelingWheelRepository.saveAndFlush( steelingWheel ); carRepository.saveAndFlush( benz ); } System.out.println("\r\n*************start*****************"); List<SteelingWheel> steelingWheelList = steelingWheelRepository.findAll(); //List<Car> cars = carRepository.findAll(); System.out.println("*************end*****************"); } }
加个断点,看看:
我们可以看到只有一条select语句,hibernate采用了延迟加载,对应的关联实体Car的记录没有被取出。
同时在QueryResult中,steelingWheel下面的car是一个javassit的代理类。这个代理类的作用在延迟加载的文章中有解释。
总结一下:
在双向(Bidirectional)的OneToOne关系中,在Owner这一方作查询时,延迟加载是没有问题。
问题出在另一端。
我们调用List cars = carRepository.findAll(); 看看会发生什么。
*************start***************** 13:18:08.015 [main] INFO o.h.h.i.QueryTranslatorFactoryInitiator - HHH000397: Using ASTQueryTranslatorFactory 13:18:08.203 [main] DEBUG org.hibernate.SQL - select car0_.id as id1_0_, car0_.brand as brand2_0_ from car car0_ 13:18:08.238 [main] DEBUG org.hibernate.SQL - select steelingwh0_.id as id1_1_0_, steelingwh0_.car_id as car_id3_1_0_, steelingwh0_.type as type2_1_0_ from steeling_wheel steelingwh0_ where steelingwh0_.car_id=? 13:18:08.238 [main] TRACE o.h.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - [2] 13:18:08.241 [main] DEBUG org.hibernate.SQL - select steelingwh0_.id as id1_1_0_, steelingwh0_.car_id as car_id3_1_0_, steelingwh0_.type as type2_1_0_ from steeling_wheel steelingwh0_ where steelingwh0_.car_id=? 13:18:08.242 [main] TRACE o.h.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - [4] 13:18:08.243 [main] DEBUG org.hibernate.SQL - select steelingwh0_.id as id1_1_0_, steelingwh0_.car_id as car_id3_1_0_, steelingwh0_.type as type2_1_0_ from steeling_wheel steelingwh0_ where steelingwh0_.car_id=? ... 13:18:10.174 [main] DEBUG org.hibernate.SQL - select steelingwh0_.id as id1_1_0_, steelingwh0_.car_id as car_id3_1_0_, steelingwh0_.type as type2_1_0_ from steeling_wheel steelingwh0_ where steelingwh0_.car_id=? 13:18:10.175 [main] TRACE o.h.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - [2000]
Hibernate在select car之后,针对cars的每一条record,又执行了
一次:
select steelingwheel from steeling_wheel where steelingwheel.car_id=?
一共1000次。运行的时间和资源开销大幅上升。从断点处我们也可以看到,car下面的steelingwheel已经从数据库中抓取:
虽然我们在car的定义中明确指定了fetch = FETCHTYPE.LAZY,但在实际的运行过程中它是失效的。如果不注意这个细节,那么在缺乏足够细致的压力测试的情况下,我们就很难在产品发布之前发现这个问题,因为只有在大量数据下这两者才能有明显差别。
那么原因是什么?
失效的原因
1,无法取得关系字段在Car表里由于没有关系字段,因此仅从Car表角度看无法知道拥有该Car的SteelingWheel是哪个(除非在Car表建立SteelingWheel表的外键steelingWHeel_id,但由于冗余了关系字段,这样做也会导致写数据库的效率急剧降低,并且增加了不必要的存储空间),而从SteelingWheel角度不一样,由于含有car_id字段可能清楚知道该SteelingWheel拥有一辆Car。
正是由于上面的原因,因此当从Car获取SteelingWheel时,hibernate为了确定SteelingWheel表中到底有没有该Car,因此发了一条sql:select * from SteelingWheel where card_id = ?,参数既是该Car的id,以此来维护Card与SteelingWheel的关系。
还有一种解释,但需要理解hibernate的延迟加载的机制:代理。
2,延迟加载原理
hibernate使用了代理(Proxy),对实体的调用会被代理接受和处理,hibernate可以设置这个代理被调用到的时候去加载数据,从而实现延迟加载。那么对于一个映射对象,要么它有值,要么它是null,对于null值建立代理是没多大作用的,而且也不能对null建立动态代理。那就是说hibernate在对延迟加载建立代理的时候要考虑这个映射的对象是否是null。如果是null不需要建立代理,直接把映射的值设置成null,如果映射的对象不为null,那么hibernate就建立代理对象。
简而言之,为null就不能延迟加载,为代理对象才能延迟加载。所以必须通过select语句确定是否为null。
相关文章推荐
- Hibernate的bug,one-to-one延迟加载时的问题
- hibernate annotations OneToMany与ManyToOne 分析 以及延迟加载性能分析
- failed to lazily initialize a collection of role: no session or session was closed (Hibernate) 解决方法(延迟加载问题)
- Spring使用OpenSessionInViewFilter解决Hibernate的lazy延时加载问题
- failed to lazily initialize a collection of role(hibernate一对多或者多对多懒加载问题)
- Hibernate One to Many 迟延加载分析
- hibernate annotations OneToMany与ManyToOne 分析 以及延迟加载性能分析
- hibernate的many-to-one的级联删除问题
- Spring使用OpenSessionInViewFilter解决Hibernate的lazy延时加载问题
- hibernate 的many to one 的问题IllegalArgumentException occurred while calling setter of hibernatePOJO
- hibernate注解(1-2-3)+Hibernate @OneToOne懒加载实现解决方案
- Hibernate使用property-ref属性解决遗留数据库One To Many关系的问题。
- hibernate延迟加载会出现的问题(failed to lazily initialize)
- Hibernate映射mysql问题 one-to-one
- hibernate懒加载的问题,failed to lazily initialize a collection of role
- Hibernate延时加载lazy机制问题及其解决方法
- Hibernate One to Many 迟延加载分析
- WebLogic下开发Hibernate问题 java.lang.NoSuchMethodError:javax.persistence.OneToMany.orphanRemoval()Z
- hibernate 双向 OneToOne fetchType lazy 问题
- 关于hibernate中双向外键关联one-to-one的property-ref=的问题(转)