Spring Data JPA如何用於資料庫檢視?

banq發表於2024-03-05

資料庫檢視是關聯式資料庫系統中的一種類似表的結構,其中資料來源來自連線在一起的一個或多個表。

雖然 Spring Data儲存庫通常用於資料庫表,但它們也可以有效地應用於資料庫檢視。

在本教程中,我們將探索採用 Spring Data 儲存庫作為資料庫檢視。

在本教程中,我們將採用 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)模型中,我們可以將其描述為一對多關係,其中一個商店可以有多個交易。儘管如此,每筆交易僅與一家商店相關。

資料庫檢視
資料庫檢視提供了一個虛擬表,該表從預定義查詢的結果中收集資料。使用資料庫檢視而不是連線查詢有以下優點:

  • 簡單性——檢視封裝了複雜的連線,無需重複重寫相同的連線查詢
  • 安全性 – 檢視可能僅包含基表中的資料子集,從而降低了暴露基表中敏感資訊的風險
  • 可維護性 – 當基表結構更改時更新檢視定義,無需修改引用應用程式中已更改基表的查詢


有兩種常見型別的資料庫檢視,它們有不同的用途:

  • 標準檢視 – 這些是透過在查詢時執行預定義的 SQL 查詢而生成的。它們本身不儲存資料。所有資料都儲存在底層基表中。
  • 物化檢視 – 這些與標準檢視類似,也是從預定義的 SQL 查詢生成的。相反,它們將查詢結果複製到資料庫中的物理表中。後續查詢將從該表中檢索資料,而不是動態生成資料。

標準檢視示例
在我們的示例中,我們希望定義一個檢視來總結每個日曆月商店的總銷售額。事實證明,物化檢視是合適的,因為前幾個月的銷售額保持不變。除非需要當月的資料,否則計算總銷售額時不需要實時資料。

但是,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;

實體Bean定義
現在,我們可以為資料庫檢視SHOP_SALE_VIEW定義實體 bean 。實際上,該定義與為普通資料庫表定義實體 bean 幾乎相同。

在 JPA 中,實體 bean 要求它必須具有主鍵。我們可以考慮兩種策略來在資料庫檢視中定義主鍵。

物理主鍵
在大多數情況下,我們可以在檢視中選取一列或多列來標識資料庫檢視中一行的唯一性。在我們的場景中,商店 ID、年份和月份可以唯一標識檢視中的每一行。

因此,我們可以透過列shop_id、transaction_year和transaction_month匯出複合主鍵。在JPA中,我們首先要定義一個單獨的類來表示複合主鍵:

public class ShopSaleCompositeId {
    private int shopId;
    private int year;
    private int month;
    <font>// constructors, getters, setters<i>
}

隨後,我們使用@EmbeddedId將此複合 ID 類嵌入到實體類中,並透過@AttributeOverrides註解複合 ID 來定義列對映:

@Entity
@Table(name = <font>"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<i>
}

虛擬主鍵
在某些場景下,由於缺乏可以確保資料庫檢視中每一行的唯一性的列組合,定義物理主鍵是不可行的。作為一種解決方法,我們可以生成一個虛擬主鍵來模擬行的唯一性。

在我們的資料庫檢視定義中,我們有一個附加的列id,它利用ROW_NUMBER() OVER ()生成行號作為識別符號。這是我們採用虛擬主鍵策略時的實體類定義:

@Entity
@Table(name = <font>"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<i>
}

需要注意的是,這些識別符號特定於當前結果集。重新查詢時分配給每行的行號可能不同。因此,後續查詢中的相同行號可能代表資料庫檢視中的不同行。

Spring檢視儲存庫
根據資料庫的不同,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 儲存庫時將排除這些方法並僅公開資料檢索方法。

 物理主鍵
對於具有物理主鍵的檢視,我們可以定義一個新的基本儲存庫介面,僅公開資料檢索方法:

@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。

虛擬主鍵
當我們處理具有虛擬主鍵的資料庫檢視的儲存庫設計時,我們的方法略有不同,因為虛擬主鍵是人造的,無法真正識別資料行的唯一性。

由於這種性質,我們將定義另一個基本儲存庫介面,該介面也排除透過主鍵的查詢方法。這是因為我們使用的是虛擬主鍵,並且使用假主鍵檢索資料是沒有意義的:

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() 。
 

相關文章