關注公眾號: 鍋外的大佬
每日推送國外優秀的技術翻譯文章,勵志幫助國內的開發者更好地成長!
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語句以在數據庫中保存更改。這使得大多數創建,更新和刪除操作的實現變得非常簡單和有效。
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)。所以,每本書都是由一位作者撰寫。
@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
