当获取一个一对一实体关联的父级时,Hibernate会执行额外的查询。

3

我有两个实体,其中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表的属性列出。
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也无法找到任何行)。
Hibernate 5.2.18(最后支持JPA 2.1)

我认为OneToOne连接应该默认为FetchType.EAGER,但是值得尝试添加它以查看是否有所不同。 - Andy N
@AndyN EAGER是JPA的默认设置,而LAZY@OneToOne引用上不起作用。但无论使用哪种注解,我都希望通过显式查询来加载该值。 - bish
3个回答

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

在子表上添加@MapsId

@MapsId 是唯一的一种方法,可以映射真正的 一对一 表关系:

@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,你需要使用一对多的表关系,其中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查询问题

1

你的查询看起来没问题,但是你的映射存在一些问题:

  1. 当你建立双向一对一关联时,你需要在一个实体上(拥有关联的那一方)定义关联及其连接列,并在另一个实体(引用方)上引用该属性。拥有方映射包含外键列的表。
  2. 如果你想要使用相同的主键值来表示两个实体, 需要拥有主键值的实体拥有关联(映射外键列)。你还需要使用@MapsId进行注释并建立一个主键属性。

我根据假设调整了你的映射: FsakNachricht 定义了主键值,表 FNACHRICHTAKTSTATUS 将重用该值作为主键和外键引用。两个表中的主键列都被称为 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上对FsakNachrichtfnachrichtAktStatus属性使用延迟加载,因为外键由其他实体映射。因此,我建议使用带有额外查询的单向映射


0
原来我的映射是错误的。将其更改为以下内容解决了问题:
@Id
private Long id;

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

这个解决方案是由Vlad Mihalcea在他的博客中提供的。


你还需要移除mappedBy一侧,因为它会导致N+1查询问题。如果你使用了@MapsId注解,就不需要父级关联了。 - Vlad Mihalcea

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接