Spring JPA + Hibernate OneToOne关系进行n + 1次查询

时间:2017-01-18 22:33:01

标签: java hibernate jpa groovy spring-boot

我正在使用Spring Boot,Groovy和JPA / Hibernate来迁移旧的应用程序。唯一的限制是不能更改数据库模型,我发现自己处于OneOnOne关系的奇怪情况:

请查看以下型号设置:

@Entity
@Table(name='table1')
class Table1 {

  @Id
  Table1Id id

  @Column(name='sequence_num')
  Integer seqNum

  @Column(name='item_source')
  String itemSource

  @Column(name='source_type')
  String sourceType

  @OneToOne(fetch=FetchType.EAGER, cascade=CascadeType.ALL)
  @JoinColumn(name='key_field_2', insertable=false, updatable=false)
  @NotFound(action=NotFoundAction.IGNORE)
//  @Fetch(FetchMode.JOIN)
  Table2 table2
}

@Embeddable
class Table1Id implements Serializable {

  @Column(name='key_field_1')
  String key1

  @Column(name='key_field_2')
  String key2
}

@Entity
@Table(name='table2')
class Table2 {

  @Id
  @Column(name='key_id')
  String keyId

  @Column(name='field1')
  String field1

  @Column(name='field2')
  String field2

  @Column(name='field3')
  String field3
}

我的Spock测试如下:

def "Try out the JOIN select with Criteria API"() {
  given:

    CriteriaBuilder cb = entityManager.getCriteriaBuilder()

    CriteriaQuery<Object[]> cQuery = cb.createQuery(Object[].class)
    Root<Table1> t1 = cQuery.from(Table1.class)
    Path<Table2> t2 = t1.get('table2')
    Join<Table1, Table2> lanyonLeftJoin = t1.join('table2', JoinType.INNER)

    Predicate where = cb.equal(t1.get('itemSource'), 'ABC')

    cQuery.multiselect(t1, t2)
    cQuery.where(where)

  when:
    List<Object[]> result = entityManager.createQuery(cQuery).getResultList()

  then:
    result.each{ aRow -> 
      println "${aRow[0]}, ${aRow[1]}"
    }
}

此配置在Table1和Table2之间成功生成INNER JOIN,注意即使是&#34;中的常量,其中&#34;子句被正确解释。

然而,由于某些奇怪的原因,在第一个查询中返回的每一行都会重新查询Table2。

我看到的输出是:

Hibernate: 
    select
        table10_.key_field_1 as key_field_11_3_0_,
        table10_.key_field_2 as key_field_22_3_0_,
        table21_.key_id as key_id1_5_1_,
        table10_.item_source as item_source3_3_0_,
        table10_.sequence_num as sequence_num4_3_0_,
        table10_.source_type as source_type5_3_0_,
        table21_.field2 as field23_5_1_,
        table21_.field3 as field34_5_1_,
        table21_.field1 as field15_5_1_ 
    from
        table1 table10_ 
    inner join
        table2 table21_ 
            on table10_.key_field_2=table21_.key_id 
    where
        table10_.item_source=?
Hibernate: 
    select
        table20_.key_id as key_id1_5_0_,
        table20_.field2 as field23_5_0_,
        table20_.field3 as field34_5_0_,
        table20_.field1 as field15_5_0_ 
    from
        table2 table20_ 
    where
        table20_.key_id=?
Hibernate: 
    select
        table20_.key_id as key_id1_5_0_,
        table20_.field2 as field23_5_0_,
        table20_.field3 as field34_5_0_,
        table20_.field1 as field15_5_0_ 
    from
        table2 table20_ 
    where
        table20_.key_id=?

// 500+ more of these

正如我们所看到的,第一个查询成功返回两个表中的所有行,它实际上是我正在寻找的确切查询。但是,正在执行所有不必要的额外查询。

有没有理由为什么JPA会做这样的事情并且有办法防止它?

我的印象是我遗漏了一些非常明显的东西。

提前感谢您的帮助


更新1

如果我更换

cQuery.multiselect(t1, t2)

cQuery.multiselect(t1.get('id').get('key1'), t1.get('id').get('key2'), 
  t1.get('fieldX'), t1.get('fieldY'), t1.get('fieldZ'), 
  t2.get('fieldA'), t2.get('fieldB'), t2.get('fieldC') ...)

它生成完全相同的内连接查询,并且不再重新查询Table2。

换句话说,看起来(至少在这种情况下)我需要明确列出两个表中的所有字段。不是一个很好的解决方法,因为对于有很多字段的表来说,它会很快变得非常难看。我想知道是否有办法检索所有@Column注释字段/ getter而无需资源到一堆反射内容?

1 个答案:

答案 0 :(得分:0)

我想我已经拥有它了!

  1. @JoinFormula:

    Table2中的主键是INT,Table1中用作FK的字段是String(我完全错过了!duh!)。因此,解决方案是以以下形式应用@JoinFormula而不是@JoinColumn:

    @OneToOne(fetch=FetchType.EAGER, cascade=CascadeType.ALL)
    @JoinColumnsOrFormulas([
       @JoinColumnOrFormula(formula=@JoinFormula(value='CAST(key_field_2 AS INT)'))
    ])
    @NotFound(action=NotFoundAction.IGNORE)
    Table2 table2
    

    这奇怪地返回List&lt; Object []&gt; List的每个项目都包含一个包含2个元素的数组:Table1的一个实例和Table2的一个实例。

  2. 加入抓取:

    根据您的建议,我在查询中添加了“join fetch”,如下所示:

    select t1, t2 from Table1 t1 **join fetch** t1.table2 t2 where t1.itemSource = 'ABC'
    

    这会导致Hibernate正确返回List&lt; Table1&gt;

  3. 单独使用@JoinFormula或@JoinFormula +“join fetch”hibernate停止生成n + 1个查询。

    调试Hibernate代码我发现它在第一次使用连接查询查询数据库时正确地在Session中检索和存储两个实体,但是PK和FK数据类型之间的区别导致Hibernate再次重新查询数据库,在第一个查询中检索到的每一行一次。