为什么使用Spring Data JPA与Hibernate时,接口投影比构造函数投影和实体投影要慢得多?

34

我一直在想应该使用哪种投影方式,所以我进行了一些测试,涵盖了5种不同的投影方式(基于文档:https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#projections):

1. 实体投影

这只是Spring Data存储库提供的标准findAll()。这里没有什么花哨的东西。

服务:

List<SampleEntity> projections = sampleRepository.findAll();

实体:

@Entity
@Table(name = "SAMPLE_ENTITIES")
public class SampleEntity {
    @Id
    private Long id;
    private String name;
    private String city;
    private Integer age;
}

2. 构造函数映射

服务:

List<NameOnlyDTO> projections = sampleRepository.findAllNameOnlyConstructorProjection();

代码库:

@Query("select new path.to.dto.NameOnlyDTO(e.name) from SampleEntity e")
List<NameOnlyDTO> findAllNameOnlyConstructorProjection();

数据传输对象:

@NoArgsConstructor
@AllArgsConstructor
public class NameOnlyDTO {
    private String name;
}

3. 接口投影

服务:

List<NameOnly> projections = sampleRepository.findAllNameOnlyBy();

存储库:

List<NameOnly> findAllNameOnlyBy();

界面:

public interface NameOnly {
    String getName();
}

4. 元组投影

服务:

List<Tuple> projections = sampleRepository.findAllNameOnlyTupleProjection();

代码库:

@Query("select e.name as name from SampleEntity e")
List<Tuple> findAllNameOnlyTupleProjection();

5. 动态投影

服务:

List<DynamicProjectionDTO> projections = sampleRepository.findAllBy(DynamicProjectionDTO.class);

代码库:

<T> List<T> findAllBy(Class<T> type);

数据传输对象:

public class DynamicProjectionDTO {

    private String name;

    public DynamicProjectionDTO(String name) {
        this.name = name;
    }
}


一些额外信息:

该项目使用gradle spring boot插件(版本2.0.4)构建,其中底层使用了Spring 5.0.8。数据库:H2内存数据库。

结果:

Entity projections took 161.61 ms on average out of 100 iterations.
Constructor projections took 24.84 ms on average out of 100 iterations.
Interface projections took 252.26 ms on average out of 100 iterations.
Tuple projections took 21.41 ms on average out of 100 iterations.
Dynamic projections took 23.62 ms on average out of 100 iterations.
-----------------------------------------------------------------------
One iteration retrieved (from DB) and projected 100 000 objects.
-----------------------------------------------------------------------

注:

可以理解检索实体需要一些时间,Hibernate 跟踪这些对象的变化、延迟加载等。

构造函数投影非常快速,在 DTO 方面没有任何限制,但需要在 @Query 注释中进行手动对象创建。

接口投影非常慢。请参见问题。

元组投影是最快的,但不是最方便的投影方式。它们需要在 JPQL 中指定别名,并且必须通过调用 .get("name") 而不是 .getName() 来检索数据。

动态投影看起来很酷也很快,但必须恰好有一个构造函数。没有更多,也没有更少。否则,Spring Data 会抛出异常,因为它不知道使用哪一个(它需要构造函数参数来确定从数据库检索哪些数据)。

问题:

为什么接口投影比检索实体需要更长时间?每个接口投影返回实际上都是代理。创建该代理非常昂贵吗?如果是这样,那么它是否打败了投影的主要目的(因为它们本来就应该比实体更快)?其他投影看起来很棒。我真的很想了解一些见解。谢谢。

编辑:

这是测试存储库:https://github.com/aurora-software-ks/spring-boot-projections-test,以防您想要运行它。它非常容易设置。自述文件包含您需要了解的所有内容。


所以你担心在调用方法时检索100000个对象需要2.5毫秒,而该方法调用可能需要大约300毫秒。同时确保你正在运行适当的测试(包括预热迭代等等)。并且单独运行每个测试(不是一个包含多个测试用例的测试,而是一个包括JVM加载等等的单独测试)。但由于它们是代理,并且被包装在实体周围,我会怀疑它们确实比实体慢。然而总的来说,这感觉像是过早的优化。 - M. Deinum
嘿,感谢您的评论。这个测试是在热身迭代之后完成的。它不是单元测试,而是在完全启动项目并通过多次调用来进行预热后再次调用以测试投影。结果几乎都相同。此外,这不是关于2.5毫秒的问题。一次迭代(投影100,000个对象)平均需要252毫秒,共进行了100次尝试。使用实际的业务逻辑、关系、多次调用和其他内容可能会使其变得更慢。这个测试只是为了找出哪些更好。 - Sikor
有没有可能公开这些测试?甚至可以作为一个 pull request 吗? - Jens Schauder
@JensSchauder 我已经在原始帖子的最底部添加了存储库链接。你所说的“一些时间”是指几个小时还是几天?我问这个问题是因为我非常好奇这里发生了什么,但请慢慢来 :) 谢谢。 - Sikor
@JensSchauder 好的,请您有了信息后告知我们。 - Sikor
显示剩余3条评论
2个回答

19

我曾在旧版Spring Data中遇到类似的问题,这是我的看法:https://arnoldgalovics.com/how-much-projections-can-help/

我与Oliver Gierke(Spring Data领导)交谈过,并进行了一些改进(这就是为什么你会得到如此“好”的结果 :-)),但基本上总会有抽象与手动编码之间的成本。

这是一个权衡,就像其他所有事情一样。一方面,您可以获得灵活性、更容易的开发和更少的维护(希望如此),另一方面,您可以获得完全控制,略显丑陋的查询模型。


1
嘿,感谢您的回答。我已经阅读了您的文章,看起来确实是同样的“问题”。无论如何,我宁愿拥有完全控制权或只使用一个构造函数的动态投影,而不是遇到这种情况。所以这种行为更多或少是有意的,对吗?如果是这样的话,我相信文档中应该提到这些类型的投影是最慢的,甚至比检索实体还要慢,因为人们主要使用它来提高性能。 - Sikor
1
我是说,使用更高级别的抽象肯定会有代价,所以这完全是可以预料的。个人而言,我不会将其放在文档中,因为使用Spring Data的主要目的是快速入门和覆盖一般用例。一旦您想要获得最佳性能,您还必须摆脱抽象化。 - Arnold Galovics

0
每种方法都有其优缺点:
接口投影: 允许嵌套、动态和开放式投影,但Spring会在运行时生成代理。
DTO投影: 更快速,但不允许嵌套、动态和开放式投影。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接