繁体   English   中英

QueryDSL、Hibernate、JPA——使用 .fetchJoin() 并在第一个 SELECT 中获取数据,那么为什么 N+1 查询之后呢?

[英]QueryDSL, Hibernate, JPA — using .fetchJoin() and getting data in first SELECT, so why N+1 queries after?

我正在尝试查询具有到几个简单子实体的映射的实体列表( MyOrder ):每个MyOrder都与一个Store 、零个或多个Transaction以及最多一个Tender 生成的 SELECT 看起来是正确的 - 它从所有四个连接的表中检索所有列 - 但之后,为每个MyOrder执行另外两个 SELECT ,一个用于Transaction ,一个用于Tender

我正在使用 QueryDSL 4.1.3、Spring Data 1.12、JPA 2.1 和 Hibernate 5.2。

在 QueryDSL 中,我的查询是:

... = new JPAQuery<MyOrder>(entityManager)
    .from(qMyOrder)
    .where(predicates)
    .join(qMyOrder.store).fetchJoin()
    .leftJoin(qMyOrder.transactions).fetchJoin()
    .leftJoin(qMyOrder.tender).fetchJoin()
    .orderBy(qMyOrder.orderId.asc())
    .transform(GroupBy
        .groupBy(qMyOrder.orderId)
        .list(qMyOrder));

执行如下:

SELECT myorder0_.ord_id AS col_0_0_,
    myorder0_.ord_id AS col_1_0_,
    store1_.sto_id AS sto_id1_56_1_, -- store's PK
    transactions3_.trn_no AS trn_no1_61_2_, -- transaction's PK
    tender4_.tender_id AS pos_trn_1_48_3_, -- tender's PK
    myorder0_.ord_id AS ord_id1_39_0_,
    myorder0_.app_name AS app_name3_39_0_, -- {app_name, ord_num} is unique
    myorder0_.ord_num AS ord_num8_39_0_,
    myorder0_.sto_id AS sto_id17_39_0_,
    store1_.division_num AS div_nu2_56_1_,
    store1_.store_num AS store_nu29_56_1_,
    transactions3_.trn_cd AS trn_cd18_61_2_,
    tx2myOrder2_.app_name AS app_name3_7_0__, -- join table
    tx2myOrder2_.ord_no AS ord_no6_7_0__,
    tx2myOrder2_.trn_no AS trn_no1_7_0__,
    tender4_.app_name AS app_name2_48_3_,
    tender4_.ord_num AS ord_num5_48_3_,
    tender4_.tender_cd AS tender_cd_7_48_3_,
FROM data.MY_ORDER myorder0_
INNER JOIN data.STORE store1_ ON myorder0_.sto_id=store1_.sto_id
LEFT OUTER JOIN data.TX_to_MY_ORDER tx2myOrder2_
    ON myorder0_.app_name=tx2myOrder2_.app_name
    AND myorder0_.ord_num=tx2myOrder2_.ord_no
LEFT OUTER JOIN data.TRANSACTION transactions3_ ON tx2myOrder2_.trn_no=transactions3_.trn_no
LEFT OUTER JOIN data.TENDER tender4_
    ON myorder0_.app_name=tender4_.app_name
    AND myorder0_.ord_num=tender4_.ord_num
ORDER BY myorder0_.ord_id ASC

这几乎是我所期望的。 (为简洁起见,我删除了大部分数据列,但我需要的所有内容都是 SELECTed。)

查询内存中的 H2 数据库(使用 Spring 的@DataJpaTest注释设置)时,在执行此查询后,会针对Tender表进行第二次查询,但不会对Transaction 查询 MS SQL 数据库时,初始查询是相同的,但会针对TenderTransaction发生额外的查询。 也不额外调用加载Store

