記得前幾個月,spring4all 社區剛搞過一次技術話題討論:如何對 JPA 或者 MyBatis 進行技術選型?傳送門:http://www.spring4all.com/article/391 由于平時工作接觸較多的是 JPA,所以對其更熟悉一些,這一篇文章記錄下個人在使用 JPA 時的一些小技巧。補充說明:JPA 是一個規范,本文所提到的 JPA,特指 spring-data-jpa。
tips:閱讀本文之前,建議了解值對象和實體這兩個概念的區別和領域驅動設計的基本概念。
現實世界有很多一對一的關聯關系,如人和身份證,訂單和購買者...而在 JPA 中表達一對一的關聯,通常有三種方式。下面就以訂單(Order)和購買者(CustomerVo)為例來介紹這三種方式,這里 CustomerVo 的 Vo 指的是 Value Object。
字段平鋪
這可能是最簡單的方式了,由于一對一關聯的特殊性,完全可以在 Order 類中,使用幾個字段記錄 CustomerVo的屬性。
public class Order { /*其他字段*/ ... /* Customer相關字段 */ private int customerId; private String customerName; private String customerMobile;}
實際上大多數人就是這么做的,甚至都沒有意識到這三個字段其實是屬于同一個實體類。這種形式優點是很明顯的:簡單;缺點也是很明顯的,這不符合 OO 的原則,且不利于統一檢索和維護 CustomerVo 信息。
使用 @OneToOne
public class Order { @OneToOne private CustomerVo customerVo;}
這么做的確更“面向對象”了,但代價似乎太大了,我們需要在數據庫中額外維護一張 CustomerVo 表,關聯越多,代碼處理起來就越麻煩,得不償失。
使用 @Embedded
那有沒有能中和上述矛盾的方案呢?引出 @Embedded 這個注解。分析下初始需求,我們發現:CustomerVo 僅僅是作為一個值對象,并不是一個實體(這里牽扯到一些領域驅動設計的知識,值對象的特點是:作為實體對象的修飾,即 CustomerVo 這個整體是 Order 實體的一個屬性;不變性,CustomerVo 一旦生成后便不可被修改,除非被整體替換)
@Embedded 注解便是內嵌值對象最好的表達形式。
@Entitypublic class Order { @Embedded private CustomerVo customerVo;}
@Embeddablepublic class CustomerVo { private int customerId; private String customerName; private String customerMobile;}
Order 擁有 @Entity 注解,表明其是 DDD 中的實體;而 CustomerVo 擁有 @Embeddable 注解,表明其是 DDD 中的值對象。這也是為什么我一直在表達這樣一種觀點:JPA 是對 DDD 很好的實踐的。
關于實體類的設計技巧,在曹祖鵬老師的 github 中可以看到很成熟的方案,可能會顛覆你對實體類設計的認知:https://github.com/JoeCao/qbike/。
說到一對多,第一反應自然是使用 @OneToMany 注解。的確,我自己在項目中也主要使用這個注解來表達一對多的關聯,但這里提供另一個思路,來關聯一對多的值對象。
以商品和商品組圖來舉例。
使用 @OneToMany
還是先想想我們原來會怎么做,保存一個 List, 一種方式是這樣
public class Goods { // 以逗號分隔 private String pictures;}
使用字符串存儲,保存成 JSON 數組的形式,或者以逗號分隔都行。
如果圖片還要保存順序,縮略圖,那就必須要得使用一對多的關聯了。
@Entitypublic class Goods { @OneToMany private List<GoodsPicture> goodsPictures;}
@Entitypublic class GoodsPicture { private String path; private Integer index; private String thumbnail;}
我們應當發現這樣的劣勢是什么,從設計的角度來看:我們并不想單獨為 GoodsPicture 單獨建立一張表,正如前面使用 String pictures 來表示 List一樣,這違反了數據庫設計的第一范式,但這對于使用者來說非常方便,這是關系型數據庫的表達能力有限而進行的妥協 。關于這一點我曾和芋艿,曹大師都進行過討論,并達成了一致的結論:數據庫中可以保存 JSON,使用時在應用層進行轉換。
使用 JSON 存儲復雜對象
@Entitypublic class Goods { /** * 圖片 JSON * {@link GoodsPicture} */ @Column(columnDefinition = "text") private String goodsPictures;}
使用 @Convert
上述的 String 使得在數據庫層面少了一張表,使得 Goods 和 GoodsPictures 的關聯更容易維護,但也有缺點:單純的 String goodsPictures 對于使用者來說毫無含義,必須經過應用層的轉換才可以使用。而 JPA 實際上也提供了自定義的轉換器來幫我們自動完成這一轉換工作,這便到了 @Convert 注解派上用場的時候了。
1 聲明 Convert 類
@Entitypublic class Goods { @Convert(converter = PicturesWrapperConverter.class) @Column(columnDefinition = "text") private PicturesWrapper picturesWrapper;}
2 設置轉換類 PicturesWrapperConverter
public class PicturesWrapperConverter implements AttributeConverter<PicturesWrapper, String> { @Override public String convertToDatabaseColumn(PicturesWrapper picturesWrapper) { return JSON.toJSONString(picturesWrapper); } @Override public PicturesWrapper convertToEntityAttribute(String dbData) { return JSON.parseobject(dbData, PicturesWrapper.class); }}
PicturesWrapperConverter 實現了 AttributeConverter接口,它表明了如何將 PicturesWrapper 轉換成 String 類型。這樣的好處是顯而易見的,對于數據庫而言,它知道 String 類型如何保存;對于 Goods 的使用者而言,也只關心 PicturesWrapper 的格式,并不關心它如何持久化。
public class PicturesWrapper { List<GoodsPicture> goodsPictures;}
對于 List 的保存,我暫時只找到了這種方式,借助一個 Wrapper 對象去存儲一個 List 對象。沒有找到直接持久化 List 的方式,如果可以實現這樣的方式,會更好一些:
@Entitypublic class Goods { @Convert(converter = SomeConverter.class) @Column(columnDefinition = "text") List<GoodsPicture> goodsPictures;}
但 converter 無法獲取到 List 的泛型參數 GoodsPicture,在實踐中沒找到方案來解決這一問題,只能退而求其次,使用一個 Wrapper 對象。
與 OneToMany 對比,這樣雖然使得維護變得靈活,但也喪失了查找的功能,我們將之保存成了 JSON 的形式,導致其不能作為查詢條件被檢索。
你可能有兩個疑問:1 在實際項目中,不是不允許對數據進行物理刪除嗎? 2 刪除對象還不簡單,JPA 自己不是有 delete 方法嗎?
關于第一點,需要區分場景,一般實體不允許做物理刪除,而是用標記位做邏輯刪除,也有部分不需要追溯歷史的實體可以做物理刪除,而值對象一般而言是可以做物理刪除的,因為它只是屬性而已。
第二點就有意思了,delete 不就可以直接刪除對象嗎,為什么需要介紹 orphanRemoval 呢?
以活動和禮包這個一對多的關系來舉例。
@Entitypublic class Activity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "activity") private List<GiftPackVo> giftPackVos;}
@Entitypublic class GiftPackVo { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String name; @ManyToOne @JoinColumn(name = "activity_id") private Activity activity;}
這是一個再簡單不過的一對多關系了,唯一可能覺得陌生的便是這個屬性了 orphanRemoval = true 。
如果想要刪除某個活動下的某個禮包,在沒有 orphanRemoval 之前,你只能這么做:
GiftPackVoRepository.delete(GiftPackVo);
但其實這違反了 DDD 中的聚合根模式,GiftPackVo 只是一個值對象,其不具備實體的生命周期,刪除一個禮包其實是一個不準確的做法,應當是刪除某一個活動下的某一個禮包,對禮包的維護,應當由活動來負責。也就是說:應該借由 Activity 刪除 GiftPackVo。使用 orphanRemoval 便可以完成這一操作,它表達這樣的含義:內存中的某個 Activity 對象屬于持久化態,對 List的移除操作,將被直接認為是刪除操作。
于是刪除某個“name = 狗年新春大禮包”的禮包便可以這樣完成:
Activity activity = activityRepository.findOne(1);activity.getGiftPackVos().removeIf(giftPackVo -> "狗年新春大禮包".equals(giftPackVo.getName()));activityRepository.save(activity);
整個代碼中只出現了 activityRepository 這一個倉儲接口。
樂觀鎖一直是保證并發問題的一個有效途徑,spring data jpa 對 @Version 進行了實現,我們給需要做樂觀鎖控制的對象加上一個 @Version 注解即可。
@Entitypublic class Activity { @Version private Integer version;}
我們在日常操作 Activity 對象時完全不需要理會 version 這個字段,當做它不存在即可,spring 借助這個字段來做樂觀鎖控制。每次創建對象時,version 默認值為 0,每次修改時,會檢查對象獲取時和保存時的 version 是否相差 1,轉化為 sql 便是這樣的語句:update activity set xx = xx,yy = yy,version= 10 where id = 1 and version = 9; 然后通過返回影響行數來判斷是否更新成功。
測試樂觀鎖
@Servicepublic class ActivityService { @Autowired ActivityRepos activityRepos; public void test(){ Activity one = activityRepos.findOne(1); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } one.setName("xx"+ new Random().nextInt()); activityRepos.save(one); }}
當 test 方法被并發調用時,可能會存在并發問題??刂婆_打印出了更新信息
2018-02-14 23:44:25.373 INFO 16256 --- [nio-8080-exec-2] jdbc.sqltiming : update activity set name='xx-1863402614', version=1 where id=1 and version=0 2018-02-14 23:44:25.672 INFO 16256 --- [nio-8080-exec-4] jdbc.sqltiming : update activity set name='xx-1095770865', version=1 where id=1 and version=0 org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
表面上看出現的是 StaleStateException,但實際捕獲時,如果你想 catch 該異常,根本沒有效果,通過 debug 信息,可以發現,真正的異常其實是 ObjectOptimisticLockingFailureException(以 MySQL 為例,實際可能和數據庫方言有關,其他數據庫未測試)。
@RequestMapping("/test")public void test(){ try{ activityService.test(); }catch (ObjectOptimisticLockingFailureException oolfe){ System.out.println("捕獲到樂觀鎖并發異常"); oolfe.printStackTrace(); }}
在 Controller 層嘗試捕獲該異常,控制輸出如下:
捕獲到樂觀鎖并發異常org.springframework.orm.ObjectOptimisticLockingFailureException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; nested exception is org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
成功捕獲到了并發沖突,這一切都是 @Version 幫我們完成的,非常方便,不需要我們通過編碼去實現樂觀鎖。
本文簡單聊了幾個個人感觸比較深的 JPA 小技巧,JPA 真的很強大,也很復雜,可能還有不少“隱藏”的特性等待我們挖掘。它不僅僅是一個技術框架,本文的所有內容即使不被使用,也無傷大雅,但在領域驅動設計等軟件設計思想的指導下,它完全可以實踐的更好。
本文由 貴州做網站公司 整理發布,部分圖文來源于互聯網,如有侵權,請聯系我們刪除,謝謝!
網絡推廣與網站優化公司(網絡優化與推廣專家)作為數字營銷領域的核心服務提供方,其價值在于通過技術手段與策略規劃幫助企業提升線上曝光度、用戶轉化率及品牌影響力。這...
在當今數字化時代,公司網站已成為企業展示形象、傳遞信息和開展業務的重要平臺。然而,對于許多公司來說,網站建設的價格是一個關鍵考量因素。本文將圍繞“公司網站建設價...
在當今的數字化時代,企業網站已成為企業展示形象、吸引客戶和開展業務的重要平臺。然而,對于許多中小企業來說,高昂的網站建設費用可能會成為其發展的瓶頸。幸運的是,隨...
QLDownload是什么文件?這是QQ電腦管理器的默認下載文件夾。如果已安裝軟件,則可以刪除qmdownload。刪除步驟如下:1。打開電腦。2. 在“計算機管理”頁上,單擊“搜索”“下載”。3. 單擊“搜索結果”。4. 進入qmdownload管理頁面,選擇文件并右鍵單擊。5. 最后,單擊刪除。qldownload是什么文件夾?這是QQ計算機管理器的默認下載文件夾。您通過QQ計算機管理器下載的...
tvb男演員一覽表tvb男演員:tvb老演員男明星? 1、羅嘉良 羅嘉良(Gallen Lo),1962年12月16日出生于中國香港,祖籍廣東東莞,演員、歌手,三次獲獎TVB萬千星輝頒獎典禮最佳男主角獎是第一個三次獲得該獎項的人TVB藝人 。 2、陶大宇 陶大宇(Michael Tao),1963年8月26日出生于中國香港,是中國香港的影視演員。1983年,從無線電視藝人培訓班畢業后,他加入了無線...
什么是new age?新時代,中國俗稱“新世紀音樂”。與古典音樂和流行音樂相比,新時代音樂只是近年來興起的一種新的音樂形式,但它已迅速發展成為當代音樂的一大流派。它以其豐富的音效、華麗的旋律、雄偉的氣勢、逼真的氛圍征服了眾多的歌迷,尤其是那些熱心的音響愛好者。上世紀60年代末,一些德國音樂家將電子合成器聲音的概念融入到原創聲音表演或即興創作的方式中,這激發了許多新音樂家運用更多元技術探索新領域。這...