何時使用Entity或DTO

鍋外的大佬發表於2019-06-03

關注公眾號: 鍋外的大佬

每日推送國外優秀的技術翻譯文章,勵志幫助國內的開發者更好地成長!

JPAHibernate允許你在JPQLCriteria查詢中使用DTOEntity作為對映。當我在我的線上培訓或研討會上討論Hibernate效能時,我經常被問到,選擇使用適當的對映是否是重要的? 答案是:是的!為你的用例選擇正確的對映會對效能產生巨大影響。我只選擇你需要的資料。很明顯,選擇不必要的資訊不會為你帶來任何效能優勢。

1.DTO與Entity之間的主要區別

EntityDTO之間常被忽略的區別是——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.測試設定

我使用以下領域模型進行測試。它由AuthorBook實體組成,使用多對一關聯(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不獲取任何額外的資料,我設定了@ManyToOneFetchTypeLAZH。你可以閱讀 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)是最受歡迎的。有了EntityJPA可以很容易地將它們用作投影。 執行這個小測試用例並測量檢索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關聯的FetchtTypeEAGER,它告訴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關聯設定FetchTypeLAZY

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實體。但它需要測試用例進行修改。

JPAHibernate支援一組查詢提示(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會處理剩下的事情。

你已經看到了我的小型效能測試的結果。我的膝上型電腦可能不是執行這些測試的最佳環境,它肯定比生產環境慢。但是效能的提升是如此之大,很明顯你應該使用哪種投影。

file

使用DTO投影的查詢比選擇實體的查詢快約40%。因此,最好花費額外的精力為你的只讀操作建立DTO並將其用作投影。

此外,還應確保對所有關聯使用FetchType.LAZY。正如在測試中看到的那樣,即使是一個熱切獲取to-one的關聯操作,也可能會將查詢的執行時間增加兩倍。因此,最好使用FetchType.LAZY並初始化你的用例所需的關係

原文連結:thoughts-on-java.org/entities-dt…

作者: Thorben Janssen

譯者:Yunooa

何時使用Entity或DTO

相關文章