接(4) - Database 系列.html
Java Persistence API,能夠理解就是 Java 一個持久化標準或規範,Spring Data JPA 是對它的實現。而且提供多個 JPA 廠商適配,如 Hibernate、Apache 的 OpenJpa、Eclipse的EclipseLink等。java
spring-boot-starter-data-jpa 默認使用的是 Hibernate 實現。mysql
<!-- more -->spring
直接引入依賴:sql
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>
開啓 SQL 調試:數據庫
spring.jpa.database=mysql spring.jpa.show-sql=true
在 SpringBoot + Spring Data Jpa 中,不須要額外的配置什麼,只須要編寫實體類(Entity)與數據訪問接口(Repository)就能開箱即用,Spring Data JPA 能基於接口中的方法規範命名自動的幫你生成實現(根據方法命名生成實現,是否是很牛逼?)app
Spring Data JPA 還默認提供了幾個經常使用的Repository接口:函數
Repository: 僅僅是一個標識,沒有任何方法,方便 Spring 自動掃描識別spring-boot
CrudRepository: 繼承 Repository,實現了一組 CRUD 相關的方法測試
PagingAndSortingRepository: 繼承 CrudRepository,實現了一組分頁排序相關的方法
JpaRepository: 繼承 PagingAndSortingRepository,實現一組JPA規範相關的方法
推薦教程:Spring Data JPA實戰入門訓練 https://course.tianmaying.com...
根據 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<User, Long>, 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 接口都具備分頁和排序的功能。由於後者繼承自前者。好比下面這個方法:
Page<T> findAll(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); Page<User> users = 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) { Page<User> users = userRepository.findAll(pageable); return this.responseData(users); }
看起來,這種方式更優雅一些。
Spring Data JPA 的關聯關係定義上,感受並非很靈活,姿式也比較難找。
視頻教程:http://www.jikexueyuan.com/co...
一對一的關係,拿 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 來查詢某個角色的列表可能性很小。
附表表含外鍵的關聯:其實和主表含外鍵的關聯徹底相反,關聯的定義也是相反的。
單向關聯,直接在 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<Order>,而且通常設置爲 LAZY
@OneToMany(fetch = FetchType.LAZY) @JoinColumn(name="user_id") private List<Order> orders;
測試一下:
User user = userService.findOne(userId); if (user != null) { // LAZY 的緣故,在 getOrders 纔會觸發獲取操做 List<Order> orders = user.getOrders(); return this.responseData(orders); }
再看看反向關聯,也就是 @ManyToOne,稍做調整
User 實體類 @OneToMany(fetch = FetchType.LAZY, mappedBy = "user") private List<Order> orders; 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 List<Product> products;
這樣就定義了單向關聯,雙向關聯相似在 Product 實體配置:
@ManyToMany(mappedBy = "products", fetch = FetchType.LAZY) @JsonIgnore private List<Order> orders;
好了,這樣就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 參數能夠設置級聯的相關東西。
通過一番研究,這部分暫時我還沒搞明白正確姿式,玩不轉。
通常關鍵表會記錄建立、更新時間,知足基本審計需求,之前我喜歡使用 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()); } }
緣由大概是,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 在 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 的字段。