关注公众号: 锅外的大佬java
每日推送国外优秀的技术翻译文章,励志帮助国内的开发者更好地成长!数据库
JPA
和Hibernate
容许你在JPQL
和Criteria
查询中使用DTO
和Entity
做为映射。当我在个人在线培训或研讨会上讨论Hibernate
性能时,我常常被问到,选择使用适当的映射是不是重要的? 答案是:是的!为你的用例选择正确的映射会对性能产生巨大影响。我只选择你须要的数据。很明显,选择没必要要的信息不会为你带来任何性能优点。缓存
Entity
和DTO
之间常被忽略的区别是——Entity
被持久上下文(persistence context)所管理。当你想要更新Entity
时,只须要调用setter
方法设置新值。Hibernate
将处理所需的SQL语句并将更改写入数据库。bash
天下没有免费的午饭。Hibernate
必须对全部托管实体(managed entities)执行脏检查(dirty checks),以肯定是否须要在数据库中保存变动。这很耗时,当你只想向客户端发送少许信息时,这彻底没有必要。app
你还须要记住,Hibernate
和任何其余JPA
实现都将全部托管实体存储在一级缓存中。这彷佛是一件好事。它能够防止执行重复查询,这是Hibernate写入优化所必需的。可是,须要时间来管理一级缓存,若是查询数百或数千个实体,甚至可能发生问题。函数
使用Entity
会产生开销,而你能够在使用DTO
时避免这种开销。但这是否意味着不该该使用Entity
?显然不是。性能
实体投影(Entity Projections)适用于全部写操做。Hibernate
以及其余JPA
实现管理实体的状态,并建立所需的SQL语句以在数据库中保存更改。这使得大多数建立,更新和删除操做的实现变得很是简单和有效。测试
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Author a = em.find(Author.class, 1L);
a.setFirstName("Thorben");
em.getTransaction().commit();
em.close();
复制代码
可是只读(read-only)操做要用不一样方式处理。若是想从数据库中读取数据,那么Hibernate
就不会管理状态或执行脏检查。 所以,从理论上说,对于读取数据,DTO
投影是更好的选择。但真的有什么不一样吗?我作了一个小的性能测试来回答这个问题。fetch
我使用如下领域模型进行测试。它由Author
和Book
实体组成,使用多对一关联(many-to-one)。因此,每本书都是由一位做者撰写。优化
@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", updatable = false, nullable = false)
private Long id;
@Version
private int version;
private String firstName;
private String lastName;
@OneToMany(mappedBy = "author")
private List bookList = new ArrayList();
...
}
复制代码
要确保Hibernate
不获取任何额外的数据,我设置了@ManyToOne
的FetchType
为LAZH
。你能够阅读 Introduction to JPA FetchTypes获取不一样FetchType
及其效果的更多信息。
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", updatable = false, nullable = false)
private Long id;
@Version
private int version;
private String title;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "fk_author")
private Author author;
...
}
复制代码
我用10个做者建立了一个测试数据库,他们每人写了10 本书,因此数据库总共包含100 本书。在每一个测试中,我将使用不一样的投影来查询100 本书并测量执行查询和事务所需的时间。为了减小任何反作用的影响,我这样作1000次并测量平均时间。 OK,让咱们开始吧。
在大多数应用程序中,实体投影(Entity Projection)是最受欢迎的。有了Entity
,JPA
能够很容易地将它们用做投影。 运行这个小测试用例并测量检索100个Book
实体所需的时间。
long timeTx = 0;
long timeQuery = 0;
long iterations = 1000;
// Perform 1000 iterations
for (int i = 0; i < iterations; i++) {
EntityManager em = emf.createEntityManager();
long startTx = System.currentTimeMillis();
em.getTransaction().begin();
// Execute Query
long startQuery = System.currentTimeMillis();
List<Book> books = em.createQuery("SELECT b FROM Book b").getResultList();
long endQuery = System.currentTimeMillis();
timeQuery += endQuery - startQuery;
em.getTransaction().commit();
long endTx = System.currentTimeMillis();
em.close();
timeTx += endTx - startTx;
}
System.out.println("Transaction: total " + timeTx + " per iteration " + timeTx / (double)iterations);
System.out.println("Query: total " + timeQuery + " per iteration " + timeQuery / (double)iterations);
复制代码
平均而言,执行查询、检索结果并将其映射到100个Book
实体须要2ms。若是包含事务处理,则为2.89ms。对于小型且不那么新的笔记本电脑来讲也不错。
Transaction: total 2890 per iteration 2.89
Query: total 2000 per iteration 2.0
复制代码
当我向你展现Book实体时,我指出我将FetchType设置为LAZY
以免其余查询。默认状况下,To-one
关联的FetchtType
是EAGER
,它告诉Hibernate
当即初始化关联。
这须要额外的查询,若是你的查询选择多个实体,则会产生巨大的性能影响。让咱们更改Book
实体以使用默认的FetchType
并执行相同的测试。
@Entity
public class Book {
@ManyToOne
@JoinColumn(name = "fk_author")
private Author author;
...
}
复制代码
这个小小的变化使测试用例的执行时间增长了两倍多。如今花了7.797ms执行查询并映射结果,而不是2毫秒。每笔交易的时间上升到8.681毫秒而不是2.89毫秒。
Transaction: total 8681 per iteration 8.681
Query: total 7797 per iteration 7.797
复制代码
所以,最好确保To-one
关联设置FetchType
为LAZY
。
Joao Charnet在评论中告诉我要在测试中添加一个不可变的实体(Immutable Entity)。有趣的问题是:返回使用@Immutable
注解的实体,查询性能会更好吗?
Hibernate
没必要对这些实体执行任何脏检查,由于它们是不可变的。这可能会带来更好的表现。因此,让咱们试一试。
我在测试中添加了如下ImmutableBook
实体。
@Entity
@Table(name = "book")
@Immutable
public class ImmutableBook {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", updatable = false, nullable = false)
private Long id;
@Version
private int version;
private String title;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "fk_author")
private Author author;
...
}
复制代码
它是Book
实体的副本,带有2个附加注解。@Immutable
注解告诉Hibernate
,这个实体是不可变得。而且@Table(name =“book”)
将实体映射到book
表。所以,咱们可使用与之前相同的数据运行相同的测试。
long timeTx = 0;
long timeQuery = 0;
long iterations = 1000;
// Perform 1000 iterations
for (int i = 0; i < iterations; i++) {
EntityManager em = emf.createEntityManager();
long startTx = System.currentTimeMillis();
em.getTransaction().begin();
// Execute Query
long startQuery = System.currentTimeMillis();
List<Book> books = em.createQuery("SELECT b FROM ImmutableBook b")
.getResultList();
long endQuery = System.currentTimeMillis();
timeQuery += endQuery - startQuery;
em.getTransaction().commit();
long endTx = System.currentTimeMillis();
em.close();
timeTx += endTx - startTx;
}
System.out.println("Transaction: total " + timeTx + " per iteration " + timeTx / (double)iterations);
System.out.println("Query: total " + timeQuery + " per iteration " + timeQuery / (double)iterations);
复制代码
有趣的是,实体是不是不可变的,对查询没有任何区别。测量的事务和查询的平均执行时间几乎与先前的测试相同。
Transaction: total 2879 per iteration 2.879
Query: total 2047 per iteration 2.047
复制代码
Andrew Bourgeois建议在测试中包含只读查询。因此,请看这里。
此测试使用我在文章开头向你展现的Book
实体。但它须要测试用例进行修改。
JPA
和Hibernate
支持一组查询提示(hits),容许你提供有关查询及其执行方式的其余信息。查询提示QueryHints.HINT_READONLY
告诉Hibernate
以只读模式查询实体。所以,Hibernate
不须要对它们执行任何脏检查,也能够应用其余优化。
你能够经过在Query
接口上调用setHint
方法来设置此提示。
long timeTx = 0;
long timeQuery = 0;
long iterations = 1000;
// Perform 1000 iterations
for (int i = 0; i < iterations; i++) {
EntityManager em = emf.createEntityManager();
long startTx = System.currentTimeMillis();
em.getTransaction().begin();
// Execute Query
long startQuery = System.currentTimeMillis();
Query query = em.createQuery("SELECT b FROM Book b");
query.setHint(QueryHints.HINT_READONLY, true);
query.getResultList();
long endQuery = System.currentTimeMillis();
timeQuery += endQuery - startQuery;
em.getTransaction().commit();
long endTx = System.currentTimeMillis();
em.close();
timeTx += endTx - startTx;
}
System.out.println("Transaction: total " + timeTx + " per iteration " + timeTx / (double)iterations);
System.out.println("Query: total " + timeQuery + " per iteration " + timeQuery / (double)iterations);
复制代码
你可能但愿将查询设置为只读来让性能显著的提高——Hibernate
执行了更少的工做,所以应该更快。
但正如你在下面看到的,执行时间几乎与以前的测试相同。至少在此测试场景中,将QueryHints.HINT_READONLY
设置为true
不会提升性能。
Transaction: total 2842 per iteration 2.842
Query: total 2006 per iteration 2.006
复制代码
加载100 本书实体大约须要2ms。让咱们看看在JPQL
查询中使用构造函数表达式获取相同的数据是否表现更好。
固然,你也能够在Criteria
查询中使用构造函数表达式。
long timeTx = 0;
long timeQuery = 0;
long iterations = 1000;
// Perform 1000 iterations
for (int i = 0; i < iterations; i++) {
EntityManager em = emf.createEntityManager();
long startTx = System.currentTimeMillis();
em.getTransaction().begin();
// Execute the query
long startQuery = System.currentTimeMillis();
List<BookValue> books = em.createQuery("SELECT new org.thoughts.on.java.model.BookValue(b.id, b.title) FROM Book b").getResultList();
long endQuery = System.currentTimeMillis();
timeQuery += endQuery - startQuery;
em.getTransaction().commit();
long endTx = System.currentTimeMillis();
em.close();
timeTx += endTx - startTx;
}
System.out.println("Transaction: total " + timeTx + " per iteration " + timeTx / (double)iterations);
System.out.println("Query: total " + timeQuery + " per iteration " + timeQuery / (double)iterations);
复制代码
正如所料,DTO
投影比实体(Entity)
投影表现更好。
Transaction: total 1678 per iteration 1.678
Query: total 1143 per iteration 1.143
复制代码
平均而言,执行查询须要1.143ms,执行事务须要1.678ms。查询的性能提高43%,事务的性能提升约42%。
对于一个花费一分钟实现的小改动而言,这已经很不错了。
在大多数项目中,DTO
投影的性能提高将更高。它容许你选择用例所需的数据,而不只仅是实体映射的全部属性。选择较少的数据几乎总能带来更好的性能。
为你的用例选择正确的投影比你想象的更容易也更重要。
若是要实现写入操做,则应使用实体(Entity)做为投影。Hibernate
将管理其状态,你只需在业务逻辑中更新其属性。而后Hibernate
会处理剩下的事情。
你已经看到了个人小型性能测试的结果。个人笔记本电脑可能不是运行这些测试的最佳环境,它确定比生产环境慢。可是性能的提高是如此之大,很明显你应该使用哪一种投影。
使用DTO
投影的查询比选择实体的查询快约40%。所以,最好花费额外的精力为你的只读操做建立DTO
并将其用做投影。
此外,还应确保对全部关联使用FetchType.LAZY
。正如在测试中看到的那样,即便是一个热切获取to-one
的关联操做,也可能会将查询的执行时间增长两倍。所以,最好使用FetchType.LAZY
并初始化你的用例所需的关系。
做者: Thorben Janssen
译者:Yunooa