【问题标题】:select new (JPQL Constructor Expression) in jpa/hibernate causes "lazy" loading for each row在 jpa/hibernate 中选择新的(JPQL 构造函数表达式)会导致每一行的“延迟”加载
【发布时间】:2022-06-10 17:45:59
【问题描述】:

最近我发现在 jpa/hibernate 中使用 'select new/constructor expression' 时没有明显的行为。它对结果集中每一行中的每个实体使用一种延迟加载,效率不高。

测试示例

@Value
public class PojoTuple {
    Entity1 e1;
    Entity2 e2;
}

@Entity
@Table(name = "entity1", schema = DEFAULT_DATABASE_SCHEMA)
@NoArgsConstructor(access = PROTECTED)
public class Entity1 {

    @Id
    @Column(name = "id", nullable = false)
    private String id;

    @Column(name = "field1", nullable = false)
    private String field1;
}

@Entity
@Table(name = "entity2", schema = DEFAULT_DATABASE_SCHEMA)
@NoArgsConstructor(access = PROTECTED)
public class Entity2 {

    @Id
    @Column(name = "id", nullable = false)
    private String id;

    @Column(name = "fkentity1", nullable = false)
    private String entity1Id;

    @Column(name = "field2", nullable = false)
    private String field2;
}

create table entity1
(
    id     varchar2(22 char) not null primary key,
    field1 varchar2(50 char) not null
);

create table entity2
(
    id        varchar2(22 char) not null primary key,
    fkentity1 varchar2(22 char) not null,
    field2    varchar2(50 char) not null

);

insert into entity1 (id, field1) values ('10', 'anyvalue1');
insert into entity1 (id, field1) values ('11', 'anyvalue2');

insert into entity2 (id, fkentity1, field2) VALUES ('20', '10', 'anyvalue3');
insert into entity2 (id, fkentity1, field2) VALUES ('21', '11', 'anyvalue4');

第一种情况

我们使用 select new 技术发出查询:

Query query = entityManager.createQuery("select new my.package.PojoTuple(e1, e2) " +
                                        "from Entity1 e1 " +
                                        "join Entity2 e2 on e1.id=e2.entity1Id ");
query.getResultList();

这会发出一个查询以获取 仅 ids 的 e1 和 e2,然后更多查询以按 id 为结果集中的每一行一个接一个地获取 e1、e2:

查询:["选择 entity1x0_.id 作为 col_0_0_,entity2x1_.id 作为 col_1_0_ 从 schema.entity1 entity1x0_ 内部连接 ​​schema.entity2 entity2x1_ on (entity1x0_.id=entity2x1_.fkentity1)"]

