Hibernate 在获取一对一实体关联的父端时执行附加查询

Hibernate executes additional query when fetching the parent-side of a one-to-one entity association

我有这些实体,其中 FSAK_NachrichtFNACHRICHTAKTSTATUS@OneToOne 关系。

@Entity
@Table(name = "FSAK_NACHRICHT")
public class FsakNachricht implements Serializable {
  private static final long serialVersionUID = 1L;

  @Id
  @SequenceGenerator(name = "FSAK_NACHRICHT_FNNR_GENERATOR", sequenceName = "SEQ_FSAK_NACHRICHT", allocationSize = 1)
  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "FSAK_NACHRICHT_FNNR_GENERATOR")
  @Column(name = "FN_NR", unique = true, nullable = false, updatable = false)
  private long fnNr;

  // one-to-one association to FNachrichtAktStatus
  @OneToOne
  @JoinColumn(name = "FN_NR")
  private FNachrichtAktStatus fnachrichtAktStatus;

  // Other attributes, Getter / Setter
}  

@Entity
@Table(name = "FNACHRICHTAKTSTATUS")
public class FNachrichtAktStatus implements Serializable {
  private static final long serialVersionUID = 1L;

  @OneToOne
  @Id
  @JoinColumn(name = "FN_NR", referencedColumnName = "FN_NR")
  private FsakNachricht fsakNachricht;

  @Column(name = "FN_AKTSTATUS", length = 30)
  private String fnAktStatus;

  //  Getter / Setter

}

我想做的是从 FSAK_Nachricht 加载一条记录并通过一个查询获取 FNACHRICHTAKTSTATUS。所以我写了这个标准声明:

CriteriaBuilder builder = this.getCriteriaBuilder();
CriteriaQuery<FsakNachricht> criteriaQuery = builder.createQuery(FsakNachricht.class);
Root<FsakNachricht> rootFsakNachricht = criteriaQuery.from(FsakNachricht.class);

Join<FsakNachricht, FNachrichtAktStatus> joinFNachrichtAktStatus = (Join<FsakNachricht, FNachrichtAktStatus>) rootFsakNachricht.fetch(
    FsakNachricht_.fnachrichtAktStatus);

// I also join some other tables    

Predicate wherePrimaryKey = builder.equal(rootFsakNachricht.get(FsakNachricht_.fnNr), primaryKey);

criteriaQuery.where(wherePrimaryKey);

List<FsakNachricht> result = this.findByCriteriaQuery(criteriaQuery);

这导致以下语句,这对我来说看起来很完美 - 除了作为 @OneToOne table 属性的投影 列出

2020-01-22T15:00:15,358 DEBUG [AThreadPool : 2] o.h.SQL:92 - 
    select
        fsaknachri0_.FN_NR as FN_NR1_11_0_,
        // Other attributes of fsaknachri0_
        // Attributs of nachrichta2_
        // attributes of user tables
        // 
        // !!!
        // But none of fnachricht1_
from
    FSAK_NACHRICHT fsaknachri0_ 
inner join
    FNACHRICHTAKTSTATUS fnachricht1_ 
        on fsaknachri0_.FN_NR=fnachricht1_.FN_NR 
inner join
    NACHRICHTART nachrichta2_ 
        on fsaknachri0_.NRART_NR=nachrichta2_.NRART_NR 
inner join
    USER user3_ 
        on fsaknachri0_.FN_U1=user3_.U_NR
left outer join
    USER user4_ 
        on fsaknachri0_.FN_U2=user4_.U_NR
left outer join
    USER user5_ 
        on fsaknachri0_.FN_U3=user5_.U_NR 
where
    fsaknachri0_.FN_NR=<KEY>

因此再次查询

2020-01-22T15:22:44,244 DEBUG [AThreadPool : 1] o.h.SQL:92 - 
    select
        fnachricht0_.FN_NR as FN_NR2_7_0_,
        fnachricht0_.FN_AKTSTATUS as FN_AKTSTATUS1_7_0_ 
    from
        FNACHRICHTAKTSTATUS fnachricht0_ 
    where
        fnachricht0_.FN_NR=?            

如何避免这个额外的查询并使用所需的单个语句获取数据(fnAktStatus 列)? 进一步的问题:额外的查询是否是我不能使用 this.findByCriteriaQuerySingelResult(criteriaQuery); 的原因(当使用 Hibernate 时找不到任何行,即使生成的查询在 SQLDeveloper 中执行时只生成一行)

Hibernate 5.2.18(最后支持 JPA 2.1)

事实证明,我的映射是错误的。将其更改为以下内容解决了问题:

@Id
private Long id;

@OneToOne(fetch = FetchType.LAZY)
@MapsId
@JoinColumn(name = "FN_NR", referencedColumnName = "FN_NR")
private FsakNachricht fsakNachricht;

