爲了解決這種情況(當然也不可能完全解決啦),我們會借用一些ORM框架來減少我們的工作負擔。本章我們來學習如何在Spring Boot中集成JPA框架來訪問數據庫。
JPA全稱Java Persistence API.JPA通過JDK 5.0註解或XML描述對象-關係表的映射關係,並將運行期的實體對象持久化到數據庫中。
JPA的查詢語言是面向對象而非面向數據庫的,它以面向對象的自然語法構造查詢語句,可以看成是Hibernate HQL的等價物。JPA定義了獨特的JPQL(Java Persistence Query Language),JPQL是EJB QL的一種擴展,它是針對實體的一種查詢語言,操作對象是實體,而不是關係數據庫的表,而且能夠支持批量更新和修改、JOIN、GROUP BY、HAVING 等通常只有 SQL 才能夠提供的高級查詢特性,甚至還能夠支持子查詢。
爲了抽象出不同對象的“增刪改查”操作,通常我們會寫一個模板Dao來簡化我們的開發,而Spring-data-jpa可以更加減輕我們的開發工作量,使得數據訪問層變成只是一層接口的編寫方式,如下所示:
package com.bluecoffee.repository;
import com.bluecoffee.domain.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
/**
* Created by qianlong on 16/9/27.
*/
public interface BookDao extends JpaRepository<Book,Long> {
Book findByTitle(String title);
Book findByTitleAndAuthor(String title,String author);
@Query("from Book b where b.title=:title")
Book findBook(@Param("title") String bookTitle);
}
我們只需要通過編寫一個繼承自JpaRepository的接口就能完成數據訪問,下面以一個具體實例來體驗Spring-data-jpa給我們帶來的強大功能。
使用示例
Spring-data-jpa依賴於Hibernate。如果您已經對Hibernate有一定了解,那你可以毫不費力的看懂並上手使用Spring-data-jpa。如果您還是Hibernate新手,您可以先按如下方式入門,再建議回頭學習一下Hibernate以幫助這部分的理解和進一步使用。
引入JPA依賴
pom.xml中添加JPA和MySQL數據庫驅動的依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.39</version>
</dependency>
配置數據庫連接信息
spring.datasource.url=jdbc:mysql://localhost:3306/spring_boot
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#spring.jpa.properties.hibernate.hbm2ddl.auto=create
#spring.jpa.properties.hibernate.hbm2ddl.auto=create-drop
spring.jpa.properties.hibernate.hbm2ddl.auto=update
#spring.jpa.properties.hibernate.hbm2ddl.auto=validate
spring.jpa.properties.hibernate.hbm2ddl.auto是hibernate的配置屬性,其主要作用是:自動創建、更新、驗證數據庫表結構。該參數的幾種配置如下:
- create:每次加載hibernate時都會刪除上一次的生成的表,然後根據你的model類再重新來生成新表,哪怕兩次沒有任何改變也要這樣執行,這就是導致數據庫表數據丟失的一個重要原因。
- create-drop:每次加載hibernate時根據model類生成表,但是sessionFactory一關閉,表就自動刪除。
- update:最常用的屬性,第一次加載hibernate時根據model類會自動建立起表的結構(前提是先建立好數據庫),以後加載hibernate時根據model類自動更新表結構,即使表結構改變了但表中的行仍然存在不會刪除以前的行。要注意的是當部署到服務器後,表結構是不會被馬上建立起來的,是要等應用第一次運行起來後纔會。
- validate:每次加載hibernate時,驗證創建數據庫表結構,只會和數據庫中的表進行比較,不會創建新表,但是會插入新值。
創建實體
package com.bluecoffee.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.util.Date;
/**
* Created by qianlong on 16/10/2.
*/
@Entity
public class Book {
@Id
@GeneratedValue
private Long bookId;
@Column(nullable = false)
private String title;
@Column(nullable = true)
private String author;
@Column(name = "create_time" ,nullable = true)
private Date createTime;
public Book(){}
public Book(String title,String author,Date createTime){
this.title = title;
this.author = author;
this.createTime = createTime;
}
public Long getBookId() {
return bookId;
}
public void setBookId(Long bookId) {
this.bookId = bookId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
}
在上述代碼中,註解@GeneratedValue表示bookId我們設置爲主鍵從1開始自增,但是通常我們在實際業務開發過程中,主鍵需要從自定義序列中獲取,我們會建一張表,然後在表中維護不同表主鍵的自增序列,做法如下:
新建序列號生成表
DROP TABLE IF EXISTS `sequence_generator`;
CREATE TABLE `sequence_generator` (
`id` decimal(10,0) NOT NULL,
`sequence_name` varchar(255) NOT NULL,
`sequence_value` decimal(10,0) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
對book_store表新建自增序列
INSERT INTO `sequence_generator` VALUES ('1', 'book_store_pk', '10000000');
修改實體類中主鍵生成規則
package com.bluecoffee.domain;
import javax.persistence.*;
/**
* Created by qianlong on 16/10/2.
*/
@Entity
@Table(name="book_store")
public class BookStore {
@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator="pk_gen")
@TableGenerator(name = "pk_gen",
table="sequence_generator",
pkColumnName="sequence_name",
valueColumnName="sequence_value",
pkColumnValue="book_store_pk",
allocationSize=1
)
private Long bookId;
@Column(nullable = true)
private String address;
@Column(nullable = true)
private String storeManager;
public BookStore(){};
public BookStore(String address,String storeManager){
this.address = address;
this.storeManager = storeManager;
}
public Long getBookId() {
return bookId;
}
public void setBookId(Long bookId) {
this.bookId = bookId;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getStoreManager() {
return storeManager;
}
public void setStoreManager(String storeManager) {
this.storeManager = storeManager;
}
}
@GeneratedValue(strategy = GenerationType.TABLE, generator="pk_gen") 代表主鍵生成策略是從表中獲取序列,序列號生成器名稱爲“pk_gen”
@TableGenerator的定義(Java代碼):
@Target({TYPE, METHOD, FIELD})
@Retention(RUNTIME)
public @interface TableGenerator {
String name();
String table() default "";
String catalog() default "";
String schema() default "";
String pkColumnName() default "";
String valueColumnName() default "";
String pkColumnValue() default "";
int initialValue() default 0;
int allocationSize() default 50;
UniqueConstraint[] uniqueConstraints() default {};
}
name:序列號生成器名稱,與@GeneratedValue
中的generator
一致
table:表生成策略所持久化的表名,例如,這裏表使用的是數據庫中的“sequence_generator”。
pkColumnName:表示在持久化表中,該主鍵生成策略所對應鍵值的名稱。例如在“sequence_generator”中將“sequence_name”作爲主鍵的鍵值
valueColumnName:表示在持久化表中,該主鍵當前所生成的值,它的值將會隨着每次創建累加。例如,在“sequence_generator”中將“sequence_value”作爲主鍵的值
pkColumnValue:表示在持久化表中,該生成策略所對應的主鍵。例如在“sequence_generator”表中,將“sequence_name”的值爲“book_store_pk”。
allocationSize:表示每次主鍵值增加的大小,例如設置成1,則表示每次創建新記錄後自動加1,默認爲50。
initialValue:表示主鍵初識值,默認爲0。
UniqueConstraint:與@Table標記中的用法類似。
創建數據訪問dao
package com.bluecoffee.repository;
import com.bluecoffee.domain.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
/**
* Created by qianlong on 16/9/27.
*/
public interface BookDao extends JpaRepository<Book,Long> {
Book findByTitle(String title);
Book findByTitleAndAuthor(String title,String author);
@Query("from Book b where b.title=:title")
Book findBook(@Param("title") String bookTitle);
}
BookDao繼承了==JpaRepository==,我們通過查看JpaRepository的API接口文檔可以看到該接口已經實現了創建(save)、更新(save)、刪除(delete)、查詢(findAll、findOne)等基本操作的函數,因此對於這些基礎操作的數據訪問就不需要我們再開發了。
Book findByTitle(String title);
Book findByTitleAndAuthor(String title,String author);
findByTitle和findByTitleAndAuthor方法實現了根據書名和作者查詢的方法,可以看到我們並沒有編寫任何SQL就完成了查詢功能,這就是spring-data-jpa的一大特色:通過解析方法名創建查詢。 但是我們需要注意,查詢訪問名稱必須遵循JPA的規範,查詢方法名稱必須以find開頭,否則就需要自己通過@Query 註解來創建查詢,需要編寫JPQL語句,並通過類似“:title”來映射@Param指定的參數,如下所示:
@Query("from Book b where b.title=:title")
Book findBook(@Param("title") String bookTitle);
分頁查詢
@Query("select book from Book book where book.author = :author")
Page findBookPage(Pageable pageable,@Param("author") String author);
單元測試
最後我們來通過編寫單元測試來驗證,如下代碼所示:
package com.bluecoffee;
import com.bluecoffee.Repository.BookDao;
import com.bluecoffee.Repository.BookStoeDao;
import com.bluecoffee.domain.Book;
import com.bluecoffee.domain.BookStore;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.Date;
import java.util.Iterator;
/**
* Created by qianlong on 16/9/27.
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
public class BookDaoTest {
@Autowired
private BookDao bookDao;
@Autowired
private BookStoeDao bookStoeDao;
@Test
public void testBook(){
try{
//先清空已有數據
bookDao.deleteAll();
//生成10本書
for(int i=1;i<=10;i++){
bookDao.save(new Book("book"+i,"author"+i,new Date()));
}
// 測試findAll, 查詢所有記錄
Assert.assertEquals(10, bookDao.findAll().size());
// 測試findByTitle, 查詢書名爲book5的書
Assert.assertEquals("author5", bookDao.findByTitle("book5").getAuthor());
// 測試findBook, 查詢書名爲book7的書
Assert.assertEquals("author7", bookDao.findBook("book7").getAuthor());
// 測試findByTitleAndAuthor, 查詢書名爲book1,作者爲author1的書
Assert.assertEquals("author1", bookDao.findByTitleAndAuthor("book1","author1").getAuthor());
// 測試findByTitleAndAuthor, 查詢書名爲book2,作者爲author1的書
Assert.assertEquals(null, bookDao.findByTitleAndAuthor("book2", "author1"));
//測試刪除book3
bookDao.delete(bookDao.findBook("book3"));
//測試刪除是否成功
Assert.assertEquals(9, bookDao.findAll().size());
//分頁查詢
bookDao.deleteAll();
for(int i=1;i<=10;i++){
bookDao.save(new Book("book"+i,"Alex Qian",new Date()));
}
Sort sort = new Sort(Sort.Direction.DESC, "bookId");
int page = 1;
int size = 5;
Pageable pageable = new PageRequest(page, size, sort);
Page<Book> pages = bookDao.findBookPage(pageable,"Alex Qian");
Iterator<Book> it= pages.iterator();
Assert.assertEquals(size,pages.getSize());
Assert.assertEquals(2,pages.getTotalPages());
while(it.hasNext()){
Book book = (Book)it.next();
System.out.println("title/author/createTime:"+book.getTitle()+"/"+book.getAuthor()+"/"+book.getCreateTime());
}
}catch (Exception ex){
Assert.fail(ex.getMessage());
}
}
@Test
public void testBookStore(){
try{
bookStoeDao.deleteAll();
//生成5個書店
for(int i=1;i<=5;i++){
bookStoeDao.save(new BookStore("address_"+i,"manager_"+i));
}
// 測試findAll, 查詢所有記錄
Assert.assertEquals(5, bookStoeDao.findAll().size());
Assert.assertEquals(1, bookStoeDao.getBookStoreByManager("manager_2").size());
Assert.assertEquals("manager_2", bookStoeDao.getBookStoreByManager("manager_2").get(0).getStoreManager());
}catch (Exception ex){
Assert.fail(ex.getMessage());
}
}
}