查询:[“选择 entity1x0_.id 作为 id1_1_0_, entity1x0_.field1 作为 field2_1_0_ 来自 schema.entity1 entity1x0_ where entity1x0_.id=?"] 参数:[(10)]

查询:[“选择 entity2x0_.id 作为 id1_2_0_, entity2x0_.fkentity1 作为 fkentity2_2_0_,entity2x0_.field2 作为来自 schema.entity2 的 field3_2_0_ entity2x0_ where entity2x0_.id=?"] 参数:[(20)]

查询:[“选择 entity1x0_.id 作为 id1_1_0_, entity1x0_.field1 作为 field2_1_0_ 来自 schema.entity1 entity1x0_ where entity1x0_.id=?"] 参数:[(11)]

查询:[“选择 entity2x0_.id 作为 id1_2_0_, entity2x0_.fkentity1 作为 fkentity2_2_0_,entity2x0_.field2 作为来自 schema.entity2 的 field3_2_0_ entity2x0_ where entity2x0_.id=?"] 参数:[(21)]

第二种情况

而将上面的示例重写为:

Query query = entityManager.createQuery("select e1, e2 " +
                                        "from Entity1 e1 " +
                                        "join Entity2 e2 on e1.id=e2.entity1Id ");

query.getResultList();

仅向数据库发出一个查询,并选择所有必填字段:

查询:["选择 entity1x0_.id 作为 id1_1_0_,entity2x1_.id 作为 id1_2_1_, entity1x0_.field1 作为 field2_1_0_,entity2x1_.fkentity1 作为 fkentity2_2_1_,entity2x1_.field2 作为来自 schema.entity1 的 field3_2_1_ entity1x0_ 内部连接 ​​schema.entity2 entity2x1_ on (entity1x0_.id=entity2x1_.fkentity1)"] 参数:[()]

问题

在我看来,这两个查询的执行方式没有太大区别。第一种情况会发出许多我不认为效率非常低的查询。第二种情况按预期工作,向数据库发出一个查询。这是一个错误、次优解决方案还是一些我看不到的隐藏功能?

环境 休眠核心:5.6.9.Final

【问题讨论】:

    标签: java hibernate jpa orm jpql


    【解决方案1】:

    所以我终于从我所知道的关于hibernate的最权威的知识来源中找到了部分解释——Vlad Mihalcea:Paragraph: Returning an entity in a DTO projection

    但是,当您想要在 DTO 投影中选择一个实体时,可能会有一些用例。 (...)

    当您执行这样的 JPQL 查询时:

    List<PersonAndCountryDTO> personAndAddressDTOs = entityManager.createQuery(
    "select new " +
    "   com.vladmihalcea.book.hpjp.hibernate.query.dto.PersonAndCountryDTO(" +
    "       p, " +
    "       c.name" +
    "   ) " +
    "from Person p " +
    "join Country c on p.locale = c.locale " +
    "order by p.id", PersonAndCountryDTO.class) .getResultList();
    

    Hibernate 生成以下 SQL 查询:

    SELECT p.id AS col_0_0_,
       c.name AS col_1_0_ FROM   Person p INNER JOIN
       Country c ON
       ( p.locale = c.locale ) ORDER BY
       p.id   
    
    SELECT p.id AS id1_1_0_,
       p.locale AS locale2_1_0_,
       p.name AS name3_1_0_ FROM   Person p WHERE  p.id = 3   
    
    SELECT p.id AS id1_1_0_,
       p.locale AS locale2_1_0_,
       p.name AS name3_1_0_ FROM   Person p WHERE  p.id = 4
    

    DTO 投影的 Hibernate 5.2 实现不能 从 ResultSet 实现 DTO 投影而不执行 二次查询。但是,这对性能非常不利,因为它可以 导致 N+1 查询问题。

    已经讨论过这个 HQL 限制,Hibernate 6.0 新的 SQM 解析器可能会解决这个问题,敬请期待!

    总结一下:

    1. 我询问的行为是休眠开发人员已知的,并且有希望得到修复。
    2. 就目前而言,必须知道使用构造函数表达式提取完整的托管实体作为一种设计是完全可以的,但是使用 hibernate 5.x 可能会导致非最佳解决方案,因为 hibernate 发出许多查询

    【讨论】:

      【解决方案2】:

      您不应在使用构造函数表达式创建的对象中返回实体。这不是此功能的目的,这也是它通过许多查询加载数据的原因。

      https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#hql-select-new

      【讨论】:

      • 我认为您的回答毫无意义。 OP 显示了一个示例,其中在查询中返回 PojoTuple,它不是实体,它只是一个 POJO。我认为这是 Hibernate 中的一个错误,因为我希望这两个查询的行为完全相同。您指向的文档指出:“select new 构造将查询结果打包到用户编写的 Java 类而不是数组中。”所以它正是为此目的而设计的,至少根据文档。
      • 重点是 PojoTuple 中的两个 Entities 在查询后都会处于“托管”状态。这意味着根据事务和持久性上下文,实体是可编辑的,并且将在事务提交时对数据库进行更改
      • 不,他们不会被管理。引用表单文档:“此类不需要以任何方式进行映射或注释。即使该类是实体类,生成的实例也不是托管实体并且与会话无关。”
      • Simon,首先非常感谢您对帮助的兴趣。但是仍然不清楚为什么这个特性“不应该以这种方式使用”以及为什么“它不是这个特性的意图”。您提供的文档没有任何地方禁止这种用法,也没有任何地方警告这种用法的后果,只是简单地说 “select new 构造将查询结果打包到用户编写的 Java 类而不是数组中。”所以我会认为两种方法是相等的,它并不能解释这种行为上的差异。
      • Simon,我刚刚阅读了您关于 PojoTuple 中的实体处于可管理状态的评论。但我希望将它们放入数组(第二种情况)也会使它们处于可管理状态。我错了吗?
      猜你喜欢
      • 2012-09-28
      • 2011-06-25
      • 2013-11-16
      • 1970-01-01
      • 2016-12-21
      • 1970-01-01
      • 2019-04-28
      • 2017-08-21
      • 1970-01-01
      相关资源
      最近更新 更多