20

我有两个表 Employee 和 Department 以下是它们的实体类

Department.java
@Entity
@Table(name = "DEPARTMENT")
public class Department {
    @Id
    @Column(name = "DEPARTMENT_ID")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer departmentId;
    @Column(name = "DEPARTMENT_NAME")
    private String departmentName;
    @Column(name = "LOCATION")
    private String location;

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "department", orphanRemoval = true)
    @Fetch(FetchMode.SUBSELECT)
    //@Fetch(FetchMode.JOIN)
    private List<Employee> employees = new ArrayList<>();
}


Employee.java
@Entity
@Table(name = "EMPLOYEE")
public class Employee {
    @Id
    @SequenceGenerator(name = "emp_seq", sequenceName = "seq_employee")
    @GeneratedValue(generator = "emp_seq")
    @Column(name = "EMPLOYEE_ID")
    private Integer employeeId;
    @Column(name = "EMPLOYEE_NAME")
    private String employeeName;

    @ManyToOne
    @JoinColumn(name = "DEPARTMENT_ID")
    private Department department;
}

以下是我执行时触发的查询em.find(Department.class, 1);

-- 获取模式 = fetchmode.join

    SELECT department0_.DEPARTMENT_ID AS DEPARTMENT_ID1_0_0_,
      department0_.DEPARTMENT_NAME    AS DEPARTMENT_NAME2_0_0_,
      department0_.LOCATION           AS LOCATION3_0_0_,
      employees1_.DEPARTMENT_ID       AS DEPARTMENT_ID3_1_1_,
      employees1_.EMPLOYEE_ID         AS EMPLOYEE_ID1_1_1_,
      employees1_.EMPLOYEE_ID         AS EMPLOYEE_ID1_1_2_,
      employees1_.DEPARTMENT_ID       AS DEPARTMENT_ID3_1_2_,
      employees1_.EMPLOYEE_NAME       AS EMPLOYEE_NAME2_1_2_
    FROM DEPARTMENT department0_
    LEFT OUTER JOIN EMPLOYEE employees1_
    ON department0_.DEPARTMENT_ID   =employees1_.DEPARTMENT_ID
    WHERE department0_.DEPARTMENT_ID=?

-- 获取模式 = fetchmode.subselect

    SELECT department0_.DEPARTMENT_ID AS DEPARTMENT_ID1_0_0_,
      department0_.DEPARTMENT_NAME    AS DEPARTMENT_NAME2_0_0_,
      department0_.LOCATION           AS LOCATION3_0_0_
    FROM DEPARTMENT department0_
    WHERE department0_.DEPARTMENT_ID=?

    SELECT employees0_.DEPARTMENT_ID AS DEPARTMENT_ID3_1_0_,
      employees0_.EMPLOYEE_ID        AS EMPLOYEE_ID1_1_0_,
      employees0_.EMPLOYEE_ID        AS EMPLOYEE_ID1_1_1_,
      employees0_.DEPARTMENT_ID      AS DEPARTMENT_ID3_1_1_,
      employees0_.EMPLOYEE_NAME      AS EMPLOYEE_NAME2_1_1_
    FROM EMPLOYEE employees0_
    WHERE employees0_.DEPARTMENT_ID=?

我只是想知道我们应该更喜欢哪一个FetchMode.JOINFetchMode.SUBSELECT在哪种情况下我们应该选择哪一个?

4

4 回答 4

41

Marmite 所指的 SUBQUERY 策略与 FetchMode.SELECT 相关,而不是 SUBSELECT。

您发布的有关fetchmode.subselect的控制台输出很奇怪,因为这不是应该的工作方式。

FetchMode.SUBSELECT _

使用子选择查询加载其他集合

休眠文档

如果必须获取一个惰性集合或单值代理,Hibernate 将加载所有这些,在子选择中重新运行原始查询。这与批量获取的工作方式相同,但没有零碎加载。

FetchMode.SUBSELECT 应该如下所示:

SELECT <employees columns>
FROM EMPLOYEE employees0_
WHERE employees0_.DEPARTMENT_ID IN
(SELECT department0_.DEPARTMENT_ID FROM DEPARTMENT department0_)

您可以看到,第二个查询将把属于某个部门的所有员工都带入内存(即employee.department_id 不为空),如果它不是您在第一个查询中检索到的部门,则无关紧要。因此,如果员工表很大,这可能是一个主要问题,因为它可能会意外地将整个数据库加载到内存中

但是,FetchMode.SUBSELECT 显着减少了查询的数量,因为与 FecthMode.SELECT 的 N+1 个查询相比,它只需要两个查询。

您可能会认为 FetchMode.JOIN 进行的查询更少,只有 1 个,那么为什么要使用 SUBSELECT 呢?嗯,这是真的,但代价是重复数据和更重的响应。

如果必须使用 JOIN 获取单值代理,则查询可能会检索:

