您的位置:首页 > 数据库

Hibernate中OneToOne延时加载的问题-现象和原因

2016-08-16 13:57 405 查看
关于OneToOne的延迟加载(fetch = FetchType.LAZY),我觉得是非常需要注意的一个问题。它不只是存在于Hibernate中,其他的ORM persistence framekwork也有同样的问题,至少我用过的eclipseLink也是一样的。这个问题之所以重要,是因为它在背后偷偷的降低了查询数据库的效率,但又极难发现,特别是当你进入一个老项目,发现软件的运行效率极低时,你就应该检查一下已有代码中是否合理定义了OneToOne的关联。

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。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