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

来源:互联网 发布:linux服务启动关闭管理 编辑:程序博客网 时间:2024/05/13 00:18

关于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:

@SpringBootTestpublic 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 ASTQueryTranslatorFactory13: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。

1 0