我发现的所有来源都表明.fetchJoin()应该足够了(例如Opinionated JPA with Query DSL ;从锚点向上滚动几行)并且实际上如果我删除它们,初始查询只会从 MY_ORDER 中选择列. 所以看起来.fetchJoin()确实会强制生成一个查询,一次获取所有的边表,但由于某种原因,没有使用额外的信息。 真正奇怪的是,我确实看到在我的 H2 准单元测试中附加了Transaction数据,而没有第二个查询(当且仅当我使用 .fetchJoin() ),但在使用 MS SQL 时却没有。

我已经尝试使用@Fetch(FetchMode.JOIN)注释实体映射,但辅助查询仍然会触发。 我怀疑可能有一个涉及扩展CrudRepository<>的解决方案,但我什至没有成功在那里获得正确的初始查询。

我的主要实体映射,使用 Lombok 的@Data注释,为简洁起见修剪了其他字段。 StoreTransactionTender都有一个@Id一些简单的数字和字符串字段-列映射,没有@Formula@OneToOne或其他任何东西。)

@Data
@NoArgsConstructor
@Entity
@Immutable
@Table(name = "MY_ORDER", schema = "Data")
public class MyOrder implements Serializable {

@Id
@Column(name = "ORD_ID")
private Integer orderId;

@NonNull
@Column(name = "APP_NAME")
private String appName;
@NonNull
@Column(name = "ORD_NUM")
private String orderNumber;

@ManyToOne
@JoinColumn(name = "STO_ID")
private Store store;

@OneToOne
@JoinColumns({
        @JoinColumn(name = "APP_NAME", referencedColumnName = "APP_NAME", insertable = false, updatable = false),
        @JoinColumn(name = "ORD_NUM", referencedColumnName = "ORD_NUM", insertable = false, updatable = false)})
@org.hibernate.annotations.ForeignKey(name = "none")
private Tender tender;

@OneToMany
@JoinTable(
        name = "TX_to_MY_ORDER", schema = "Data",
        joinColumns = { // note X_to_MY_ORDER.ORD_NO vs. ORD_NUM
                @JoinColumn(name = "APP_NAM", referencedColumnName = "APP_NAM", insertable = false, updatable = false),
                @JoinColumn(name = "ORD_NO", referencedColumnName = "ORD_NUM", insertable = false, updatable = false)},
        inverseJoinColumns = {@JoinColumn(name = "TRN_NO", insertable = false, updatable = false)})
@org.hibernate.annotations.ForeignKey(name = "none")
private Set<Transaction> transactions;

/**
 * Because APP_NAM and ORD_NUM are not foreign keys to TX_TO_MY_ORDER (and they shouldn't be),
 * Hibernate 5.x saves this toString() as the 'owner' key of the transactions collection such that
 * it then appears in the transactions collection's own .toString(). Lombok's default generated
 * toString() includes this.getTransactions().toString(), which causes an infinite recursive loop.
 * @return a string that is unique per order
 */
@Override
public String toString() {
    // use appName + orderNumber since, as they are the columns used in the join, they must (?) have
    // already been set when attaching the transactions - primary key sometimes isn't set yet.
    return this.appName + "\00" + this.orderNumber;
}
}

我的问题是:为什么我会得到多余的 SELECT,我怎么能不这样做呢?

我的答案有点晚了,但今天同样的问题发生在我身上。 这个回应可能对你没有帮助,但至少它可以让某人免于我们所经历的头痛。

问题在于实体之间的关系,而不是查询。 我尝试过 QueryDSL、JPQL 甚至本机 SQL,但问题总是一样。

解决方案是通过在这些连接字段上使用 @Id 注释子类来欺骗 JPA 相信这些关系存在。

基本上,您需要像这样设置Tender的 id 并从MyOrder使用它,就像它是正常关系一样。

public class Tender {
    @EmbeddedId
    private TenderId id;
}

@Embeddable
public class TenderId {
    @Column(name = "APP_NAME")
    private String appName;

    @Column(name = "ORD_NUM")
    private String orderNumber;
}

Transaction实体也是如此。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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