摘要:關聯關系的關聯關系定義上,感覺并不是很靈活,姿勢也比較難找。如,定義在關聯關系上的參數可以設置級聯的相關東西。因為序列化會涉及到實體類關聯對象的獲取,會觸發所有的關聯關系。
接(4) - Database 系列.
Java Persistence API,可以理解就是 Java 一個持久化標準或規范,Spring Data JPA 是對它的實現。并且提供多個 JPA 廠商適配,如 Hibernate、Apache 的 OpenJpa、Eclipse的EclipseLink等。
spring-boot-starter-data-jpa 默認使用的是 Hibernate 實現。
直接引入依賴:
org.springframework.boot spring-boot-starter-data-jpa
開啟 SQL 調試:
spring.jpa.database=mysql spring.jpa.show-sql=true
在 SpringBoot + Spring Data Jpa 中,不需要額外的配置什么,只需要編寫實體類(Entity)與數據訪問接口(Repository)就能開箱即用,Spring Data JPA 能基于接口中的方法規范命名自動的幫你生成實現(根據方法命名生成實現,是不是很牛逼?)
Spring Data JPA 還默認提供了幾個常用的Repository接口:
Repository: 僅僅是一個標識,沒有任何方法,方便 Spring 自動掃描識別
CrudRepository: 繼承 Repository,實現了一組 CRUD 相關的方法
PagingAndSortingRepository: 繼承 CrudRepository,實現了一組分頁排序相關的方法
JpaRepository: 繼承 PagingAndSortingRepository,實現一組JPA規范相關的方法
推薦教程:Spring Data JPA實戰入門訓練 https://course.tianmaying.com...
Entity 實體和 Respository 接口根據 user 表結構,我們定義好 User 實體類與 UserRespository 接口類。
這里,還自定義了一個 @Query 接口,為了體驗下自定義查詢。因為使用了 lombok,所以實體類看起來很干凈。
User.java
@Data @Entity public class User { @Id @GeneratedValue private Long id; @Column(nullable = false) private String name; @Column(nullable = false, unique = true, updatable = false) @JsonProperty(value = "email") private String username; @Column(nullable = false) @JsonIgnore private String password; @Column(nullable = false) @JsonIgnore private String salt; @Column(nullable = true) private Date birthday; @Column(nullable = false) private String sex; @Column(nullable = true) @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss") private Timestamp access; @Column(nullable = true) @JsonFormat(pattern="HH:mm:ss") private Time accessTime; @Column(nullable = false) private Integer state; @Column(nullable = false, insertable = false, updatable = false) @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss") private Timestamp created; @Column(nullable = false, insertable = false, updatable = false) @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss") private Timestamp updated; }
@Data 是 lombok 的注解,自動生成Getter,Setter,toString,構造函數等
@Entity 注解這是個實體類
@Table 注解表相關,如別名等
@Id 注解主鍵,@GeneratedValue 表示自動生成
@DynamicUpdate,@DynamicInsert 注解可以動態的生成insert、update 語句,默認會生成全部的update
@Column 標識一些字段特性,字段別名,是否允許為空,是否唯一,是否進行插入和更新(比如由MySQL自動維護)
@Transient 標識該字段并非數據庫字段映射
@JsonProperty 定義 Spring JSON 別名,@JsonIgnore 定義 JSON 時忽略該字段,@JsonFormat 定義 JSON 時進行格式化操作
UserRepository.java
public interface UserRepository extends JpaRepository, UserCustomRepository { User findByUsername(String username); @Transactional @Modifying @Query("UPDATE User SET state = ?2 WHERE id = ?1 ") Integer saveState(Long id, Integer state); }
@Transactional 用來標識事務,一般修改、刪除會用到, @Modifying 標識這是個修改、刪除的Query
@Param 標注在參數上,可用于標識參數式綁定(不使用 ?1 而使用 :param)
好了,接下來我們就可以進行單表的增、刪、改、查分頁排序操作了:
@Autowired private UserRepository userRepository; User user = new User(); userRepository.save(user); // 插入或保存 userRepository.saveFlush(user); // 保存并刷新 userRepository.exists(1) // 主鍵查詢是否存在 userRepository.findOne(1); // 主鍵查詢單條 userRepository.delete(1); // 主鍵刪除 userRepository.findByUsername("a@b.com"); // 查詢單條 userRepository.findAll(pageable); // 帶排序和分頁的查詢列表 userRepository.saveState(1, 0); // 更新單個字段
通常,exist(),delete()之類的方法,我們可能直接會操作 UserRepository,但是一般情況下,在 UserRepository 上面還會提供一個 UserService 來進行一系列的操作(比如數據校驗,邏輯判斷之類)
分頁和排序PagingAndSortingRepository 和 JpaRepository 接口都具有分頁和排序的功能。因為后者繼承自前者。比如下面這個方法:
PagefindAll(Pageable var1);
Pageable 是Spring Data庫中定義的一個接口,該接口是所有分頁相關信息的一個抽象,通過該接口,我們可以得到和分頁相關所有信息(例如pageNumber、pageSize等),這樣,Jpa就能夠通過pageable參數來組裝一個帶分頁信息的SQL語句。
Page 類也是Spring Data提供的一個接口,該接口表示一部分數據的集合以及其相關的下一部分數據、數據總數等相關信息,通過該接口,我們可以得到數據的總體信息(數據總數、總頁數...)以及當前數據的信息(當前數據的集合、當前頁數等)
Pageable只是一個抽象的接口。可以通過兩種途徑生成 Pageable 對象:
通過參數,自己接收參數,自己構造生成 Pageable 對象
@RequestMapping(value = "", method = RequestMethod.GET) public Object page(@RequestParam(name = "page", required = false) Integer page, @RequestParam(name="size", required = false) Integer size) { Sort sort = new Sort(Sort.Direction.DESC, "id"); Pageable pageable = new PageRequest(page, size, sort); Pageusers = userRepository.findAll(pageable); return this.responseData(users); }
這種方式你可以靈活的定義傳參。
通過 @PageableDefault 注解,會把參數自動注入成 Pageable 對象,默認是三個參數值:
page=,第幾頁,從0開始,默認為第0頁
size=,每一頁的大小
sort=,排序相關的信息,例如sort=firstname&sort=lastname,desc
@RequestMapping(value = "/search", method = RequestMethod.GET) public Object search(@PageableDefault(size = 3, sort = "id", direction = Sort.Direction.DESC) Pageable pageable) { Pageusers = userRepository.findAll(pageable); return this.responseData(users); }
看起來,這種方式更優雅一些。
關聯關系Spring Data JPA 的關聯關系定義上,感覺并不是很靈活,姿勢也比較難找。
視頻教程:http://www.jikexueyuan.com/co...
OneToOne 一對一一對一的關系,拿 user,user_detail 來說,一般應用起來,有以下幾種情況:
主鍵直接關聯:user(id, xx);user_detail(id, xx) 或 user(id, xx),user_detail(user_id, xx) 其中 id, userid 為主鍵
主表含外鍵的關聯:user(id, role_id, xx);role(id, xx) 。 其中 id 為自增主鍵
附表含外鍵的關聯:user(id, xx);user_detail(id, user_id, xx) 。其中 id 為自增主鍵
主表含外鍵的關聯:用戶->角色是一對一,而角色->用戶是多對一,而大部分情況,我們是通過 user 表來查詢某個角色的列表,而通過 role 來查詢某個角色的列表可能性很小。
附表表含外鍵的關聯:其實和主表含外鍵的關聯完全相反,關聯的定義也是相反的。
主鍵ID關聯單向關聯,直接在 User 上定義 @OneToOne 與 @PrimaryKeyJoinColumn 即可完成
@Entity @Data public class User { ... @OneToOne @PrimaryKeyJoinColumn private UserDetail detail; ... } // 獲取的user,會包含detail屬性 User user = userRepository.findOne(userId);
雙向關聯,除了要定義 User 的 @OneToOne,還需要定義 UserDetail 的 @OneToOne,用 mappedBy 指示 User 表的屬性名。
@Entity @Data public class UserDetail { ... @OneToOne(mappedBy = "detail") private User user; ... }
出問題了,雙向關聯,涉及到一個循環引用無限遞歸的問題,這個問題會發生在 toString、 JSON 轉換上。可能這只是個基礎問題,但對于我這個入門漢,抓瞎了好長時間。
解決辦法:
分別給User、UserDetail的關聯屬性加上:@JsonManagedReference、@JsonBackReference注解,解決 JSON 問題
給 UserDetail 實體類加上 @ToString(exclude = "user") 注解,解決 toString 的問題。
所以 UserDetail 最終造型應該是這樣的:
@Entity @Data @ToString(exclude = "user") public class UserDetail { ... @OneToOne(mappedBy = "detail") @JsonBackReference private User user; } // 現在可以進行雙向查詢了 User user1 = userRepository.findOne(userId); userDetail userdetail = userDetailRepository.findOne(userId); User user2 = userdetail.getUser();
@PrimaryKeyJoinColumn 注解主要用于主鍵關聯,注意實體屬性需要使用 @Id 的為主鍵,假如現在是:user(id, xx),user_detail(user_id, xx) 這種情況。則需要在 User 類上自定義它的屬性:
// User @OneToOne @PrimaryKeyJoinColumn(referencedColumnName = "user_id") @JsonManagedReference private UserDetail detail;主表含外鍵
使用 @JoinColumn 注解即可完成,默認使用的外鍵是(屬性名+下劃線+id)。關聯附表的主鍵 id。
可以通過 name=,referencedColumnName= 屬性重新自定義。
@Entity @Data public class User { ... // 屬性名為role,所以 @JoinColumn 會默認外鍵是 role_id @OneToOne @JoinColumn @JsonManagedReference private Role role; ... }
對于 user->role 的表關聯需求,我們不需要定義 OneToOne 反向關系,并且 role->user 本來是個一對多關系。
附表含外鍵這種情況一般也會經常出現,它可以保證每個表都有一個自增主鍵的id
因為外鍵在附表上,所以需要反過來,在 User 上定義 mapped。
如果是雙向關聯,同樣需要加上忽略 toString(),JSON 的注解
@Entity @Data public class User { ... @OneToOne(mappedBy = "user") @JsonManagedReference private UserDetail detail; ... } @Entity @Data @ToString(exclude = "user") public class UserDetail { ... @OneToOne @JoinColumn @JsonBackReference private User user; ... } User user1 = userRepository.findOne(userId); // 給 UserDetail 定義一個獨立的 findByUserId 接口,這樣可以通過操作 UserDetail 反向獲取到 user 的數據 userDetail userdetail = userDetailRepository.findByUserId(userId); User user2 = userdetail.getUser();
實際上,在上面的例子里面,考慮實際的場景,幾乎不需要定義 OneToOne 的反向關聯(偽需求),這樣就不用解決循環引用的問題了。這里只是意淫,不是嗎?
現在有個問題出現了,這種情況下(附表含外鍵),我如何定義 User->UserDetail 的單向關系呢?
關聯關系:一對多接著上面的例子,Role -> User 實際上是個一對多的關系。但我們一般不會這么做。直接通過 User 就可以查詢嘛。所以這里演示另一個例子。
User->Order 是一對多,Order->User 是多對一,定義 Order 實體,注意@Table 注解,因為 order 是 MySQL 關鍵詞(此處中槍)
@Entity @Data @Table(name = "`order`") public class Order { @Id @GeneratedValue private Long id; @Column(nullable = false) private String name; }
然后在 User 中定義 @OneToMany,因為是一對多,所以返回的是List
@OneToMany(fetch = FetchType.LAZY) @JoinColumn(name="user_id") private Listorders;
測試一下:
User user = userService.findOne(userId); if (user != null) { // LAZY 的緣故,在 getOrders 才會觸發獲取操作 Listorders = user.getOrders(); return this.responseData(orders); }
再看看反向關聯,也就是 @ManyToOne,稍作調整
User 實體類 @OneToMany(fetch = FetchType.LAZY, mappedBy = "user") private Listorders; Order 實體類 @ManyToOne private User user;
再測試一下:
Order order = orderRepository.findOne(orderId); if (order != null) { User user = order.getUser(); return this.responseData(user); }總結
想想實際場景,我們不太需要定義 User->Order 這種關聯,因為用戶可能有很多訂單,這個量是無可預測的。這時候這種關聯查詢,不能分頁,沒有意義(也可能是我姿勢不對)。
如果是有限的 XToMany 關聯,是有意義的。比如配置管理。一個應用擁有有限的多項配置?
Order->User 這種關聯是有意義的。拿到一個 order_id 去反查用戶信息。
關聯關系:多對多Order <-> Product 是多對多的關系,關聯表是 order_product,
Order 實體配置 @ManyToMany 屬性,不需要定義 OrderProduct 實體類,
// @JoinTable 實際可以省略,因為使用的是默認配置 @ManyToMany(fetch = FetchType.LAZY) @JoinTable( name = "order_product", joinColumns = @JoinColumn(name = "order_id"), inverseJoinColumns = @JoinColumn(name = "product_id")) @JsonManagedReference private Listproducts;
這樣就定義了單向關聯,雙向關聯類似在 Product 實體配置:
@ManyToMany(mappedBy = "products", fetch = FetchType.LAZY) @JsonIgnore private Listorders;
好了,這樣就OK了,實際按照上面的解釋,Product -> Order 是不太有意義的。
屬性參數@OneToOne的屬性:
cascade 屬性表示級聯操作策略,有 CascadeType.ALL 等值。
fetch 屬性表示實體的加載方式,有 FetchType.LAZY 和 FetchType.EAGER 兩種取值,默認值為 EAGER
拿OneToOne來說,如果是 EAGER 方式,那么會產生一個連接查詢,如果是 LAZY 方式,則是兩個查詢。并且第二個查詢會在用的時候才會觸發(僅僅.getXXX是不夠的)。
級聯在未定義級聯的情況下,我們通常需要手動插入。
如 user(id, xx),user_detail(id, user_id, xx)
User user = new User(); userRepository.save(user); UserDetail userDetail = new UserDetail(); userDetail.setUserId(user.getId()); userDetailRepository.save(userDetail);
定義在關聯關系上的 cascade 參數可以設置級聯的相關東西。
經過一番研究,這部分暫時我還沒搞明白正確姿勢,玩不轉。
復雜的查詢 問題總結 數據庫默認值字段,插入后不會自動返回默認值。Entity not return default value after insert一般關鍵表會記錄創建、更新時間,滿足基本審計需求,以前我喜歡使用 MySQL 默認值特性,這樣應用層就可以不用管他們了,如:
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
在實體中,我們要忽略插入和更新對他們的操作。
@Column(nullable = false, insertable = false, updatable = false) @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss") private Timestamp created; @Column(nullable = false, insertable = false, updatable = false) @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss") private Timestamp updated;
看起來不錯哦,工作正常,但是:
Spring Data Jpa 在 save() 完成以后,對于這種數據庫默認插入的值,拿不到回寫的數據啊,無論我嘗試網上的方法使用 saveAndFlush() 還是手動 flush() 都是扯淡。
這個坑,我踩了好久,到現在,依然不知道這種情況怎么解決。
臨時解決方案:
拋棄數據庫默認值特性,在實體類借助 @PrePersist、@PreUpdate 手動實現,如果有多個表,遵循同一規范,可以搞個基類,雖然不太爽,但是能正常工作。
@MappedSuperclass @Getter @Setter public class BaseEntity { @Column(nullable = false, updatable = false) @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss") private Timestamp created; @Column(nullable = false) @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss") private Timestamp updated; @PrePersist public void basePrePersist() { long timestamp = new java.util.Date().getTime(); created = new Timestamp(timestamp); updated = new Timestamp(timestamp); } @PreUpdate public void basePreUpdate() { updated = new Timestamp(new java.util.Date().getTime()); } }OneToOne 關聯關系,指定 FetchType.LAZY,JSON 時會出錯,得不到數據
原因大概是,JSON序列化的時候,數據還沒有fetch到,出錯信息如下:
Could not write JSON: No serializer found for class org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer and no properties discovered to create BeanSerializer
解決方法:
application.properties 增加配置項:
spring.jackson.serialization.fail-on-empty-beans=false
然而你會發現最終的 JSON 多出來兩個key,分別是handler、hibernateLazyInitializer
所以還需要在實體類上增加注解:
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
搞定!
關于循環引用和 Lazy Fetch接上個問題,這里要思考一個問題場景:
Lazy Fetch 在 JSON 序列化實體類時失效
我們定義了一個 User 實體類,這個實體類有幾個關聯,比如 OneToOne和 OneToMany,并且設置了Lazy,我們執行了 findOne 查詢并返回結果,在 RestController 的時候,會默認執行 Jackson 的序列化JSON 操作。
因為序列化會涉及到實體類關聯對象的獲取,會觸發所有的關聯關系。生成一大堆的查詢 SQL, 這樣 LAZY 就失去意義了啊,比如我只想要 User 單表的基本信息怎么辦?
stackoverflow 可以搜到了好多類似問題,我目前還沒找到正確的姿勢。
可以想象的是,不應當將實體類直接返回給客戶端,應該再定義一個返回數據的DTO,將實體類的數據復制到DTO,然后返回并JSON。然而這樣好蛋疼,隨便一個項目你至少需要定義實體類,輸入參數的DTO,輸出參數的DTO。
問題暫放這里。
循環引用
我們雖然通過 @JsonBackReference 和 JsonManagedReference 來解決。但是有時候,對于兩個 OneToOne 實體,我們都需要 JSON 序列化怎么辦?如 User 與 UserDetail
另一個辦法,給實體類加上 @JsonIdentityInfo:
@JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
這樣好處是顯而易見的,只需要在實體類上注解一下即可。
猜測原理是引入了id,檢測了主鍵是否一致,決定是否引用下去。如 User->UserDetail->User。
所以他還會多一次查詢,并且關聯數據上會多一個關聯關系的 id 的字段。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/67271.html
摘要:開始介紹簡化了基于的應用開發,你只需要就能創建一個獨立的,產品級別的應用。該包含很多搭建,快速運行項目所需的依賴,并提供一致的,可管理傳遞性的依賴集。日志級別通過標識開啟控制臺級別日志記錄,也可以在中指定日志級別配置示例 開始 介紹 Spring Boot 簡化了基于 Spring 的應用開發,你只需要 run 就能創建一個獨立的,產品級別的 Spring 應用。 Spring 平臺...
摘要:本文只是引子,后續更新到獨立章節。尤其是,這也是現在號稱流行的組合。幸虧現在看起來不主流了。增刪改查多條件組合查詢分頁,排序等多表關聯。而每個類寫上構造函數,,實在是蛋疼。 本文只是引子,后續更新到獨立章節。 環境:Spring Boot 1.5.4 到了操作數據庫的環節,以 MySQL 為基準,體驗一下數據庫的相關操作,先讓我糾結一下,至少有以下四種姿勢。 JDBC。原生的 JD...
摘要:與的關系是什么是官方提出的持久化規范。它為開發人員提供了一種對象關聯映射工具來管理應用中的關系數據。他的出現主要是為了簡化現有的持久化開發工作和整合技術,結束現在,,等框架各自為營的局面。定義了在對數據庫中的對象處理查詢和事務運行時的的。 導讀: 在上篇文章中對Spring MVC常用的一些注解做了簡要的說明,在這篇文章中主要對Spring Data JPA 做一個簡要的說明,并附有一...
摘要:初次使用的人往往會困惑,不知道該使用哪種方法。目前來說,團隊推薦使用基于的方法來提供更高的靈活性。配置,從而在應用啟動時執行腳本來初始化數據庫。目前為止我們沒有任何消息需要配置,所以只在文件夾中創建一個空的文件。將配置為,它包含的上下文。 前言 spring是一個用于創建web和企業應用的一個很流行的框架。和別的只關注于一點的框架不同,Spring框架通過投資并組合項目提供了大量的功能...
摘要:下一代服務端開發下一代服務端開發第部門快速開始第章快速開始環境準備,,快速上手實現一個第章企業級服務開發從到語言的缺點發展歷程的缺點為什么是產生的背景解決了哪些問題為什么是的發展歷程容器的配置地獄是什么從到下一代企業級服務開發在移動開發領域 《 Kotlin + Spring Boot : 下一代 Java 服務端開發 》 Kotlin + Spring Boot : 下一代 Java...
閱讀 3512·2021-11-17 17:01
閱讀 3918·2021-11-08 13:12
閱讀 2477·2021-10-08 10:04
閱讀 686·2021-09-29 09:35
閱讀 1418·2021-09-26 10:12
閱讀 2020·2021-09-07 09:58
閱讀 1953·2019-08-30 15:55
閱讀 2134·2019-08-30 13:14