Jimmer ORM簡介
1. 簡介
在本教程中,我們將回顧Jimmer ORM 框架。在撰寫本文時,這個 ORM 框架相對較新,但它有一些值得期待的功能。我們將回顧 Jimmer 的理念,然後用它來寫一些範例。
2. 總體架構
首先,Jimmer 並非 JPA 實作。這意味著 Jimmer 並未實現所有 JPA 特性。例如,Jimmer 本身就沒有髒值檢查機制。不過,值得一提的是,Jimmer 與 Hibernate 有許多類似的概念。這樣做的目的是為了讓從 Hibernate 的過渡更加順暢。所以,總的來說,了解 JPA 知識有助於理解 Jimmer。
舉個例子,Jimmer 有一個實體的概念,儘管它的形式和設計與 Hibernate 有很大不同。然而,像延遲載入或級聯這樣的概念在 Jimmer 中並不存在。原因是由於 Jimmer 的設計方式,這些概念在 Jimmer 中沒有太大意義。我們稍後會講到這一點。
本節的最後說明:Jimmer 支援多種資料庫,包括 MySQL、Oracle、PostgreSQL、SQL Server、SQLite 和 H2。
3. 實體樣本
如上所述,Jimmer 與 Hibernate 和許多其他 ORM 框架有許多不同之處;它有幾個關鍵的設計原則。首先,我們的實體只有一個用途──表示底層資料庫的模式。但是,這裡重要的是,我們沒有透過註解指定我們打算與之互動的方式。相反,Jimmer 要求開發人員提供派生出要在呼叫點執行的查詢所需的所有資訊。
那麼,這意味著什麼呢?為了理解,讓我們回顧以下 Jimmer 實體:
import org.babyfish.jimmer.client.TNullable;
import org.babyfish.jimmer.sql.Column;
import org.babyfish.jimmer.sql.Entity;
import org.babyfish.jimmer.sql.GeneratedValue;
import org.babyfish.jimmer.sql.GenerationType;
import org.babyfish.jimmer.sql.Id;
import org.babyfish.jimmer.sql.JoinColumn;
import org.babyfish.jimmer.sql.ManyToOne;
import org.babyfish.jimmer.sql.OneToMany;
@Entity
public interface Book {
@Id
@GeneratedValue(strategy = GenerationType.USER)
long id();
@Column(name = "title")
String title();
@Column(name = "created_at")
Instant createdAt();
@ManyToOne
@JoinColumn(name = "author_id")
Author author();
@TNullable
@Column(name = "rating")
Long rating();
@OneToMany(mappedBy = "book")
List<Page> pages();
// equals and hashcode implementation
}
正如您所注意到的,它具有與 JPA 類似的註解。但有一點缺失-我們沒有為關係(例如本例中的pages )指定任何級聯。對於獲取類型(延遲或立即)也類似——在聲明端,它沒有指定。我們也無法像在 JPA 等中那樣指定@Column註解的insertable或updatable屬性。
我們沒有這樣做,因為 Jimmer 希望我們在嘗試執行相應操作時明確提供它。我們將在下面的部分中詳細介紹這一點。
4. DTO語言
另一個讓我們印象深刻的是, Book是一個interface ,而不是一個class 。這是有意為之的,因為在 Jimmer 中,我們不應該直接操作實體,也就是說,我們不應該實例化它們。相反,我們假設我們將透過 DTO 讀寫資料。這些 DTO 應該具有我們想要寫入或讀取資料庫的確切結構。讓我們來看一個例子(不要關注我們現在進行的具體 API 呼叫):
public void saveAdHocBookDraft(String title) {
Book book = BookDraft.$.produce(bookDraft -> {
bookDraft.setCreatedAt(Instant.now());
bookDraft.setTitle(title);
bookDraft.setAuthor(AuthorDraft.$.produce(authorDraft -> {
authorDraft.setId(1L);
}));
bookDraft.setId(1L);
});
sqlClient.save(book);
}
一般來說,在大多數互動中,我們需要使用SqlClient來與資料庫互動。
在上面的範例中,我們透過BookDraft介面建立了一個臨時 DTO。 Jimmer 為我們產生了BookDraft介面以及AuthorDraft接口,它並非手寫程式碼。生成過程本身在編譯時透過 Java 註解處理工具(如果我們使用 Java)進行,或透過 Kotlin 符號處理(如果我們使用 Kotlin)進行。
這兩個產生的介面允許建構任意形狀的 DTO 對象,Jimmer 稍後會在內部將其轉換為Book實體。所以,我們確實保存了一個實體,只是我們自己沒有實例化它,而是 Jimmer 替我們完成了。
5. 空值處理
此外,Jimmer 只會保存 DTO 中存在的組件。這是因為 Jimmer 對未設定的屬性和明確設定為null屬性進行了嚴格區分。換句話說,如果我們不想在生成的 SQL 中包含給定的scalar屬性,只需建立一個 DTO 而不明確設定它即可。 scalar,是指不代表關係屬性的欄位:
public void insertOnlyIdAndAuthorId() {
Book book = BookDraft.$.produce(
bookDraft -> {
bookDraft.setAuthor(AuthorDraft.$.produce(authorDraft -> {
authorDraft.setId(1L);
}));
bookDraft.setId(1L);
});
sqlClient.insert(book);
}
上述案例中為Book產生的INSERT如下圖所示:
INSERT INTO BOOK(ID, author_id) VALUES(?, ?)
如果我們明確地將標量屬性設為null ,那麼 Jimmer 會將此屬性包含在底層INSERT / UPDATE語句中並為其指派一個null值:
public void insertExplicitlySetRatingToNull() {
Book book = BookDraft.$.produce(bookDraft -> {
bookDraft.setAuthor(AuthorDraft.$.produce(authorDraft -> {
authorDraft.setId(1L);
}));
bookDraft.setRating(null);
bookDraft.setId(1L);
});
sqlClient.insert(book);
}
產生的INSERT語句如下所示:
INSERT INTO BOOK(ID, author_id, rating) VALUES(?, ?, ?)
請注意, INSERT包含rating屬性。此rating屬性的綁定值將在底層 JDBC Statement中設定為null 。
最後,對於表示關係的屬性(非標量屬性),其行為更加複雜,值得單獨寫一篇文章。
6. DTO爆炸問題
現在,經驗豐富的開發人員可能會注意到一個問題。 Jimmer 處理資料庫的方法意味著需要建立數十個 DTO,每個 DTO 都用於特定的操作。答案是──並非如此。雖然我們確實需要大量的 DTO,但我們可以大幅減少手動編寫它們的開銷。原因在於 Jimmer 擁有一種專用的 DTO 語言。以下是一個例子:
export com.baeldung.jimmer.models.Book
-> package com.baeldung.jimmer.dto
BookView {
#allScalars(Book)
author {
id
}
pages {
#allScalars(Page)
}
}
上面的例子是一個用 Jimmer DTO 語言寫的標記。與上一節中的範例一樣,使用該標記語言產生 POJO 是在編譯時進行的。
例如,在上面的標記中,我們要求 Jimmer 使用#allScalars指令將所有標量欄位包含在產生的 DTO 中。除此之外,我們也提到 DTO 只包含Author的 ID,而不包含Author本身。頁面集合將完整地存在於 DTO 中(僅包含標量欄位)。
所以,總的來說,使用 Jimmer 時,我們確實需要大量的 DTO 來描述每種情況下所需的行為。但我們可以創建臨時版本,或依賴編譯器插件在建置過程中為我們產生的 POJO。
7.閱讀路徑
到目前為止,我們只討論了將資料保存到資料庫的方法。讓我們回顧一下讀取路徑。為了讀取數據,我們還需要透過 DTO 明確指定需要取得哪些數據。 DTO 的結構本身就指示了 Jimmer 需要取得哪些欄位。如果 DTO 中不存在該字段,則不會取得該字段:
public List<BookView> findAllByTitleLike(String title) {
List<BookView> values = sqlClient.createQuery(BookTable.$)
.where(BookTable.$.title()
.like(title))
.select(BookTable.$.fetch(BookView.class))
.execute();
return values;
}
這裡我們使用了上一節的BookView DTO。我們也可以透過 Fetcher 的 ad-hoc API 指定需要讀取的欄位。它與我們寫入資料庫時使用的非常相似:
public List<BookView> findAllByTitleLikeProjection(String title) {
List<Book> books = sqlClient.createQuery(BookTable.$)
.where(BookTable.$.title()
.like(title))
.select(BookTable.$.fetch(Fetchers.BOOK_FETCHER.title()
.createdAt()
.author()))
.execute();
return books.stream()
.map(BookView::new)
.collect(Collectors.toList());
}
這裡,我們使用 Object Fetcher API 來建構表示待讀取結構體的 DTO。但我們仍然在呼叫處(而不是聲明處)標記待讀取的列。這種方法與臨時創建用於保存的 DTO 非常相似。
7. 交易管理
最後,我們將快速回顧 Jimmer 的事務管理方式。通常,Jimmer 本身沒有內建的事務管理機制。因此,Jimmer 嚴重依賴 Spring 框架的事務管理基礎架構。例如,我們來回顧一下本地事務管理(非分散式)的使用情況,這是最常見的場景。在這種情況下,Jimmer 依賴 Spring 的TransactionSynchronizationManager功能以及綁定到目前執行緒的事務連線。
綜上所述,Spring 的@Transactional的傳統用法對 Jimmer 是可行的。透過 Spring 的TransactionTemplate進行命令式事務管理對 Jimmer 也是可行的。
8. 結論
在本文中,我們討論了 Jimmer ORM。正如我們所見,Jimmer 在數據操作方面採用了獨特的方法。 JPA 和 Hibernate 主要透過註解來表達與資料庫的互動方式,而 Jimmer 則要求開發人員在呼叫點動態提供所有資訊。為此,Jimmer 使用了 DTO,我們通常使用 Jimmer 的 DTO 語言來產生 DTO。但是,我們也可以臨時建立它們。在事務管理方面,Jimmer 仰賴 Spring 框架的基礎架構。
與往常一樣,本文的源代碼可在 GitHub 上找到。