簡介
對任何企業及應用來說,數據模型都是應用的基石之一。在設計數據模型時,除了需要考慮業務數據的要求之外,還應該思考一些有可能會影響應用程序設計的問題。比如,是否需要數據庫錶行級的安全機制?是否需要使用軟刪除?是否需要 CRUD API,如果需要的話,誰來幹這個枯燥的活?
事實上,JPA 已經是 Java 應用程序中創建數據模型的標準了。但是 JPA 不提供能實現高級安全機制的接口,也沒有軟刪除,所以有需求的程序員只能自己實現。
Jmix 框架提供了通用的引擎,可以爲使用 JPA 的應用程序增加額外的功能。在不破壞 JPA 兼容性的情況下,Jmix 框架可以讓您愉快的使用高級數據安全機制,無感的使用軟刪除,並且爲 CRUD 操作提供通用 REST API 以及其他的功能。
本文將討論 Jmix 中的數據訪問層,談談使用該框架及其工具到底能做什麼,還會討論數據訪問層的一些底層原理。
數據模型,JPA 和 @JmixEntity
對於熟悉 JPA 的 Java 開發者來說,使用 Jmix 不需要學習任何新的知識。只需要創建 JPA 實體即可。Jmix 也提供了可視化設計器可以幫助完成這一工作。
在 Jmix 的可視化設計器中,您需要提供實體名稱,選擇 ID 類型(UUID、Long、Integer、String 或者嵌入類),然後選擇需要支持的功能:
- 實體版本 - 支持樂觀鎖
- 創建時審計 - 增加創建時間、創建人
- 更新時審計 - 增加更新時間、更新人
- 軟刪除 - 增加刪除時間、刪除人
最後,您會得到類似如下的 JPA 類:
@JmixEntity
@Table(name = "SPEAKER")
@Entity(name = "Speaker")
public class Speaker {
@JmixGeneratedValue
@Column(name = "ID", nullable = false)
@Id
private UUID id;
@Email
@Column(name = "EMAIL", unique = true)
private String email;
@Column(name = "NAME", nullable = false)
@NotNull
private String name;
@Column(name = "VERSION", nullable = false)
@Version
private Integer version;
@CreatedBy
@Column(name = "CREATED_BY")
private String createdBy;
@LastModifiedBy
@Column(name = "LAST_MODIFIED_BY")
private String lastModifiedBy;
@DeletedDate
@Column(name = "DELETED_DATE")
@Temporal(TemporalType.TIMESTAMP)
private Date deletedDate;
// 此處省略部分字段
}
注意,Jmix 並非侵入式的影響你的代碼。生成的實體只是添加了一些註解,不需要實現任何框架特有的接口,更不需要繼承某個類。這樣的非侵入式設計,在實現您應用程序特有的實體關係和架構時,能保持足夠的靈活度以適應你的業務需求。
Jmix 使用的大部分註解要麼來自 JPA 要麼來自 Spring Boot JPA 庫:@Entity、@Versioned、@CreatedBy、@LastModifiedBy
,您之前也許都見過。Jmix 框架依賴 Spring Boot 的具體實現來支持樂觀鎖以及創建和更新審計。
下面看看數據模型中幾個 Jmix 特有的註解:
@JmixEntity
- 引導 Jmix 數據訪問層將此類添加至實體倉庫(Entities repository)@JmixGeneratedValue
- 表示實體的ID
屬性必須由 Jmix 生成並指定@DeletedBy
和@DeletedDate
- 標記字段,當使用軟刪除方案時用來標記實體的刪除狀態
您也許會問:“什麼是 Jmix 數據訪問層中的實體倉庫?你們是不是有自己的數據訪問引擎?” 答案是也不是。Jmix 框架會存儲應用程序數據模型的額外信息 - metamodel(元模型),並且增加了一個特殊的工具 - DataManager
- 用來訪問數據,但是其底層還是用的 JPA。
Metamodel 和 DataManager
Jmix 基於 Spring boot 框架構建。Boot 以廣泛使用 IoC 模式而聞名天下,該模式意味着將有關 bean 的所有數據存儲在特殊的存儲庫中 - ApplicationContext
。除了這個存儲之外,Jmix 將關於應用程序數據模型的所有數據(比如 metadata)存儲在另一個存儲庫中 - Metadata
。
Jmix 中 Metadata
是數據訪問層的核心。包含關於實體、屬性、實體關係等等所有的信息。這些信息是在應用程序啓動時對類進行掃描的過程中一次收集的,Jmix 用這些信息來實現神奇的功能:從行級數據安全機制到自動生成的管理界面 UI 的雙向數據綁定。
爲了有效的使用 Jmix 數據訪問層,需要使用 DataManager 組件。該組件是 JPA 中著名 EntityManager
的包裝類。與 EntityManager
類似,DataManager
能使用 JPQL 查詢語句或者 ID 加載實體,保存或刪除實體以及對所選實例進行計數。
下面是 DataManager
的一些示例用法。
通過 ID
加載一個實體,使用 Java 類作爲參數:
Speaker loadSpeakerById(UUID speakerId) {
return dataManager.load(Speaker.class)
.id(speakerId)
.one();
}
使用 JPQL 查詢加載數據:
List<Speaker> loadSpeakesByCompanyEmail() {
return dataManager.load(Speaker.class)
.query("select s from Speaker s where s.email like :email")
.parameter("email", "%@company.com")
.list();
}
DataManager
使用 Metamodel
提供一些額外的功能。是這樣實現的:在對應用程序數據庫的請求交由 EntityManager
執行之前,對請求進行攔截並修改。
我們仔細看看 Jmix 提供的這些功能。
高級數據安全
首先需要提到的是 Jmix 的數據安全子系統。框架使用基於角色的安全機制並定義了兩種類型的角色:
1. 資源角色(Resource role)- 授予訪問實體對象並在系統中執行特定操作的權限:對實體,實體屬性進行 CURD 操作,使用 UI 界面等等。
2. 行級角色(Row-level role)- 可以限制對錶中特定數據行的訪問,換句話說,即限制訪問某些實體實例。
下面是資源角色的示例,可以定義實體 CRUD 操作的策略,甚至可以定義針對實體屬性操作的策略:
@ResourceRole(name = "Speaker Admin", code = "speaker-admin-role")
public interface SpeakerAdminRole {
@EntityPolicy(entityName = "Speaker",
actions = {EntityPolicyAction.ALL})
@EntityAttributePolicy(entityName = "Speaker",
attributes = "{name, email}",
action = EntityAttributePolicyAction.MODIFY)
void speakersAccess();
}
爲了實現這個功能,DataManager
會分析 JPQL,然後依據策略的定義限制該 JPQL 的執行或修改 JPQL 中的屬性列表以符合安全策略。
如需定義行級角色,您需要制定一個 JPQL 策略,該 JQPL 策略由 where
以及可選的 join
子句組成,用來限制從數據庫表中選取的數據:
@RowLevelRole(name = "Speaker's Talks",
code = "speakers-talks-role")
public interface SpeakersTalksRole {
@JpqlRowLevelPolicy(entityClass = Talk.class,
where = "{E}.speaker.id = :current_user_id")
void accessibleTalks();
}
因此,帶有行級角色用戶發起的所有查詢都會通過這種方式進行轉換:where
子句會用 AND 操作符添加在現有的條件之後,join
子句也會添加在現有的之後。如果 JPQL 中沒有這兩個關鍵字,相應的子句會直接添加在後面。
Metadata
存儲庫在這裏扮演了重要的角色。它能幫助我們正確的解析並修改 JPQL 並能找出受限的屬性和實體,無論使用的是 JPQL 還是 Java 類來定義 DB 查詢。
軟刪除
某些情況下,有必要在企業級應用中使用軟刪除。我們發佈了 CUBA 博客“是否實施軟刪除,這是個問題!” 專門討論了軟刪除模式的優缺點。Jmix 是下一代 CUBA,因此也支持軟刪除。
如果實體中有 @DeletedBy
和 @DeletedDate
註解的字段,則該實體會自動啓用軟刪除模式。對於這些實體,所有 DataManager
中調用的 remove()
都會被轉換成 UPDATE
語句,而所有的 find()
請求都會被修改,添加一個合適的 where
子句用來過濾那些被 “刪除” 的實體。
@JmixEntity
@Table(name = "SPEAKER")
@Entity(name = "Speaker")
public class Speaker {
@JmixGeneratedValue
@Column(name = "ID", nullable = false)
@Id
private UUID id;
@DeletedBy
@Column(name = "DELETED_BY")
private String deletedBy;
@DeletedDate
@Column(name = "DELETED_DATE")
@Temporal(TemporalType.TIMESTAMP)
private Date deletedDate;
// 此處省略部分字段
}
這裏我們需要 Metadata
來找出哪一列需要更新或者添加到 where 子句中。與 CUBA 不同,Jmix 不要求實體實現一個特定的接口才能支持軟刪除。而只需要將兩個註解添加至實體的兩個字段即可。
儘管我們用了軟刪除,但是數據並沒有刪掉,因此您還是可以使用 “硬刪除”,如果想看到軟刪除的數據,可以使用 JPA 的 EntityManager
和原生 SQL 查詢。
REST 和 GraphQL
自動創建 CRUD API 是 Jmix metamodel 的亮點。當您擁有應用程序中所有實體的信息時,創建實體操作的 API 就非常容易了。
比如,如果有數據模型的元數據,那麼處理下面這個 HTTP 請求將不是難事:
GET 'http://server:8080/rest/entities/Speaker'
比如,CRUD 操作的 endpoints 列表是這樣的:
使用 Jmix 的好處就是您不需要手動實現這些枯燥的 endpoints。由於有 metamodel,所有這些都能自動生成。在開發 “爲前端服務的後端” 時,可以從自動生成的代碼開始逐步構建您自己的特定 REST API。
如果您的要求比通用 CRUD REST API 更靈活,可以使用 GraphQL。GraphQL 的一個核心概念就是 schema。使用 Jmix 的 metamodel,我們毫不費力就能獲得 GraphQL schema 所需的所有數據。
也就是說,Jmix 應用程序使用 GraphQL API 是和 REST API 類似,能以通用的方式實現(很快將發佈)。因此,在您的 Jmix 應用程序中添加 GraphQL endpoint,只需在構建腳本中簡單增加一個 starter 即可:
implementation 'io.jmix.graphql:jmix-graphql-starter'
然後就可以使用類似這樣的查詢了:
{
speakerCount
speakerList(limit: 10, offset: 0, orderBy: {name: ASC}) {
id
name
email
}
}
結論
只需在 Jmix 中使用熟悉的 JPA 註解定義數據模型,您就能創建一個可用的應用程序。藉助 Jmix 的 metamodel,無需進行額外的編碼即可使用下列功能:
1. 行級安全機制
2. 軟刪除
3. CRUD REST API
4. GraphQL API
如您所見,使用 Jmix 創建一個簡單的以數據爲中心的應用程序,並帶有高級功能和管理界面將變得非常容易。