在同一实体上 JOIN FETCH 后休眠额外查询
Posted
技术标签:
【中文标题】在同一实体上 JOIN FETCH 后休眠额外查询【英文标题】:Hibernate extra query after JOIN FETCH on the same entity 【发布时间】:2018-01-31 13:47:20 【问题描述】:我正在尝试使用以下查询通过 Hibernate 查询大量实体:
"Select * " +
" From Dossier as dossier" +
" LEFT JOIN FETCH dossier.candidat as candidat " +
" LEFT JOIN FETCH candidat.roles as role " +
" LEFT JOIN FETCH dossier.infoPerso as infoPerso " +
" LEFT JOIN FETCH dossier.etablissementOrigine as etablissementOrigine " +
" LEFT JOIN FETCH etablissementOrigine.filieres as filieres " +
" LEFT OUTER JOIN FETCH etablissementOrigine.ville as villeOrigine " +
" LEFT JOIN FETCH dossier.etatDossier as etatDossier " +
" LEFT OUTER JOIN FETCH infoPerso.fichierCNIRecto as fichierCNIRecto " +
" LEFT OUTER JOIN FETCH fichierCNIRecto.type " +
" LEFT OUTER JOIN FETCH infoPerso.fichierCNIVerso as fichierCNIVerso " +
" LEFT OUTER JOIN FETCH fichierCNIVerso.type " +
" LEFT OUTER JOIN FETCH infoPerso.fichierCV as fichierCV " +
" LEFT OUTER JOIN FETCH fichierCV.type " +
" LEFT OUTER JOIN FETCH infoPerso.fichierJAPD as fichierJAPD " +
" LEFT OUTER JOIN FETCH fichierJAPD.type " +
" LEFT OUTER JOIN FETCH infoPerso.fichierCNIVerso as fichierCNIVerso " +
" LEFT OUTER JOIN FETCH fichierCNIVerso.type " +
" LEFT OUTER JOIN FETCH infoPerso.situationFamilliale as situation "
dossiers = getEntityManager()
.createQuery(sql, Dossier.class)
.getResultList();
我可以看到 hibernate 执行第一个大型原生 SQL 查询。但就在那之后,Hibernate 为每一行生成 1 个查询以加载 DOssier,我不知道为什么,Dossier 已经是 fetchs elements 的一部分...
/* load org.ema.ecandidature.dossier.Dossier */ select
dossier0_.id as id1_61_0_,
dossier0_.version as version2_61_0_,
dossier0_.valid as valid3_61_0_,
dossier0_.validSecretariat as validSec4_61_0_,
dossier0_.candidat_id as candidat7_17_0_,
dossier0_.casParticulier as casParti1_17_0_,
dossier0_.dateInscription as dateInsc2_17_0_,
dossier0_.dateSoumission as dateSoum3_17_0_,
dossier0_.entreprise_id as entrepri8_17_0_,
dossier0_.etablissementOrigine_id as etabliss9_17_0_,
dossier0_.etatDossier_id as etatDos10_17_0_,
dossier0_.infoPaiement_id as infoPai11_17_0_,
dossier0_.infoPerso_id as infoPer12_17_0_,
dossier0_.listCursusAcademique_id as listCur13_17_0_,
dossier0_.listDocumentsSupplementaires_id as listDoc14_17_0_,
dossier0_.listExpEntreprise_id as listExp15_17_0_,
dossier0_.listFormations_id as listFor16_17_0_,
dossier0_.listLangues_id as listLan17_17_0_,
dossier0_.listReferents_id as listRef18_17_0_,
dossier0_.listSejourEtranger_id as listSej19_17_0_,
dossier0_.motivationCentreInteret_id as motivat20_17_0_,
dossier0_.secretariatChangeDate as secretar4_17_0_,
dossier0_.secretariatChangeDateBackup as secretar5_17_0_,
dossier0_.validationCommentaire as validati6_17_0_
from
Dossier dossier0_
where
dossier0_.candidat_id=?
Dossier.class:
@Entity
@BatchSize(size=100)
public class Dossier extends ValidableEntity
/** The Constant serialVersionUID. */
private static final long serialVersionUID = 1L;
/** The etablissement origine. */
@SecretaryExport
@ManyToOne( fetch=FetchType.LAZY)
@JoinColumn()
private Etablissement etablissementOrigine;
/** The date inscription. */
@SecretaryExport
private Date dateInscription;
/** The date soumission. */
@SecretaryExport
private Date dateSoumission;
/** The date modification. */
@SecretaryExport
private Date secretariatChangeDate;
/** The date de modification backup. */
@SecretaryExport
private Date secretariatChangeDateBackup;
/** The cas particulier. */
@SecretaryExport
private Boolean casParticulier;
/** The etat dossier. */
@SecretaryExport
@ManyToOne( fetch=FetchType.LAZY)
@JoinColumn()
private EtatDossier etatDossier;
/** The candidat. */
@SecretaryExport
@OneToOne(fetch=FetchType.LAZY)
private Candidat candidat;
/** The info perso. */
@SecretaryExport
@Obligatoire
@ObligatoireSecretariat
@OneToOne(cascade = CascadeType.ALL ,fetch=FetchType.LAZY,orphanRemoval = true)
private InfoPerso infoPerso;
/** The list formations. */
//@SecretaryExport
@Obligatoire
@ObligatoireSecretariat
@OneToOne(cascade = CascadeType.ALL ,fetch=FetchType.LAZY,orphanRemoval = true)
private ListFormations listFormations;
/** The list cursus academique. */
@SecretaryExport
@Obligatoire
@ObligatoireSecretariat
@OneToOne(cascade = CascadeType.ALL ,fetch=FetchType.LAZY,orphanRemoval = true)
private ListCursusAcademique listCursusAcademique;
/** The motivation centre interet. */
@SecretaryExport
@Obligatoire
@ObligatoireSecretariat
@OneToOne(cascade = CascadeType.ALL ,fetch=FetchType.LAZY,orphanRemoval = true)
private MotivationCentreInteret motivationCentreInteret;
/** The entreprise. */
@SecretaryExport
@Obligatoire
@ObligatoireSecretariat
@OneToOne(cascade = CascadeType.ALL ,fetch=FetchType.LAZY,orphanRemoval = true)
private Entreprise entreprise;
/** The list langues. */
@SecretaryExport
@Obligatoire
@ObligatoireSecretariat
@OneToOne(cascade = CascadeType.ALL ,fetch=FetchType.LAZY,orphanRemoval = true)
private ListLangues listLangues;
/** The list sejour etranger. */
@SecretaryExport
@Obligatoire
@ObligatoireSecretariat
@OneToOne(cascade = CascadeType.ALL ,fetch=FetchType.LAZY,orphanRemoval = true)
private ListSejourEtranger listSejourEtranger;
/** The list exp entreprise. */
@SecretaryExport
@Obligatoire
@ObligatoireSecretariat
@OneToOne(cascade = CascadeType.ALL ,fetch=FetchType.LAZY,orphanRemoval = true)
private ListExpEntreprise listExpEntreprise;
/** The list referents. */
@SecretaryExport
@Obligatoire
@ObligatoireSecretariat
@OneToOne(cascade = CascadeType.ALL,fetch=FetchType.LAZY,orphanRemoval = true)
private ListReferents listReferents;
/** The info paiement. */
@SecretaryExport
@Obligatoire
@ObligatoireSecretariat
@OneToOne(cascade = CascadeType.ALL,fetch=FetchType.LAZY,orphanRemoval = true)
private InfoPaiement infoPaiement;
/** The avis jury. */
@OneToMany(mappedBy= "dossier" , cascade = CascadeType.ALL,fetch=FetchType.LAZY,orphanRemoval = true)
private Set<AvisJury> avisJury = new HashSet<>();
/** The list documents supplementaires. */
@Obligatoire
@ObligatoireSecretariat
@OneToOne(cascade = CascadeType.ALL ,fetch=FetchType.LAZY,orphanRemoval = true)
private ListDocumentsSupplementaires listDocumentsSupplementaires;
/** The list fichier. */
@OneToMany(mappedBy = "dossier", cascade = CascadeType.ALL ,fetch=FetchType.LAZY,orphanRemoval = true)
private Set<Fichier> listFichier;
/** The list avis examinateur. */
@OneToMany(mappedBy= "dossier" , cascade = CascadeType.ALL,fetch=FetchType.LAZY,orphanRemoval = true)
private Set<AvisExaminateur> listAvisExaminateur;
/** The list commentaire. */
@OneToMany(cascade = CascadeType.ALL,fetch=FetchType.LAZY,orphanRemoval = true)
private Set<Commentaire> listCommentaire;
/** The validation commentaire. */
@Column(length = 500)
@Pattern(regexp="^(.|\n|\r|\t)*$")//accepte tous les caractères et les retours lignes
private String validationCommentaire;
这有什么问题?
【问题讨论】:
【参考方案1】:这有什么问题?
除非您打算修改实体,否则不应获取实体。所以,如果你只需要一个只读视图,那么你应该使用DTO projection instead。
假设您确实需要获取整个图形,因为您计划对其进行修改,那么您必须使用以下获取策略:
您可以在第一个查询中获取尽可能多的子 @OneToOne
和 @ManyToOne
实体关联,以及最多一个 @OneToMany
或 @ManyToMany
。
对于剩余的@OneToMany
或@ManyToMany
,您必须使用辅助查询。但是,您不想在 N+1 fashion 中执行此操作,因此您需要在传递您通过第一个查询获取的根实体时为这些查询运行 JPQL 查询。
请记住,如果将辅助集合重新组装到根实体上,则会触发对根实体的一些不必要的修改。因此,如果您想将根实体传递给 Web 层,那么您应该以只读模式获取这些实体。
同样,如果您不需要实体,那么您应该只获取 DTO 投影并使用 ResultTransfomer
将类似表格的投影转换为 DTO 图。
但在那之后,Hibernate 会为每一行再生成 1 个查询以 load DOssier,不知道为什么,Dossier 已经是 fetchs 的一部分了 元素...
从这些映射中,不清楚为什么会执行该查询,但您可以在 Hibernate 中在 datasource-proxy level 轻松调试它,并查看堆栈跟踪以查看触发它的原因。
【讨论】:
我喜欢使用 ResultTransformer 获取 DTO 图的想法,但是如果要求使用分页选项(即偏移量、限制)来加载一组记录,这将不是一个可行的选择来自数据库。 而且,为什么你认为你不能使用带有分页的 ResultTransformer? ResultTransformer 适用于任何查询 ResultSet。 在@OneToMany 示例中,Projection 将加入父项,子表列和父行将根据子行重复,在这种情况下分页将检索 X(例如,10)包括将由 ResultTransformer 和最终 DTO 转换的重复父记录的记录可能仅为 Y(例如,4)。其他选项是检索所有记录并使用 ResultTransformer 处理它们,并在会影响性能的之上应用分页。 您的用例可以很容易地使用窗口函数解决,正如我在this article 中解释的那样。基本上,您需要使用dense_rank
来选择所有父母及其孩子,并按您需要过滤的父母数量进行过滤。【参考方案2】:
我猜想发出额外的查询是因为您在 *ToOne
关联上使用 EAGER
获取,这可能会意外发生,因为它是 *ToOne
关联的默认获取策略。
Vlad 提到的 DTO 方法是可行的方法,但我知道所有的布线都需要大量工作。为了减少容易出错的样板代码,我只能建议您看看 Blaze-Persistence Entity Views 提供的内容。
该库在 JPA 之上运行并为您有效地处理 DTO 映射。您只需要将目标结构定义为接口。
您甚至可以使用不同的获取策略(JOIN、SELECT 和 SUBSELECT),就像 Hibernate 为按 id 加载提供的策略一样,但这适用于任何类型的查询,而不仅仅是按 id。
【讨论】:
【参考方案3】:我遇到了同样的行为。 假设你有大师班
@OneToMany
@Fetch(FetchMode.JOIN)
private List<Detail> details = new ArrayList<>();
如果我提取所有详细信息,则使用此配置休眠首先执行选择加入,然后选择每个详细信息及其 id。 解决方案是在 Master 类中删除 @Fetch(FetchMode.JOIN)。
【讨论】:
以上是关于在同一实体上 JOIN FETCH 后休眠额外查询的主要内容,如果未能解决你的问题,请参考以下文章
Join Fetch for One To Many 关系多次返回同一个实体
是否可以在Doctrine中组合fetch join和COUNT?
休眠 OnetoMany 与 Fetch Lazy 给 LazyInitializationException