Hibernate Formula 动态查询的原理和缺点

功能需求

配送单管理页面中,“派工需求时间”和“分批时间”是多对一的关系,即多个“派工需求时间”会对应一个“分批时间”,项目需要展示一个“分批时间”对应的多个“派工需求时间”中最小的那个。
image.png

功能实现

由于页面查询使用的产品标准查询,即实体查询的模式,实体上增加对应的字段即可,而改字段的值通过一个子查询实现即可。这与 Hibernate 的虚拟列 Formula 查询场景是一致的。代码实现如下:

@Formula("(select MIN(req.TRACK_SCHEDULE_TIME) " +
            " from UMM_TRANSFER_PROREQUIREMENTS req " +
            " where req.IS_ACTIVE = 0 " +
            " and req.IS_DELETE = 0 " +
            " and req.DIS_BILL_GID = DIS_BILL_GID)")
    public String getTransferTime2()
    {
        return transferTime2;
    }

实体增加字段后,jsp 页面添加对应的字段即可,其它的内容不需要修改。

遇到的问题

该查询在测试库查询没有问题,但是部署到正式服务后却发现页面查询很慢。数据量在 10W 左右,查询时间在 3 分钟,增加对应字段的索引后,查询仍在 10 秒左右。

进一步分析

通过日志录制,抓取到该虚拟列的查询经过 Hibernate 的解析,最终转化为投影子查询

select *  
  from (select this_.gid as y0_,  
               this_.work_center_code as y1_,  
               this_.work_center_name as y2_,  
               disbill1_.bill_id as y3_,  
               disbill1_.state as y4_,  
               this_.p_req_code as y5_,  
               this_.op_code as y6_,  
               this_.op_name as y7_,  
               this_.mrl_code as y8_,  
               this_.mrl_name as y9_,  
               this_.work_cell_code as y10_,  
               this_.work_cell_name as y11_,  
               this_.create_req_time as y12_,  
               this_.schedule_time as y13_,  
               workorder2_.gid as y14_,  
               workorder2_.code as y15_,  
               order3_.sales_of_bound_code as y16_,  
               order3_.code as y17_,  
               this_.work_order_qty as y18_,  
               this_.mrl_qty as y19_,  
               this_.mrl_single_qty as y20_,  
               this_.mrl_complete_qty as y21_,  
               this_.s_node_type as y22_,  
               this_.s_node_code as y23_,  
               this_.s_node_name as y24_,  
               this_.location_code as y25_,  
               this_.location_name as y26_,  
               this_.e_node_type as y27_,  
               this_.e_node_code as y28_,  
               this_.e_node_name as y29_,  
               this_.last_e_node_code as y30_,  
               this_.supp_code as y31_,  
               this_.supp_name as y32_,  
               this_.tran_init_time as y33_,  
               this_.tran_time as y34_,  
               this_.tran_warning_time as y35_,  
               this_.track_schedule_time as y36_,  
               this_.track_code as y37_,  
               this_.state as y38_,  
               this_.num_batch as y39_,  
               this_.num_type as y40_,  
               this_.transfer_time as y41_,  
               (select MIN(req.TRACK_SCHEDULE_TIME)  
                  from UMM_TRANSFER_PROREQUIREMENTS req  
                 where req.IS_ACTIVE = 0  
                   and req.IS_DELETE = 0  
                   and req.DIS_BILL_GID = this_.DIS_BILL_GID) as y42_,
               this_.warp_code as y43_,  
               this_.create_date as y44_,  
               this_.going_to as y45_  
          from umm_transfer_prorequirements this_  
          left outer join umm_dis_bill disbill1_  
            on this_.dis_bill_gid = disbill1_.gid  
          left outer join umpp_work_order workorder2_  
            on this_.work_order_gid = workorder2_.gid  
          left outer join umpp_plan_order order3_  
            on workorder2_.order_gid = order3_.gid  
         where (this_.p_req_type = 2 and this_.is_delete <> 1)  
         order by y44_ desc)  
 where rownum <= 20

经测试,去掉其中的 select MIN()的字段后,查询可以在 1 秒内完成,由此知道查询的慢是由于子查询效率低导致的 (oracle 和 mysql 都有这个问题,原因是需要建临时表,开销比较大)。
解决办法就是使用关联查询,效率会远高于子查询,对应 SQL 如下:

