太炸了,三個注解!Spring Boot + JPA代碼量暴減60%
環境:SpringBoot3.4.2
1. 簡介
在Spring Boot結合JPA進行開發時,面對復雜查詢或動態過濾等常見場景,如果未能及時更新自身的技術知識儲備,就可能陷入編寫大量冗余代碼的困境。如下問題:
1?? 查詢邏輯分散:動態條件查詢需在Service層手動拼接CriteriaBuilder或Specification,導致代碼臃腫且難以維護;2?? 重復計算邏輯:如計算總金額、統計關聯表數量等衍生字段,需在實體中編寫冗余的字段或通過DTO層重復查詢;3?? 硬編碼過濾:數據權限控制常通過全局攔截器或硬編碼SQL實現,缺乏靈活性且難以擴展;
這些痛點導致代碼量激增、維護成本高昂,且業務邏輯與數據庫操作強耦合。
解決方案
@Formula、@SQLRestriction (@Where)、@Filter三大注解直擊痛點:
? @Formula:實體字段直接映射SQL表達式,替代重復計算邏輯;
? @SQLRestriction:實體級原生SQL條件,簡化動態查詢;
? @Filter:參數化過濾條件,支持會話級控制,告別硬編碼。
用好這3個強大的注解,能讓代碼量暴減60%,性能與可維護性雙提升!
接下來,我們將詳細的介紹這3個注解的詳細應用。
2. 實戰案例
2.1 @Formula
通過該注解你可以指定一個用原生SQL編寫的表達式,該表達式用于讀取屬性的值,而不是將該字段映射到數據庫中。@Formula映射定義了一個"派生"屬性,當從數據庫讀取實體時,該屬性的狀態是根據其他列和函數計算得出的。如下示例:
拼接字段值
private String name;
private BigDecimal price;
@Formula("(concat(name, '/', price))")
private String info ;
該示例中,并不會在數據庫中創建info字段,而是通過這里定義的表達式concat(name, '/', price)(concat數據庫函數)將name字段值與price字段值通過 "/" 拼接在一起。
當我們執行查詢時,sql輸出如下:
org.hibernate.SQL Line:135 - select p1_0.id,p1_0.deleted,
(concat(p1_0.name, '/', p1_0.price)),p1_0.name,
p1_0.price,p1_0.stock from product p1_0
將上面的表達式作為select的一部分進行查詢。
我們不僅僅可以寫表達式,我們還可以執行SQL語句。
SQL子句
@Formula("(select sum(s.sale_price * s.quantity) from sales_detail s where s.product_id = id)")
private BigDecimal salePrice ;
同樣的該salePrice字段并不會在數據庫中創建。當執行查詢時,SQL輸出如下:
SELECT
p1_0.id,
p1_0.deleted,(
concat( p1_0.NAME, '/', p1_0.price )),
p1_0.name,
p1_0.price,(
SELECT
sum( s.sale_price * s.quantity )
FROM
sales_detail s
WHERE
s.product_id = p1_0.id
),
p1_0.stock
FROM
product p1_0
表達式同樣作為select的一部分進行了子查詢。
對于這種子查詢,我們還是需要結合自己的場景來決定是否適合通過此種方式進行查詢。
2.2 @SQLRestriction
該注解可以指定一個用原生SQL編寫的約束條件,該約束條件將被添加到為實體或集合生成的SQL中。簡單說,就是可以在對當前實體查詢時動態添加查詢條件。
@Entity
@Table(name = "product")
@SQLRestriction("deleted = 0")
public class Product {
// ...其它屬性
/**0: 未刪除, 1: 已刪除*/
@Column(columnDefinition = "int default 0")
private Integer deleted ;
}
這里通過@SQLRestriction注解添加了 "deleted = 0",當我們對該實體Product進行查詢時都會在原來SQL中添加該條件。如下SQL執行:
SELECT
p1_0.id,
p1_0.deleted,
p1_0.name,
p1_0.price
p1_0.stock
FROM
product p1_0
WHERE
(p1_0.deleted = 0)
我們不僅僅可以在實體類上添加,還可以在集合屬性上添加。
集合屬性上
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "product_id")
@SQLRestriction("deleted = 0")
private Set<ProductDetail> productDetails = new HashSet<>() ;
@Entity
public class ProductDetail {
// ...
/**0: 未刪除, 1: 已刪除*/
@Column(columnDefinition = "int default 0")
private Integer deleted ;
}
當我們通過Product實體查詢時,生成SQL如下:
圖片
2.3 @Filter
@SQLRestriction 注解的問題在于,它僅允許我們指定一個不包含參數的靜態查詢,并且無法根據需求動態啟用或禁用它。@Filter 注解的作用與 @SQLRestriction 類似,但它還可以在會話(session)級別啟用或禁用,并且支持參數化。
@Entity
@Table(name = "product")
@FilterDef(name = "filterByDeletedAndStock", parameters = {
@ParamDef(name = "state", type = Integer.class),
@ParamDef(name = "stock", type = Integer.class)
})
@Filters({
@Filter(name = "filterByDeletedAndStock", condition = "deleted=:state and stock >:stock")
})
public class Product {}
在這里,我們通過@FilterDef注解,定義了一個名為filterByDeletedAndStock過濾器,并且還定義了2個參數state和stock。
接著,我們通過@Filters注解,定義了具體的過濾條件,其中name是上面@FilterDef定義的名稱,condition則為執行時動態添加的條件。
要使用定義的@Filter條件,我們這里通過AOP的方式動態設置。
首先,我們定義一個注解,只有使用了該注解的方法,才會在執行之前開啟過濾過功能。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface EnableFilter {
}
接下來,定義切面攔截使用了@EnableFilter注解的方法。
@Component
@Aspect
public class FilterAspect {
@PersistenceContext
private EntityManager entityManager;
@Around("@annotation(com.pack.formula.annotation.EnableFilter)")
public Object doProcess(ProceedingJoinPoint joinPoint) throws Throwable {
try {
// 從其它地方獲取參數值
int state = 0 ;
int stock = 80 ;
Filter filter = entityManager.unwrap(Session.class).enableFilter("filterByDeletedAndStock");
filter.setParameter("state", state) ;
filter.setParameter("stock", stock) ;
return joinPoint.proceed();
} catch (Throwable ex) {
throw ex;
} finally {
entityManager.unwrap(Session.class).disableFilter("filterByDeletedAndStock") ;
}
}
}
我們這里是模擬,所以@Filter中定義的2個參數直接寫死了。
業務代碼
@EnableFilter
public List<Product> query() {
return this.productRepository.findAll() ;
}
執行后生成的SQL如下:
圖片
動態添加了查詢條件。
當我們沒有注解時,生成SQL如下:
圖片