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 上找到。