1. 概述

数据库视图是关系型数据库中一种类似表的结构,其数据源来自一个或多个表的连接结果。虽然 Spring Data 仓库通常用于数据库表,但同样可以高效地应用于数据库视图。本文将探讨如何将 Spring Data 仓库应用于数据库视图。

2. 数据库表设计

本文采用 H2 数据库系统演示数据库视图概念,使用两个示例表:SHOPSHOP_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 图示例如下:

实体关系模型

3. 数据库视图

数据库视图提供虚拟表,通过预定义查询获取数据。 使用视图而非直接连接查询的优势包括:

  • 简洁性:视图封装复杂连接,避免重复编写相同查询
  • 安全性:视图可限制暴露基表的敏感数据
  • 可维护性:基表结构变更时只需更新视图定义,无需修改应用代码

3.1 标准视图与物化视图

两种常见视图类型及其特点:

特性 标准视图 物化视图
数据源 动态生成自基表 物理存储查询结果
性能 较慢(动态生成) 较快(直接读取物理表)
数据新鲜度 实时数据 可能过期,需定期刷新
适用场景 实时数据需求 计算密集型查询,非实时场景

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 shop_id shop_location transaction_year transaction_month amount
1 1 Ealing 2024 1 10.78
2 1 Ealing 2024 2 13.58
3 1 Ealing 2024 3 14.48
4 2 Richmond 2024 1 17.98
5 2 Richmond 2024 2 8.49
6 2 Richmond 2024 3 13.78

4. 实体类定义

为视图 SHOP_SALE_VIEW 定义实体类,与普通表定义几乎相同。JPA 要求实体必须有主键,有两种策略:

4.1 物理主键

多数场景下,可选择视图中的列组合作为主键。 本例中 shop_idtransaction_yeartransaction_month 可唯一标识行。

首先定义复合主键类:

public class ShopSaleCompositeId {
    private int shopId;
    private int year;
    private int month;
    // 构造器、getter/setter
}

在实体类中使用 @EmbeddedId 嵌入复合主键,并通过 @AttributeOverrides 映射列名:

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

    // 构造器、getter/setter
}

4.2 虚拟主键

当视图无唯一列组合时,可生成虚拟主键模拟行唯一性。 本例视图定义中已使用 ROW_NUMBER() OVER () 生成 id 列:

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

    // 构造器、getter/setter
}

⚠️ 注意:这些标识符仅对当前结果集有效,重新查询时行号可能变化,后续查询的相同行号可能代表不同数据行。

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 (?,?,?,?,?,?)]

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 的所有查询方法,排除修改方法。

为复合主键实体扩展仓库,添加按 shopId 查询的方法:

public interface ShopSaleRepository extends ViewRepository<ShopSale, ShopSaleCompositeId> {
    List<ShopSale> findByIdShopId(Integer shopId);
}

方法名 findByIdShopId 源于实体类中的 id.shopId 属性。

5.2 虚拟主键场景

虚拟主键是人工生成,无法真正标识数据行,需定义另一基础仓库接口,排除按主键查询的方法:

public interface ViewNoIdRepository<T, K> extends Repository<T, K> {
    long count();

    List<T> findAll();
}

扩展仓库接口:

public interface ShopSaleRepository extends ViewNoIdRepository<ShopSale, Long> {
    List<ShopSale> findByShopId(Integer shopId);
}

此时实体类直接定义了 shopId,故可直接使用 findByShopId

6. 总结

本文介绍了数据库视图的基本概念,对比了标准视图与物化视图的差异。根据数据特性,讨论了视图主键的两种实现策略(物理主键和虚拟主键),并基于主键策略展示了实体类和基础仓库接口的定义方式。

完整示例代码可在 GitHub 获取。


原始标题:Spring Data JPA Repository for Database View