Vlad Mihalcea in his blog

提供的解决方案

您的查询看起来不错,但您的映射存在一些问题:

  1. 当您对 bi-directional one-to-one association 建模时,您需要在一个实体(关联的拥有方)上定义与其连接列的关联,并在另一个实体(引用方)上引用该属性).拥有方映射包含外键列的 table。
  2. 如果要使用same primary key value for both entities,要重用主键值的实体需要拥有关联(映射外键列)。您还需要使用@MapsId对其进行注释并对其建模主键属性。

我根据以下假设调整了您的映射:FsakNachricht 定义了主键值,并且 table FNACHRICHTAKTSTATUS 应重用该值作为主键和外键引用. table 中的主键列称为 FN_NR.

@Entity
@Table(name = "FSAK_NACHRICHT")
public class FsakNachricht implements Serializable {
  private static final long serialVersionUID = 1L;

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "FSAK_NACHRICHT_FNNR_GENERATOR")
  @SequenceGenerator(name = "FSAK_NACHRICHT_FNNR_GENERATOR", sequenceName = "SEQ_FSAK_NACHRICHT", allocationSize = 1)
  @Column(name = "FN_NR", unique = true, nullable = false, updatable = false)
  private long fnNr;

  // one-to-one association to FNachrichtAktStatus
  @OneToOne(mappedBy = "fsakNachricht")
  private FNachrichtAktStatus fnachrichtAktStatus;

  // Other attributes, Getter / Setter
}  

@Entity
@Table(name = "FNACHRICHTAKTSTATUS")
public class FNachrichtAktStatus implements Serializable {
  private static final long serialVersionUID = 1L;

  @Id
  @Column(name = "FN_NR")
  private long fnNr;

  @OneToOne(fetch = FetchType.LAZY)
  @MapsId
  @JoinColumn(name = "FN_NR")
  private FsakNachricht fsakNachricht;

  @Column(name = "FN_AKTSTATUS", length = 30)
  private String fnAktStatus;

  //  Getter / Setter

}

在使用此映射之前,请注意您不能使用 Hibernate 为 FsakNachricht 上的 fnachrichtAktStatus 属性使用延迟加载,因为外键是由其他实体映射的。因此,我建议使用 unidirectional mapping with an additional query.

解决这个问题,你需要做两件事。

@MapsId 添加到子端 table

@MapsId is the only way you can map a true one-to-onetable关系:

@Entity
@Table(name = "FNACHRICHTAKTSTATUS")
public class FNachrichtAktStatus implements Serializable {
  private static final long serialVersionUID = 1L;

  @OneToOne
  @MapsId
  @JoinColumn(name = "FN_NR", referencedColumnName = "FN_NR")
  private FsakNachricht fsakNachricht;

  @Column(name = "FN_AKTSTATUS", length = 30)
  private String fnAktStatus;

  //  Getter / Setter

}

如果没有 @MapsId,您将使用一对多 table 关系,其中 FK 列具有唯一约束。这是不可取的,因为您会浪费一列。

避免父端关联

首先父端需要使用mappedBy,像这样:

@Entity
@Table(name = "FSAK_NACHRICHT")
public class FsakNachricht implements Serializable {
  private static final long serialVersionUID = 1L;

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "FSAK_NACHRICHT_FNNR_GENERATOR")
  @SequenceGenerator(name = "FSAK_NACHRICHT_FNNR_GENERATOR", sequenceName = "SEQ_FSAK_NACHRICHT", allocationSize = 1)
  @Column(name = "FN_NR", unique = true, nullable = false, updatable = false)
  private long fnNr;

  // one-to-one association to FNachrichtAktStatus
  @OneToOne(mappedBy = "fsakNachricht")
  private FNachrichtAktStatus fnachrichtAktStatus;

  // Other attributes, Getter / Setter
}  

此父端 @OneToOne 关联生成额外的查询,因为它需要急切地获取关联以了解是将属性分配给 null 还是代理。

因此,最好删除此父端关联:

@Entity
@Table(name = "FSAK_NACHRICHT")
public class FsakNachricht implements Serializable {
  private static final long serialVersionUID = 1L;

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "FSAK_NACHRICHT_FNNR_GENERATOR")
  @SequenceGenerator(name = "FSAK_NACHRICHT_FNNR_GENERATOR", sequenceName = "SEQ_FSAK_NACHRICHT", allocationSize = 1)
  @Column(name = "FN_NR", unique = true, nullable = false, updatable = false)
  private long fnNr;

  // Other attributes, Getter / Setter
}  

您始终可以通过父实体标识符获取子实体:

FNachrichtAktStatus fnachrichtAktStatus = entityManager.find(
    FNachrichtAktStatus.class, 
    fsakNachricht.getFnNr()
);

这样,您将只在需要时获取此关联,从而避免 N+1 query issues