前言
spring data JPA是spring團隊打造的sping生態全家桶的一部分,本身內核使用的是hibernate核心源碼,用來作爲了解java持久層框架基本構成的樣本是再好不過的選擇。最近閒來無事,構建了一個demo工程,用來閱讀spring data JPA源碼,這對於宏觀瞭解持久層框架的基本工作、微觀分析spring data JPA的原理和優缺點、避免使用過程中採坑,將會有一定的幫助。
基本工程搭建
spring data JPA的使用需要依託於web框架,最簡單快速的方式就是使用https://start.spring.io/構建一個包含spring data JPA的spring boot項目,只需要在引導的對應SQL的菜單中選中Spring Data Jpa和響應數據庫的Driver即可,目前無論是idae還是eclipse都支持採用這種方式。上述方法的實質還是在最終生成的spring boot項目的pom.xml中加入了相關的依賴,所以也可以直接生成由maven管理的spring boot項目,然後pom.xml中:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>jpademo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>jpademo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/apt</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
其中,spring-boot-starter-web是spring boot框架依賴,spring-boot-starter-data-jpa是Spring Data Jpa依賴,我使用的Mysql數據庫,所以引入了mysql-connector-java。 querydsl-jpa和querydsl-apt是在JPA基礎上使用querydsl的依賴,可以不引入。最後lombok廣泛使用的懶癌工具包。
接下來當然是構建一個數據源,本地需要安裝mysql,這部分我就不細說了。數據庫安裝完成後,創建一個test庫,建song表,建表語句:
CREATE TABLE `song` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` char(10) DEFAULT NULL,
`year` int(11) DEFAULT NULL,
`length` int(11) DEFAULT NULL,
`type` varchar(64) DEFAULT NULL,
PRIMARY KEY (`id`)
)
接着項目中application.properties中設置數據庫配置:
#driver配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#數據庫配置
spring.datasource.url=jdbc:mysql://localhost:3306/test?useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=123456
#jpa配置
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.ddl-auto是最有意思的一個配置,一不小心可能造成刪庫的悲慘結果。
- create:每次運行該程序,沒有表格會新建表格,表內有數據會清空。
- create-drop:每次程序結束的時候會清空表。
- update:每次運行程序,沒有表格會新建表格,表內有數據不會清空,只會更新
- validate:運行程序會校驗數據與數據庫的字段類型是否相同,不同會報錯
這裏的是否有表格是以應用中配置的Entity實體爲依據的,這意味着應用能夠根據自身的Entity實體來影響表的創建和表的結構。千言萬語匯成一句話,千萬不要配置爲create。
最後需要在應用中配置相應的實體Entity和實體的Repository。項目目錄整體文件結構如下:
其中:
- JpademoApplication.java:spring boot應用入口
- TestController.java: 測試用controller接口
- Song.java: 對應song表的實體類Entity
- SongRepository: 對應實體類Song的Repository接口
Song實體定義:
@Entity //定義爲實體類
@Table(name = "song") //映射表配置
@DynamicInsert //支持動態插入
@DynamicUpdate //支持動態更新
//以下爲lombok懶癌註解
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Song {
@Id //主鍵id
@GeneratedValue //主鍵自動生成
private Long id;
private String name;
private Integer year;
private Integer length;
private String type;
}
Jpa實體Entity對應了持久層的表數據,Song實體的定義,就對應了本地mysql數據test庫中的song表。而對錶數據的增刪改查操作,可以通過Repository實現。
public interface SongRepository extends JpaRepository<Song,Integer>{
Song findById(Long id);
Song getById(Long id);
Song queryByIdAndYear(Long id, Long year);
void deleteById(Long id);
}
SongRepository繼承JpaRepository接口,默認實現了諸如save(S entity)、findAll()、count()、delete(T entity)之類的方法,同時也支持子類通過名字拓展自定義新的查詢或其他操作數據的方式,比如我在上面定義的find、get、query By id或者 IdAndYear,就是通過id或者year組合查詢。雖然這種通過名字來拓展新的操作數據接口的方法看起來比較蠢,但是具體到實現原理還是值得探討一番的。
基本使用
瞭解一個事物基本原理,首先需要全面的瞭解事物的外在功能。對於spirng data jpa,主要的數據操作方式有使用Repository和使用EntityManager這兩種形式。
@Slf4j
@RestController
public class TestController {
@Resource
private EntityManager entityManager;
@Resource
private SongRepository songRepository;
@GetMapping("/")
public List<Song> test() {
return repositoryList();
}
public List<Song> repositoryList() {
//Repository方式
List<Song> songs = songRepository.findAll();
return songs;
}
@Transactional
public void repositorySave() {
Song song = new Song();
song.setName("MyHeart");
song.setLength(230);
song.setYear(2020);
songRepository.save(song);
}
@Transactional
public void entityManagerMerge() {
//EntityManager方式
Song song = new Song();
song = new Song();
song.setName("Journey");
song.setLength(230);
song.setYear(2020);
entityManager.merge(song);
}
public List<Song> hqlList() {
//hql方式
Query query = entityManager.createQuery("select s as ss from Song s");
List<Song> songs = query.getResultList();
return songs;
}
public List<Song> sqlList() {
//sql方式
Query nativeQuery = entityManager.createNativeQuery("select s from song s where name LIKE 'MyHeart'");
List<Song> songs = nativeQuery.getResultList();
return songs;
}
}
Repository提供標準Crud接口,也支持按照規範的命名標準自定義接口,這些在上文已經介紹過了。EntityManager方式其實是Repository的底層實現,這種方式提供了創建CriteriaQuery(上例沒有展示)、hql 的Query、SQL的Query,通過Query完成最終和數據庫的交互。從上例可以看出hql形式和sql比較類似,只是hql在“select s as ss from Song s”語句中,使用了面向對象的封裝,這裏Song對應的是實體Song,而不表名。原生NativeQuery對應的就是原生Sql查詢。
除此之外,Repository也提供非規範命名的接口查詢,本質也是使用EntityManager的hql和sql方式。在Repository的接口方法上可以使用@Query註解,並定義相應的hql或者sql的語句,查詢條件需要和接口方法的入參一致,查詢結果對應接口方法的返回參數。
實際上,spring data jpa提供了一個sql演化的基本思路,即從sql到hql再到結合實體使用的Repository。但是實體很難解決關係型數據庫表數據的join問題,在《java數據庫持久層框架基礎:爲什麼不是JPA?》這篇文章中,我提到了可以使用querydsl解決聯表問題,實際上spring data jpa也有自己的方案,即在單個實體中定義映射集合。簡單解釋來說,假設一個表對應實體類Person,Person包含一個集合屬性Head,Head對應的是另一個表中的數據。Person實體中使用
@JoinTable(name = "head", joinColumns = { @JoinColumn(nullable = false, name = "item_id", referencedColumnName = "id") })
實現聯表。但這種方式存在複雜的級聯關係,在更新、插入和刪除操作中極其難以處理。