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 图示例如下:
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_id
、transaction_year
和 transaction_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 获取。