Hibernate 在获取一对一实体关联的父端时执行附加查询
Hibernate executes additional query when fetching the parent-side of a one-to-one entity association
我有这些实体,其中 FSAK_Nachricht
与 FNACHRICHTAKTSTATUS
有 @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
提供的解决方案
您的查询看起来不错,但您的映射存在一些问题:
- 当您对 bi-directional one-to-one association 建模时,您需要在一个实体(关联的拥有方)上定义与其连接列的关联,并在另一个实体(引用方)上引用该属性).拥有方映射包含外键列的 table。
- 如果要使用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-one
table关系:
@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
。
我有这些实体,其中 FSAK_Nachricht
与 FNACHRICHTAKTSTATUS
有 @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
提供的解决方案您的查询看起来不错,但您的映射存在一些问题:
- 当您对 bi-directional one-to-one association 建模时,您需要在一个实体(关联的拥有方)上定义与其连接列的关联,并在另一个实体(引用方)上引用该属性).拥有方映射包含外键列的 table。
- 如果要使用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-one
table关系:
@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
。