何時使用Entity或DTO
關注公眾號: 鍋外的大佬
每日推送國外優秀的技術翻譯文章,勵志幫助國內的開發者更好地成長!
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
並初始化你的用例所需的關係。
原文連結:thoughts-on-java.org/entities-dt…
作者: Thorben Janssen
譯者:Yunooa