简体   繁体   中英

Spring JPA + Hibernate OneToOne relationships making n+1 queries

I am using Spring Boot, Groovy and JPA/Hibernate to migrate an old application. The only restriction is that the database model must not be changed and I found myself with a weird situation in which a OneOnOne relationship:

Please look at the following model setup:

@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
}

My Spock test looks as follows:

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]}"
    }
}

This configuration successfully generates an INNER JOIN between Table1 and Table2, NOTE that even the constant on the "where" clause is correctly interpreted.

However for some strange reason Table2 gets re-queried for every row returned in the first query.

The output that I see is:

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

As we can see the first query successfully returns all rows from both tables and it is actually the exact query I am looking for. However there is all those unnecessary extra queries being performed.

Is there any reason why JPA would do such thing and is there a way to prevent it??

I have the impression I am missing something very obvious here.

Thanks in advance for your help


Update 1

If I replace

cQuery.multiselect(t1, t2)

for

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') ...)

It generates the exact same inner join query and DOES NOT re-queries Table2 again.

In other words, looks like (at least for this case) I need to explicitly list all the fields from both tables. Not a nice workaround as it can get very ugly very quickly for tables with a lot of fields. I wonder if there is a way to retrieve all the @Column annotated fields/getters without resourcing to a bunch of reflection stuff?

I think I've got it!

  1. @JoinFormula:

    Primary Key in Table2 is INT and the field in Table1 that is used as FK is String (I completely missed that! duh!). So, the solution for that was to apply a @JoinFormula instead of a @JoinColumn in the form of:

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

    This strangely returns a List<Object[]> each item of the List contains an array of 2 elements: one instance of Table1 and one instance of Table2.

  2. Join Fetch:

    Following your suggestion I added "join fetch" to the query, so it looks like:

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

    This causes Hibernate to correctly return a List<Table1>

Either with the @JoinFormula alone or the @JoinFormula + "join fetch" hibernate stopped generating the n+1 queries.

Debugging Hibernate code I've found that it correctly retrieves and stores in Session both entities the first time it queries the DB with the join query, however the difference between PK and FK data types causes Hibernate to re-query the DB again, once per each row retrieved in the first query.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM