点击左上角,关注:“锅外的大佬”
专注分享国外最新技术内容
帮助每位开发者更优秀地成长
JPA和 Hibernate允许你在 JPQL和 Criteria查询中使用 DTO和 Entity作为映射。当我在我的在线培训或研讨会上讨论 Hibernate性能时,我经常被问到,选择使用适当的映射是否是重要的? 答案是:是的!为你的用例选择正确的映射会对性能产生巨大影响。我只选择你需要的数据。很明显,选择不必要的信息不会为你带来任何性能优势。

1.DTO与Entity之间的主要区别

Entity和 DTO之间常被忽略的区别是—— Entity被持久上下文(persistence context)所管理。当你想要更新 Entity时,只需要调用 setter方法设置新值。 Hibernate将处理所需的SQL语句并将更改写入数据库。
天下没有免费的午餐。 Hibernate必须对所有托管实体(managed entities)执行脏检查(dirty checks),以确定是否需要在数据库中保存变更。这很耗时,当你只想向客户端发送少量信息时,这完全没有必要。
你还需要记住, Hibernate和任何其他 JPA实现都将所有托管实体存储在一级缓存中。这似乎是一件好事。它可以防止执行重复查询,这是Hibernate写入优化所必需的。但是,需要时间来管理一级缓存,如果查询数百或数千个实体,甚至可能发生问题。
使用 Entity会产生开销,而你可以在使用 DTO时避免这种开销。但这是否意味着不应该使用 Entity?显然不是。

2.写操作投影

实体投影(Entity Projections)适用于所有写操作。 Hibernate以及其他 JPA实现管理实体的状态,并创建所需的SQL语句以在数据库中保存更改。这使得大多数创建,更新和删除操作的实现变得非常简单和有效。
  1. EntityManager em = emf.createEntityManager();
  2. em.getTransaction().begin();
  3. Author a = em.find(Author.class,1L);
  4. a.setFirstName("Thorben");
  5. em.getTransaction().commit();
  6. em.close();

3.读操作投影

但是只读(read-only)操作要用不同方式处理。如果想从数据库中读取数据,那么 Hibernate就不会管理状态或执行脏检查。 因此,从理论上说,对于读取数据, DTO投影是更好的选择。但真的有什么不同吗?我做了一个小的性能测试来回答这个问题。

3.1.测试设置

我使用以下领域模型进行测试。它由 Author和 Book实体组成,使用多对一关联(many-to-one)。所以,每本书都是由一位作者撰写。
  1. @Entity
  2. publicclassAuthor{
  3. @Id
  4. @GeneratedValue(strategy =GenerationType.AUTO)
  5. @Column(name ="id", updatable =false, nullable =false)
  6. privateLong id;
  7. @Version
  8. privateint version;
  9. privateString firstName;
  10. privateString lastName;
  11. @OneToMany(mappedBy ="author")
  12. privateList bookList =newArrayList();
  13. ...
  14. }
要确保 Hibernate不获取任何额外的数据,我设置了 @ManyToOne的 FetchType为 LAZH。你可以阅读 Introduction to JPA FetchTypes获取不同 FetchType及其效果的更多信息。
  1. @Entity
  2. publicclassBook{
  3. @Id
  4. @GeneratedValue(strategy =GenerationType.AUTO)
  5. @Column(name ="id", updatable =false, nullable =false)
  6. privateLong id;
  7. @Version
  8. privateint version;
  9. privateString title;
  10. @ManyToOne(fetch =FetchType.LAZY)
  11. @JoinColumn(name ="fk_author")
  12. privateAuthor author;
  13. ...
  14. }
我用10个作者创建了一个测试数据库,他们每人写了10 本书,所以数据库总共包含100 本书。在每个测试中,我将使用不同的投影来查询100 本书并测量执行查询和事务所需的时间。为了减少任何副作用的影响,我这样做1000次并测量平均时间。 OK,让我们开始吧。

3.2.查询实体

在大多数应用程序中,实体投影(Entity Projection)是最受欢迎的。有了 Entity, JPA可以很容易地将它们用作投影。 运行这个小测试用例并测量检索100个 Book实体所需的时间。
  1. long timeTx =0;
  2. long timeQuery =0;
  3. long iterations =1000;
  4. // Perform 1000 iterations
  5. for(int i =0; i < iterations; i++){
  6. EntityManager em = emf.createEntityManager();
  7. long startTx =System.currentTimeMillis();
  8. em.getTransaction().begin();
  9. // Execute Query
  10. long startQuery =System.currentTimeMillis();
  11. List<Book> books = em.createQuery("SELECT b FROM Book b").getResultList();
  12. long endQuery =System.currentTimeMillis();
  13. timeQuery += endQuery - startQuery;
  14. em.getTransaction().commit();
  15. long endTx =System.currentTimeMillis();
  16. em.close();
  17. timeTx += endTx - startTx;
  18. }
  19. System.out.println("Transaction: total "+ timeTx +" per iteration "+ timeTx /(double)iterations);
  20. System.out.println("Query: total "+ timeQuery +" per iteration "+ timeQuery /(double)iterations);
平均而言,执行查询、检索结果并将其映射到100个 Book实体需要2ms。如果包含事务处理,则为2.89ms。对于小型且不那么新的笔记本电脑来说也不错。
  1. Transaction: total 2890 per iteration 2.89
  2. Query: total 2000 per iteration 2.0

3.3.默认FetchType对To-One关联的影响

当我向你展示Book实体时,我指出我将FetchType设置为 LAZY以避免其他查询。默认情况下, To-one关联的 FetchtType是 EAGER,它告诉 Hibernate立即初始化关联。
这需要额外的查询,如果你的查询选择多个实体,则会产生巨大的性能影响。让我们更改 Book实体以使用默认的 FetchType并执行相同的测试。
  1. @Entity
  2. publicclassBook{
  3. @ManyToOne
  4. @JoinColumn(name ="fk_author")
  5. privateAuthor author;
  6. ...
  7. }
这个小小的变化使测试用例的执行时间增加了两倍多。现在花了7.797ms执行查询并映射结果,而不是2毫秒。每笔交易的时间上升到8.681毫秒而不是2.89毫秒。
  1. Transaction: total 8681 per iteration 8.681
  2. Query: total 7797 per iteration 7.797
因此,最好确保 To-one关联设置 FetchType为 LAZY

3.4.选择@Immutable实体

Joao Charnet在评论中告诉我要在测试中添加一个不可变的实体(Immutable Entity)。有趣的问题是:返回使用 @Immutable注解的实体,查询性能会更好吗?
Hibernate不必对这些实体执行任何脏检查,因为它们是不可变的。这可能会带来更好的表现。所以,让我们试一试。
我在测试中添加了以下 ImmutableBook实体。
  1. @Entity
  2. @Table(name ="book")
  3. @Immutable
  4. publicclassImmutableBook{
  5. @Id
  6. @GeneratedValue(strategy =GenerationType.AUTO)
  7. @Column(name ="id", updatable =false, nullable =false)
  8. privateLong id;
  9. @Version
  10. privateint version;
  11. privateString title;
  12. @ManyToOne(fetch =FetchType.LAZY)
  13. @JoinColumn(name ="fk_author")
  14. privateAuthor author;
  15. ...
  16. }
它是 Book实体的副本,带有2个附加注解。 @Immutable注解告诉 Hibernate,这个实体是不可变得。并且 @Tablename=“book”)将实体映射到 book表。因此,我们可以使用与以前相同的数据运行相同的测试。
  1. long timeTx =0;
  2. long timeQuery =0;
  3. long iterations =1000;
  4. // Perform 1000 iterations
  5. for(int i =0; i < iterations; i++){
  6. EntityManager em = emf.createEntityManager();
  7. long startTx =System.currentTimeMillis();
  8. em.getTransaction().begin();
  9. // Execute Query
  10. long startQuery =System.currentTimeMillis();
  11. List<Book> books = em.createQuery("SELECT b FROM ImmutableBook b")
  12. .getResultList();
  13. long endQuery =System.currentTimeMillis();
  14. timeQuery += endQuery - startQuery;
  15. em.getTransaction().commit();
  16. long endTx =System.currentTimeMillis();
  17. em.close();
  18. timeTx += endTx - startTx;
  19. }
  20. System.out.println("Transaction: total "+ timeTx +" per iteration "+ timeTx /(double)iterations);
  21. System.out.println("Query: total "+ timeQuery +" per iteration "+ timeQuery /(double)iterations);
有趣的是,实体是否是不可变的,对查询没有任何区别。测量的事务和查询的平均执行时间几乎与先前的测试相同。
  1. Transaction: total 2879 per iteration 2.879
  2. Query: total 2047 per iteration 2.047

3.5.使用QueryHints.HINT_READONLY查询Entity

