Spring Boot QuickStart (5) - Spring Data JPA

接(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...

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<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 對象:

  1. 經過參數,本身接收參數,本身構造生成 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);
}

這種方式你能夠靈活的定義傳參。

  1. 經過 @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...

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 轉換上。可能這只是個基礎問題,但對於我這個入門漢,抓瞎了好長時間。

解決辦法:

  1. 分別給User、UserDetail的關聯屬性加上:@JsonManagedReference、@JsonBackReference註解,解決 JSON 問題

  2. 給 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 參數能夠設置級聯的相關東西。

通過一番研究,這部分暫時我還沒搞明白正確姿式,玩不轉。

複雜的查詢

問題總結

數據庫默認值字段,插入後不會自動返回默認值。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

接上個問題,這裏要思考一個問題場景:

  1. Lazy Fetch 在 JSON 序列化實體類時失效

咱們定義了一個 User 實體類,這個實體類有幾個關聯,好比 OneToOne和 OneToMany,而且設置了Lazy,咱們執行了 findOne 查詢並返回結果,在 RestController 的時候,會默認執行 Jackson 的序列化JSON 操做。

由於序列化會涉及到實體類關聯對象的獲取,會觸發全部的關聯關係。生成一大堆的查詢 SQL, 這樣 LAZY 就失去意義了啊,好比我只想要 User 單表的基本信息怎麼辦?

stackoverflow 能夠搜到了好多相似問題,我目前還沒找到正確的姿式。

能夠想象的是,不該當將實體類直接返回給客戶端,應該再定義一個返回數據的DTO,將實體類的數據複製到DTO,而後返回並JSON。然而這樣好蛋疼,隨便一個項目你至少須要定義實體類,輸入參數的DTO,輸出參數的DTO。

問題暫放這裏。

  1. 循環引用

咱們雖然經過 @JsonBackReference 和 JsonManagedReference 來解決。可是有時候,對於兩個 OneToOne 實體,咱們都須要 JSON 序列化怎麼辦?如 User 與 UserDetail

另外一個辦法,給實體類加上 @JsonIdentityInfo:

@JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")

這樣好處是顯而易見的,只須要在實體類上註解一下便可。

猜想原理是引入了id,檢測了主鍵是否一致,決定是否引用下去。如 User->UserDetail->User。

因此他還會多一次查詢,而且關聯數據上會多一個關聯關係的 id 的字段。

相關文章
相關標籤/搜索