用於資料庫視圖的 Spring Data JPA 儲存庫
1. 概述
資料庫視圖是關聯式資料庫系統中的一種類似表的結構,其中資料來源來自連接在一起的一個或多個表。
雖然 Spring Data 儲存庫通常用於資料庫表,但它們也可以有效地應用於資料庫視圖。在本教程中,我們將探索採用 Spring Data 儲存庫作為資料庫視圖。
2. 資料庫表設置
在本教程中,我們將採用 H2 資料庫系統進行資料定義,並使用兩個範例表SHOP
和SHOP_TRANSACTION
來示範資料庫視圖概念。
SHOP
表格儲存店鋪資訊:
CREATE TABLE SHOP
(
shop_id int AUTO_INCREMENT,
shop_location varchar(100) NOT NULL UNIQUE,
PRIMARY KEY(shop_id)
);
SHOP_TRANSACTION
表儲存與商店關聯的交易記錄以及透過shop_id
對SHOP
表的引用:
CREATE TABLE SHOP_TRANSACTION
(
transaction_id bigint AUTO_INCREMENT,
transaction_date date NOT NULL,
shop_id int NOT NULL,
amount decimal(8,2) NOT NULL,
PRIMARY KEY(transaction_id),
FOREIGN KEY(shop_id) REFERENCES SHOP(shop_id)
);
在實體關係(ER)模型中,我們可以將其描述為一對多關係,其中一個商店可以有多個交易。儘管如此,每筆交易僅與一家商店相關。我們可以使用 ER 圖直觀地表示這一點:
3. 資料庫視圖
資料庫視圖提供了一個虛擬表,該表從預先定義查詢的結果中收集資料。使用資料庫視圖而不是連線查詢有以下優點:
- 簡單性-視圖封裝了複雜的連接,無需重複重寫相同的連接查詢
- 安全性 – 視圖可能僅包含基底表中的資料子集,從而降低了暴露基底表中敏感資訊的風險
- 可維護性 – 當基底表結構變更時更新檢視定義,無需修改引用應用程式中已變更基底表的查詢
3.1.標準視圖和物化視圖
有兩種常見類型的資料庫視圖,它們有不同的用途:
- 標準視圖 – 這些是透過在查詢時執行預先定義的 SQL 查詢而產生的。它們本身不儲存資料。所有資料都儲存在底層基表中。
- 物化視圖 – 這些與標準視圖類似,也是從預先定義的 SQL 查詢產生的。相反,它們將查詢結果複製到資料庫中的實體表中。後續查詢將從該表中檢索數據,而不是動態產生數據。
以下比較表突顯了標準視圖和物化視圖的不同特徵,有助於根據特定要求選擇適當的視圖類型:
標準視圖 | 物化視圖 | |
---|---|---|
資料來源 | 透過預定義查詢從基礎表動態生成 | 包含來自預定義查詢的資料的實體表 |
表現 | 由於動態查詢產生而速度較慢 | 由於從物理表檢索數據,速度更快 |
陳舊性 | 始終返回新數據 | 可能會變得陳舊並需要定期刷新 |
使用案例 | 適合即時數據 | 當資料新鮮度不重要時,適合計算成本較高的查詢 |
3.2.標準視圖範例
在我們的範例中,我們希望定義一個視圖來總結每個日曆月商店的總銷售額。事實證明,物化視圖是合適的,因為前幾個月的銷售額保持不變。除非需要當月的數據,否則計算總銷售額時不需要即時數據。
但是,H2資料庫不支援物化視圖。我們將建立一個標準視圖:
CREATE VIEW SHOP_SALE_VIEW AS
SELECT ROW_NUMBER() OVER () AS id, shop_id, shop_location, transaction_year, transaction_month, SUM(amount) AS total_amount
FROM (
SELECT
shop.shop_id, shop.shop_location, trans.amount,
YEAR(transaction_date) AS transaction_year, MONTH(transaction_date) AS transaction_month
FROM SHOP shop, SHOP_TRANSACTION trans
WHERE shop.shop_id = trans.shop_id
) SHOP_MONTH_TRANSACTION
GROUP BY shop_id, transaction_year, transaction_month;
查詢檢視後,我們應該會獲得以下資料:
ID | 店家ID | 商店位置 | 交易年 | 交易月 | 數量 |
---|---|---|---|---|---|
1 | 1 | 伊靈 | 2024年 | 1 | 10.78 |
2 | 1 | 伊靈 | 2024年 | 2 | 13.58 |
3 | 1 | 伊靈 | 2024年 | 3 | 14.48 |
4 | 2 | 列治文 | 2024年 | 1 | 17.98 |
5 | 2 | 列治文 | 2024年 | 2 | 8.49 |
6 | 2 | 列治文 | 2024年 | 3 | 13.78 |
4. 實體Bean定義
現在,我們可以為資料庫檢視SHOP_SALE_VIEW
定義實體 bean。實際上,該定義與為普通資料庫表定義實體 bean 幾乎相同。
在 JPA 中,實體 bean 要求它必須具有主鍵。我們可以考慮兩種策略來在資料庫視圖中定義主鍵。
4.1.物理主鍵
在大多數情況下,我們可以在視圖中選取一列或多列來識別資料庫視圖中一行的唯一性。在我們的場景中,商店 ID、年份和月份可以唯一標識視圖中的每一行。
因此,我們可以透過列shop_id
、 transaction_year
和transaction_month
導出複合主鍵。在JPA中,我們首先要定義一個單獨的類別來表示複合主鍵:
public class ShopSaleCompositeId {
private int shopId;
private int year;
private int month;
// constructors, getters, setters
}
隨後,我們使用@EmbeddedId
將此複合 ID 類別嵌入到實體類別中,並透過@AttributeOverrides
註解複合 ID 來定義列映射:
@Entity
@Table(name = "SHOP_SALE_VIEW")
public class ShopSale {
@EmbeddedId
@AttributeOverrides({
@AttributeOverride( name = "shopId", column = @Column(name = "shop_id")),
@AttributeOverride( name = "year", column = @Column(name = "transaction_year")),
@AttributeOverride( name = "month", column = @Column(name = "transaction_month"))
})
private ShopSaleCompositeId id;
@Column(name = "shop_location", length = 100)
private String shopLocation;
@Column(name = "total_amount")
private BigDecimal totalAmount;
// constructor, getters and setters
}
4.2.虛擬主鍵
在某些場景下,由於缺乏可以確保資料庫視圖中每一行的唯一性的列組合,定義實體主鍵是不可行的。作為一種解決方法,我們可以產生一個虛擬主鍵來模擬行的唯一性。
在我們的資料庫視圖定義中,我們有一個附加的列id
,它利用ROW_NUMBER() OVER ()
產生行號作為識別碼。這是我們採用虛擬主鍵策略時的實體類別定義:
@Entity
@Table(name = "SHOP_SALE_VIEW")
public class ShopSale {
@Id
@Column(name = "id")
private Long id;
@Column(name = "shop_id")
private int shopId;
@Column(name = "shop_location", length = 100)
private String shopLocation;
@Column(name = "transaction_year")
private int year;
@Column(name = "transaction_month")
private int month;
@Column(name = "total_amount")
private BigDecimal totalAmount;
// constructors, getters and setters
}
需要注意的是,這些標識符特定於當前結果集。重新查詢時分配給每行的行號可能不同。因此,後續查詢中的相同行號可能代表資料庫檢視中的不同行。
5. 查看儲存庫
根據資料庫的不同,Oracle 等系統可能支援可更新視圖,允許在某些條件下更新它們上的資料。然而,資料庫視圖大多是唯讀的。
對於唯讀資料庫視圖,沒有必要在我們的儲存庫中公開資料修改方法,例如save()
或delete()
。嘗試呼叫這些方法將引發異常,因為資料庫系統不支援此類操作:
org.springframework.orm.jpa.JpaSystemException: could not execute statement [Feature not supported: "TableView.addRow"; SQL statement:
insert into shop_sale_view (transaction_month,shop_id,shop_location,total_amount,transaction_year,id) values (?,?,?,?,?,?) [50100-224]] [insert into shop_sale_view (transaction_month,shop_id,shop_location,total_amount,transaction_year,id) values (?,?,?,?,?,?)]
基於這樣的理由,我們在定義 Spring Data JPA 儲存庫時將排除這些方法並僅公開資料檢索方法。
5.1.物理主鍵
對於具有實體主鍵的視圖,我們可以定義一個新的基本儲存庫接口,僅公開資料檢索方法:
@NoRepositoryBean
public interface ViewRepository<T, K> extends Repository<T, K> {
long count();
boolean existsById(K id);
List<T> findAll();
List<T> findAllById(Iterable<K> ids);
Optional<T> findById(K id);
}
@NoRepositoryBean
註解指示此介面是基本儲存庫接口,並指示 Spring Data JPA 不要在執行時建立此介面的實例。在此儲存庫介面中,我們包含ListCrudRepository
中的所有資料檢索方法,並排除所有資料變更方法。
對於具有複合 ID 的實體 bean,我們擴展ViewRepository
並定義一個附加方法來查詢shopId
的商店銷售情況:
public interface ShopSaleRepository extends ViewRepository<ShopSale, ShopSaleCompositeId> {
List<ShopSale> findByIdShopId(Integer shopId);
}
我們將查詢方法定義為findByIdShopId()
而不是findByShopId()
,因為它是從ShopSale
實體類別中的屬性id.shopId
衍生而來。
5.2.虛擬主鍵
當我們處理具有虛擬主鍵的資料庫視圖的儲存庫設計時,我們的方法略有不同,因為虛擬主鍵是人造的,無法真正識別資料行的唯一性。
由於這種性質,我們將定義另一個基本存儲庫接口,該接口也排除通過主鍵的查詢方法。這是因為我們使用的是虛擬主鍵,並且使用假主鍵檢索資料是沒有意義的:
public interface ViewNoIdRepository<T, K> extends Repository<T, K> {
long count();
List<T> findAll();
}
隨後,我們將其擴展為ViewNoIdRepository
來定義我們的儲存庫:
public interface ShopSaleRepository extends ViewNoIdRepository<ShopSale, Long> {
List<ShopSale> findByShopId(Integer shopId);
}
由於ShopSale
實體類別這次直接定義了shopId
,因此我們可以在儲存庫中使用findByShopId()
。
六,結論
本文介紹了資料庫視圖,並對標準視圖和物化視圖進行了簡要比較。
此外,我們也描述了根據資料的性質在資料庫視圖上套用不同的主鍵策略。最後,我們根據我們選擇的關鍵策略探索了實體 bean 和基本Repository
介面的定義。
與往常一樣,討論的範例可以在 GitHub 上找到。