Andrew Bourgeois建议在测试中包含只读查询。所以,请看这里。
此测试使用我在文章开头向你展示的 Book实体。但它需要测试用例进行修改。
JPA和 Hibernate支持一组查询提示(hits),允许你提供有关查询及其执行方式的其他信息。查询提示 QueryHints.HINT_READONLY告诉 Hibernate以只读模式查询实体。因此, Hibernate不需要对它们执行任何脏检查,也可以应用其他优化。
你可以通过在 Query接口上调用 setHint方法来设置此提示。
  1. long timeTx =0;
  2. long timeQuery =0;
  3. long iterations =1000;
  4. // Perform 1000 iterations
  5. for(int i =0; i < iterations; i++){
  6. EntityManager em = emf.createEntityManager();
  7. long startTx =System.currentTimeMillis();
  8. em.getTransaction().begin();
  9. // Execute Query
  10. long startQuery =System.currentTimeMillis();
  11. Query query = em.createQuery("SELECT b FROM Book b");
  12. query.setHint(QueryHints.HINT_READONLY,true);
  13. query.getResultList();
  14. long endQuery =System.currentTimeMillis();
  15. timeQuery += endQuery - startQuery;
  16. em.getTransaction().commit();
  17. long endTx =System.currentTimeMillis();
  18. em.close();
  19. timeTx += endTx - startTx;
  20. }
  21. System.out.println("Transaction: total "+ timeTx +" per iteration "+ timeTx /(double)iterations);
  22. System.out.println("Query: total "+ timeQuery +" per iteration "+ timeQuery /(double)iterations);
你可能希望将查询设置为只读来让性能显著的提升—— Hibernate执行了更少的工作,因此应该更快。
但正如你在下面看到的,执行时间几乎与之前的测试相同。至少在此测试场景中,将 QueryHints.HINT_READONLY设置为 true不会提高性能。
  1. Transaction: total 2842 per iteration 2.842
  2. Query: total 2006 per iteration 2.006

3.6.查询DTO

加载100 本书实体大约需要2ms。让我们看看在 JPQL查询中使用构造函数表达式获取相同的数据是否表现更好。
当然,你也可以在 Criteria查询中使用构造函数表达式。
  1. long timeTx =0;
  2. long timeQuery =0;
  3. long iterations =1000;
  4. // Perform 1000 iterations
  5. for(int i =0; i < iterations; i++){
  6. EntityManager em = emf.createEntityManager();
  7. long startTx =System.currentTimeMillis();
  8. em.getTransaction().begin();
  9. // Execute the query
  10. long startQuery =System.currentTimeMillis();
  11. List<BookValue> books = em.createQuery("SELECT new org.thoughts.on.java.model.BookValue(b.id, b.title) FROM Book b").getResultList();
  12. long endQuery =System.currentTimeMillis();
  13. timeQuery += endQuery - startQuery;
  14. em.getTransaction().commit();
  15. long endTx =System.currentTimeMillis();
  16. em.close();
  17. timeTx += endTx - startTx;
  18. }
  19. System.out.println("Transaction: total "+ timeTx +" per iteration "+ timeTx /(double)iterations);
  20. System.out.println("Query: total "+ timeQuery +" per iteration "+ timeQuery /(double)iterations);
正如所料, DTO投影比 实体(Entity)投影表现更好。
  1. Transaction: total 1678 per iteration 1.678
  2. Query: total 1143 per iteration 1.143
平均而言,执行查询需要1.143ms,执行事务需要1.678ms。查询的性能提升43%,事务的性能提高约42%。
对于一个花费一分钟实现的小改动而言,这已经很不错了。
在大多数项目中, DTO投影的性能提升将更高。它允许你选择用例所需的数据,而不仅仅是实体映射的所有属性。选择较少的数据几乎总能带来更好的性能。

4.摘要

为你的用例选择正确的投影比你想象的更容易也更重要。
如果要实现写入操作,则应使用实体(Entity)作为投影。 Hibernate将管理其状态,你只需在业务逻辑中更新其属性。然后 Hibernate会处理剩下的事情。
你已经看到了我的小型性能测试的结果。我的笔记本电脑可能不是运行这些测试的最佳环境,它肯定比生产环境慢。但是性能的提升是如此之大,很明显你应该使用哪种投影。 
使用 DTO投影的查询比选择实体的查询快约40%。因此,最好花费额外的精力为你的只读操作创建 DTO并将其用作投影。
此外,还应确保对所有关联使用 FetchType.LAZY。正如在测试中看到的那样,即使是一个热切获取 to-one的关联操作,也可能会将查询的执行时间增加两倍。因此,最好使用 FetchType.LAZY并初始化你的用例所需的关系。
原文链接:https://thoughts-on-java.org/entities-dtos-use-projection/
作者: Thorben Janssen
译者:Yunooa

上篇好文:Spring Data Web 支持



点击在看,和我一起帮助更多开发者!
继续阅读
阅读原文