select THIS_.GID as Y0_,  
       THIS_.WORK_CENTER_CODE as Y1_,  
       THIS_.WORK_CENTER_NAME as Y2_,  
       DISBILL1_.BILL_ID as Y3_,  
       DISBILL1_.STATE as Y4_,  
       THIS_.P_REQ_CODE as Y5_,  
       THIS_.OP_CODE as Y6_,  
       THIS_.OP_NAME as Y7_,  
       THIS_.MRL_CODE as Y8_,  
       THIS_.MRL_NAME as Y9_,  
       THIS_.WORK_CELL_CODE as Y10_,  
       THIS_.WORK_CELL_NAME as Y11_,  
       THIS_.CREATE_REQ_TIME as Y12_,  
       THIS_.SCHEDULE_TIME as Y13_,  
       WORKORDER2_.GID as Y14_,  
       WORKORDER2_.CODE as Y15_,  
       ORDER3_.SALES_OF_BOUND_CODE as Y16_,  
       ORDER3_.CODE as Y17_,  
       THIS_.WORK_ORDER_QTY as Y18_,  
       THIS_.MRL_QTY as Y19_,  
       THIS_.MRL_SINGLE_QTY as Y20_,  
       THIS_.MRL_COMPLETE_QTY as Y21_,  
       THIS_.S_NODE_TYPE as Y22_,  
       THIS_.S_NODE_CODE as Y23_,  
       THIS_.S_NODE_NAME as Y24_,  
       THIS_.LOCATION_CODE as Y25_,  
       THIS_.LOCATION_NAME as Y26_,  
       THIS_.E_NODE_TYPE as Y27_,  
       THIS_.E_NODE_CODE as Y28_,  
       THIS_.E_NODE_NAME as Y29_,  
       THIS_.LAST_E_NODE_CODE as Y30_,  
       THIS_.SUPP_CODE as Y31_,  
       THIS_.SUPP_NAME as Y32_,  
       THIS_.TRAN_INIT_TIME as Y33_,  
       THIS_.TRAN_TIME as Y34_,  
       THIS_.TRAN_WARNING_TIME as Y35_,  
       THIS_.TRACK_SCHEDULE_TIME as Y36_,  
       THIS_.TRACK_CODE as Y37_,  
       THIS_.STATE as Y38_,  
       THIS_.NUM_BATCH as Y39_,  
       THIS_.NUM_TYPE as Y40_,  
       THIS_.TRANSFER_TIME as Y41_,  
       t.min_TRACK_SCHEDULE_TIME as Y42_,  
       THIS_.WARP_CODE as Y43_,  
       THIS_.CREATE_DATE as Y44_,  
       THIS_.GOING_TO as Y45_  
  from UMM_TRANSFER_PROREQUIREMENTS THIS_  
  left outer join UMM_DIS_BILL DISBILL1_  
    on THIS_.DIS_BILL_GID = DISBILL1_.GID  
  left outer join UMPP_WORK_ORDER WORKORDER2_  
    on THIS_.WORK_ORDER_GID = WORKORDER2_.GID  
  left outer join UMPP_PLAN_ORDER ORDER3_  
    on WORKORDER2_.ORDER_GID = ORDER3_.GID  
LEFT JOIN (  
select min(REQ.TRACK_SCHEDULE_TIME) as min_TRACK_SCHEDULE_TIME,DIS_BILL_GID  
          from UMM_TRANSFER_PROREQUIREMENTS REQ  
         where REQ.IS_ACTIVE = 0  
           and REQ.IS_DELETE = 0 group by DIS_BILL_GID  
) t on t.DIS_BILL_GID=THIS_.DIS_BILL_GID  
 where (THIS_.P_REQ_TYPE = 2 and THIS_.IS_DELETE <> 1)  
 order by Y44_ desc

问题解决

从 SQL 上解决了查询的效率问题,怎么转化代码实现呢?关联查询再 Hibernate 中体现为实体关联,则需要增加一个实体,然后关联到原实体中。新增的实体,可以先在数据库建视图,然后实体直接关联视图,也可以直接使用 Hibernate 的 subselect 通过 sql 实现。现采用第一种方式。
数据库视图:

create or replace view UMM_TRANSFER_REQ_VIEW as
SELECT DIS_BILL_GID AS GID, MIN(REQ.TRACK_SCHEDULE_TIME) as TRACK_SCHEDULE_TIME
from UMM_TRANSFER_PROREQUIREMENTS REQ
where REQ.IS_ACTIVE = 0 and REQ.IS_DELETE = 0 
group by DIS_BILL_GID

新增实体:

@Entity
@Table(name = "UMM_TRANSFER_REQ_VIEW")
@Immutable
public class UmmTransferReqView

实体关联

    @ManyToOne(fetch=FetchType.LAZY,targetEntity=UmmTransferReqView.class)
    @JoinColumn(name="DIS_BILL_GID", insertable=false, updatable=false)
    public UmmTransferReqView getReqView()
    {
        return reqView;
    }