+---------------+---------+-----------+
| DEPARTMENT_ID | BOSS_ID | BOSS_NAME |
+---------------+---------+-----------+
|             1 |       1 | GABRIEL   |
|             2 |       1 | GABRIEL   |
|             3 |       2 | ALEJANDRO |
+---------------+---------+-----------+

如果老板领导多个部门,则老板的员工数据是重复的,并且需要带宽成本。

如果必须使用 JOIN 获取惰性集合,则查询可能会检索:

+---------------+---------------+-------------+
| DEPARTMENT_ID | DEPARTMENT_ID | EMPLOYEE_ID |
+---------------+---------------+-------------+
|             1 | Sales         | GABRIEL     |
|             1 | Sales         | ALEJANDRO   |
|             2 | RRHH          | DANILO      |
+---------------+---------------+-------------+

如果部门数据包含超过一名员工(自然情况),则部门数据会重复。我们不仅要承受带宽成本,而且还会得到重复的重复部门对象,我们必须使用 SET 或DISTINCT_ROOT_ENTITY进行重复数据删除。

然而,正如 Markus Winand所说,在许多情况下,延迟较低的 pos 中的重复数据是一个很好的折衷方案。

SQL 连接仍然比嵌套选择方法更有效——即使它执行相同的索引查找——因为它避免了大量的网络通信。如果由于每次销售的员工属性重复,传输的数据总量更大,则速度会更快。那是因为性能的两个维度:响应时间和吞吐量;在计算机网络中,我们称它们为延迟和带宽。带宽对响应时间的影响很小,但延迟影响很大。这意味着数据库往返次数对响应时间的影响比传输的数据量更重要。

因此,使用 SUBSELECT 的主要问题是难以控制,并且可能会将整个实体图加载到内存中。通过批量获取,您可以在单独的查询中获取关联实体作为 SUBSELECT(因此您不会遭受重复),逐渐且最重要的是您仅查询相关实体(因此您不会遭受潜在的加载巨大图表的影响),因为 IN子查询由外部查询检索到的 ID 过滤)。

Hibernate: 
    select ...
    from mkyong.stock stock0_

Hibernate: 
    select ...
    from mkyong.stock_daily_record stockdaily0_ 
    where
        stockdaily0_.STOCK_ID in (
            ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
        )

(如果批量获取非常大的批次大小会像 SUBSELECT 但没有加载整个表的问题,这可能是一个有趣的测试)

几篇文章展示了不同的获取策略和 SQL 日志(非常重要):

概括:

  • JOIN:避免了 N+1 查询的主要问题,但它可能会检索到重复的数据。
  • SUBSELECT:也避免了 N+1 并且不重复数据,但它将关联类型的所有实体加载到内存中。

这些表是使用ascii-tables构建的。

于 2016-05-02T13:08:01.047 回答
9

我会说这取决于...

假设您在一个部门中有 N 名员工,其中包含 D 个字节的信息,而一个普通员工由 E 个字节组成。(字节是带有一些开销的属性长度的总和)。

使用连接策略,您执行 1 次查询并传输 N * (D + E) 数据。

使用子查询策略,您执行 1 + N 个查询,但只传输 D + N*E 数据。

如果 N 很大,通常N+1 查询NO GO,因此首选 JOIN。

但实际上,您必须检查查询次数和数据传输之间的里程。

请注意,我没有将其他方面视为 Hibernate 缓存。

如果员工表很大且已分区,则其他微妙的方面可能是有效的 - 索引访问上的分区修剪也需要考虑。

于 2015-10-07T07:18:26.563 回答
2

普朗基 说

(1) 这是严重误导。(2) 子选择不会将您的整个数据库提取到内存中。链接的文章是关于一个怪癖,其中 subselect (3) 忽略来自父级的分页命令,(4) 但它仍然是一个 subselect。

  1. 在您发表评论后,我再次调查了 FetchMode.SUBSELECT 并发现我的答案并不完全正确。
  2. 这是一种假设情况,其中每个完全加载到内存中的实体(在本例中为员工)的水合将结束对许多其他实体的水合。真正的问题是如果该表包含数千行(即使其中每一行都没有急切地从其他表中获取其他实体),则加载整个被子选择的表。
  3. 我不知道您对来自父级的分页命令是什么意思。
  4. 是的,它仍然是一个子选择,但我不知道你想用这个指出什么。

您发布的有关 fetchmode.subselect 的控制台输出很奇怪,因为这不是应该的工作方式。

这是真的,但只有当隐藏了多个部门实体(这意味着多个员工集合未初始化)时,我已经使用3.6.10.Final4.3.8.Final 在场景2.2 中对其进行了测试(FetchMode.SUBSELECT hidrating 2 3 个部门)和 3.2 (FetchMode.SUBSELECT 隐藏所有部门)SubselectFetch.toSubselectString返回以下内容(Hibernate 类的链接取自 4.3.8.Final 标记):

