每日推送國外優秀的技術翻譯文章,勵志幫助國內的開發者更好地成長!web
JPA
和Hibernate
容許你在JPQL
和Criteria
查詢中使用DTO
和Entity
做爲映射。當我在個人在線培訓或研討會上討論Hibernate
性能時,我常常被問到,選擇使用適當的映射是不是重要的? 答案是:是的!爲你的用例選擇正確的映射會對性能產生巨大影響。我只選擇你須要的數據。很明顯,選擇沒必要要的信息不會爲你帶來任何性能優點。數據庫
1.DTO與Entity之間的主要區別
Entity
和DTO
之間常被忽略的區別是——Entity
被持久上下文(persistence context)所管理。當你想要更新Entity
時,只須要調用setter
方法設置新值。Hibernate
將處理所需的SQL語句並將更改寫入數據庫。緩存
天下沒有免費的午飯。Hibernate
必須對全部託管實體(managed entities)執行髒檢查(dirty checks),以肯定是否須要在數據庫中保存變動。這很耗時,當你只想向客戶端發送少許信息時,這徹底沒有必要。bash
你還須要記住,Hibernate
和任何其餘JPA
實現都將全部託管實體存儲在一級緩存中。這彷佛是一件好事。它能夠防止執行重複查詢,這是Hibernate寫入優化所必需的。可是,須要時間來管理一級緩存,若是查詢數百或數千個實體,甚至可能發生問題。app
使用Entity
會產生開銷,而你能夠在使用DTO
時避免這種開銷。但這是否意味着不該該使用Entity
?顯然不是。函數
2.寫操做投影
實體投影(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();
3.讀操做投影
可是隻讀(read-only)操做要用不一樣方式處理。若是想從數據庫中讀取數據,那麼Hibernate
就不會管理狀態或執行髒檢查。 所以,從理論上說,對於讀取數據,DTO
投影是更好的選擇。但真的有什麼不一樣嗎?我作了一個小的性能測試來回答這個問題。測試
3.1.測試設置
我使用如下領域模型進行測試。它由Author
和Book
實體組成,使用多對一關聯(many-to-one)。因此,每本書都是由一位做者撰寫。fetch
@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,讓咱們開始吧。
3.2.查詢實體
在大多數應用程序中,實體投影(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
3.3.默認FetchType對To-One關聯的影響
當我向你展現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
。
3.4.選擇@Immutable實體
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
3.5.使用QueryHints.HINT_READONLY查詢Entity
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
3.6.查詢DTO
加載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
投影的性能提高將更高。它容許你選擇用例所需的數據,而不只僅是實體映射的全部屬性。選擇較少的數據幾乎總能帶來更好的性能。
4.摘要
爲你的用例選擇正確的投影比你想象的更容易也更重要。
若是要實現寫入操做,則應使用實體(Entity)做爲投影。Hibernate
將管理其狀態,你只需在業務邏輯中更新其屬性。而後Hibernate
會處理剩下的事情。
你已經看到了個人小型性能測試的結果。個人筆記本電腦可能不是運行這些測試的最佳環境,它確定比生產環境慢。可是性能的提高是如此之大,很明顯你應該使用哪一種投影。
使用DTO
投影的查詢比選擇實體的查詢快約40%。所以,最好花費額外的精力爲你的只讀操做建立DTO
並將其用做投影。
此外,還應確保對全部關聯使用FetchType.LAZY
。正如在測試中看到的那樣,即便是一個熱切獲取to-one
的關聯操做,也可能會將查詢的執行時間增長兩倍。所以,最好使用FetchType.LAZY
並初始化你的用例所需的關係。
做者: Thorben Janssen
譯者:Yunooa