太炸了,三個注解!Spring Boot + JPA代碼量暴減60%
環(huán)境:SpringBoot3.4.2
1. 簡介
在Spring Boot結(jié)合JPA進行開發(fā)時,面對復雜查詢或動態(tài)過濾等常見場景,如果未能及時更新自身的技術(shù)知識儲備,就可能陷入編寫大量冗余代碼的困境。如下問題:
1?? 查詢邏輯分散:動態(tài)條件查詢需在Service層手動拼接CriteriaBuilder或Specification,導致代碼臃腫且難以維護;2?? 重復計算邏輯:如計算總金額、統(tǒng)計關(guān)聯(lián)表數(shù)量等衍生字段,需在實體中編寫冗余的字段或通過DTO層重復查詢;3?? 硬編碼過濾:數(shù)據(jù)權(quán)限控制常通過全局攔截器或硬編碼SQL實現(xiàn),缺乏靈活性且難以擴展;
這些痛點導致代碼量激增、維護成本高昂,且業(yè)務(wù)邏輯與數(shù)據(jù)庫操作強耦合。
解決方案
@Formula、@SQLRestriction (@Where)、@Filter三大注解直擊痛點:
? @Formula:實體字段直接映射SQL表達式,替代重復計算邏輯;
? @SQLRestriction:實體級原生SQL條件,簡化動態(tài)查詢;
? @Filter:參數(shù)化過濾條件,支持會話級控制,告別硬編碼。
用好這3個強大的注解,能讓代碼量暴減60%,性能與可維護性雙提升!
接下來,我們將詳細的介紹這3個注解的詳細應(yīng)用。
2. 實戰(zhàn)案例
2.1 @Formula
通過該注解你可以指定一個用原生SQL編寫的表達式,該表達式用于讀取屬性的值,而不是將該字段映射到數(shù)據(jù)庫中。@Formula映射定義了一個"派生"屬性,當從數(shù)據(jù)庫讀取實體時,該屬性的狀態(tài)是根據(jù)其他列和函數(shù)計算得出的。如下示例:
拼接字段值
private String name;
private BigDecimal price;
@Formula("(concat(name, '/', price))")
private String info ;
該示例中,并不會在數(shù)據(jù)庫中創(chuàng)建info字段,而是通過這里定義的表達式concat(name, '/', price)(concat數(shù)據(jù)庫函數(shù))將name字段值與price字段值通過 "/" 拼接在一起。
當我們執(zhí)行查詢時,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的一部分進行查詢。
我們不僅僅可以寫表達式,我們還可以執(zhí)行SQL語句。
SQL子句
@Formula("(select sum(s.sale_price * s.quantity) from sales_detail s where s.product_id = id)")
private BigDecimal salePrice ;
同樣的該salePrice字段并不會在數(shù)據(jù)庫中創(chuàng)建。當執(zhí)行查詢時,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的一部分進行了子查詢。
對于這種子查詢,我們還是需要結(jié)合自己的場景來決定是否適合通過此種方式進行查詢。
2.2 @SQLRestriction
該注解可以指定一個用原生SQL編寫的約束條件,該約束條件將被添加到為實體或集合生成的SQL中。簡單說,就是可以在對當前實體查詢時動態(tài)添加查詢條件。
@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執(zhí)行:
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 注解的問題在于,它僅允許我們指定一個不包含參數(shù)的靜態(tài)查詢,并且無法根據(jù)需求動態(tài)啟用或禁用它。@Filter 注解的作用與 @SQLRestriction 類似,但它還可以在會話(session)級別啟用或禁用,并且支持參數(shù)化。
@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個參數(shù)state和stock。
接著,我們通過@Filters注解,定義了具體的過濾條件,其中name是上面@FilterDef定義的名稱,condition則為執(zhí)行時動態(tài)添加的條件。
要使用定義的@Filter條件,我們這里通過AOP的方式動態(tài)設(shè)置。
首先,我們定義一個注解,只有使用了該注解的方法,才會在執(zhí)行之前開啟過濾過功能。
@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 {
// 從其它地方獲取參數(shù)值
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個參數(shù)直接寫死了。
業(yè)務(wù)代碼
@EnableFilter
public List<Product> query() {
return this.productRepository.findAll() ;
}
執(zhí)行后生成的SQL如下:
圖片
動態(tài)添加了查詢條件。
當我們沒有注解時,生成SQL如下:
圖片