select this_.DEPARTMENT_ID from SUBSELECT_DEPARTMENT this_

此子查询用于构建由OneToManyJoinWalker.initStatementString结尾的 where 子句

employees0_.DEPARTMENT_ID in (select this_.DEPARTMENT_ID from SUBSELECT_DEPARTMENT this_)

然后在CollectionJoinWalker.whereString中添加 where 子句,以

select employees0_.DEPARTMENT_ID as DEPARTMENT3_2_1_, employees0_.EMPLOYEE_ID as EMPLOYEE1_1_, employees0_.EMPLOYEE_ID as EMPLOYEE1_3_0_, employees0_.DEPARTMENT_ID as DEPARTMENT3_3_0_, employees0_.EMPLOYEE_NAME as EMPLOYEE2_3_0_ from SUBSELECT_EMPLOYEE employees0_ where employees0_.DEPARTMENT_ID in (select this_.DEPARTMENT_ID from SUBSELECT_DEPARTMENT this_)

通过这个查询,在这两种情况下,所有员工都被检索和补充。这显然是场景 2.2 中的一个问题,因为我们仅对部门 1 和 2 进行水合,而且对所有员工进行水合,即使他们不属于这些部门(在本例中为部门 3 的员工)。

如果会话中只有一个 Department 实体处于水合状态,且其员工集合未初始化,则查询就像 eatSleepCode 所写的一样。检查场景 1.2

select subselectd0_.department_id as departme1_2_0_, subselectd0_.department_name as departme2_2_0_, subselectd0_.location as location3_2_0_ from subselect_department subselectd0_ where subselectd0_.department_id=?

来自FetchStyle

    /**
     * Performs a separate SQL select to load the indicated data.  This can either be eager (the second select is
     * issued immediately) or lazy (the second select is delayed until the data is needed).
     */
    SELECT,
    /**
     * Inherently an eager style of fetching.  The data to be fetched is obtained as part of an SQL join.
     */
    JOIN,
    /**
     * Initializes a number of indicated data items (entities or collections) in a series of grouped sql selects
     * using an in-style sql restriction to define the batch size.  Again, can be either eager or lazy.
     */
    BATCH,
    /**
     * Performs fetching of associated data (currently limited to only collections) based on the sql restriction
     * used to load the owner.  Again, can be either eager or lazy.
     */
    SUBSELECT

到目前为止,我无法解决这个 Javadoc 的含义:

基于用于加载所有者的sql限制

更新 Planky 说:

相反,它只会在最坏的情况下加载表,即使那样,只有当您的初始查询没有 where 子句时。所以我想说,如果您限制结果并且您没有任何 WHERE 条件,则使用子选择查询可能会意外加载整个表

这是真的,这是我在新场景 4.2中测试过的一个非常重要的细节

为获取员工而生成的查询是

select employees0_.department_id as departme3_4_1_, employees0_.employee_id as employee1_5_1_, employees0_.employee_id as employee1_5_0_, employees0_.department_id as departme3_5_0_, employees0_.employee_name as employee2_5_0_ from subselect_employee employees0_ where employees0_.department_id in (select this_.department_id from subselect_department this_ where this_.department_name>=?)

where 子句中的子查询包含原始限制this_.department_name>=? ,避免所有员工的负担。这就是 javadoc 的含义

基于用于加载所有者的sql限制

我所说的关于 FetchMode.JOIN 的所有内容以及与 FetchMode.SUBSELECT 的差异都是正确的(并且也适用于 FetchMode.SELECT)。

于 2017-09-03T23:53:02.063 回答
1

我的一个客户(金融服务)也有类似的问题,他想“在单个查询中获取数据”。好吧,我解释说最好有多个查询,原因如下:

对于 FetchMode.JOIN,部门将从数据库转移到每个员工一次的应用程序,因为联接操作会导致每个员工的部门相乘。如果您有 10 个部门,每个部门有 100 名员工,那么这 10 个部门中的每一个部门都将在一个查询中转移 100 次,简单的 SQL。因此,在这种情况下,每个部门的传输频率比必要的高 99 倍,从而导致该部门的数据传输开销。

对于 Fetchmode SUBSELECT 两个查询被触发到数据库。一个用于获取 1000 名员工的数据,一个用于获取 10 个部门的数据。对我来说,这听起来更有效率。当然,您会确保索引到位,以便可以立即检索数据。

我更喜欢 FetchMode.SUBSELECT。

如果每个部门只有一名员工,那将是另一种情况,但是,正如“部门”名称所暗示的那样,这种情况不太可能发生。

我建议测量访问时间来支持这个理论。对于我的客户,我对不同类型的访问进行了测量,我的客户的“部门”表有更多的字段(不过我没有设计它)。所以很快就很明显 FetchMode.SUBSELECT 快得多。

于 2017-08-02T07:03:55.283 回答