前言
第一次使用 Spring JPA 的時(shí)候,感覺這東西簡直就是神器榨惰,幾乎不需要寫什么關(guān)于數(shù)據(jù)庫訪問的代碼一個(gè)基本的 CURD 的功能就出來了噪舀。下面我們就用一個(gè)例子來講述以下 JPA 使用的基本操作擅威。
新建項(xiàng)目壕探,增加依賴
在 Intellij IDEA 里面新建一個(gè)空的 SpringBoot 項(xiàng)目。具體步驟參考
SpringBoot 的第一次邂逅郊丛。根據(jù)本樣例的需求李请,我們要添加下面三個(gè)依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>6.0.6</version>
</dependency>
準(zhǔn)備數(shù)據(jù)庫環(huán)境
為這個(gè)項(xiàng)目派继,我們專門新建一個(gè) springboot_jpa 的數(shù)據(jù)庫,并且給 springboot 用戶授權(quán)
create database springboot_jpa;
grant all privileges on springboot_jpa.* to 'springboot'@'%' identified by 'springboot';
flush privileges;
項(xiàng)目配置
#通用數(shù)據(jù)源配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://10.110.2.56:3306/springboot_jpa?charset=utf8mb4&useSSL=false
spring.datasource.username=springboot
spring.datasource.password=springboot
# Hikari 數(shù)據(jù)源專用配置
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
# JPA 相關(guān)配置
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create
這李前面的數(shù)據(jù)源配置和前文《SpringBoot 中使用 JDBC Templet》中的一樣捻艳。后面的幾個(gè)配置需要解釋一下
- spring.jpa.show-sql=true 配置在日志中打印出執(zhí)行的 SQL 語句信息。
- spring.jpa.hibernate.ddl-auto=create 配置指明在程序啟動(dòng)的時(shí)候要?jiǎng)h除并且創(chuàng)建實(shí)體類對(duì)應(yīng)的表庆猫。這個(gè)參數(shù)很危險(xiǎn)认轨,因?yàn)樗麜?huì)把對(duì)應(yīng)的表刪除掉然后重建。所以千萬不要在生成環(huán)境中使用月培。只有在測試環(huán)境中嘁字,一開始初始化數(shù)據(jù)庫結(jié)構(gòu)的時(shí)候才能使用一次。
- spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect 杉畜。在 SrpingBoot 2.0 版本中纪蜒,Hibernate 創(chuàng)建數(shù)據(jù)表的時(shí)候,默認(rèn)的數(shù)據(jù)庫存儲(chǔ)引擎選擇的是 MyISAM (之前好像是 InnoDB此叠,這點(diǎn)比較詭異)纯续。這個(gè)參數(shù)是在建表的時(shí)候,將默認(rèn)的存儲(chǔ)引擎切換為 InnoDB 用的灭袁。
建立第一個(gè)數(shù)據(jù)實(shí)體類
數(shù)據(jù)庫實(shí)體類是一個(gè) POJO Bean 對(duì)象猬错。這里我們先建立一個(gè) UserDO 的數(shù)據(jù)庫實(shí)體。數(shù)據(jù)庫實(shí)體的源碼如下
package com.yanggaochao.springboot.learn.springbootjpalearn.security.domain.dao;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
/**
* 用戶實(shí)體類
*
* @author 楊高超
* @since 2018-03-12
*/
@Entity
@Table(name = "AUTH_USER")
public class UserDO {
@Id
private Long id;
@Column(length = 32)
private String name;
@Column(length = 32)
private String account;
@Column(length = 64)
private String pwd;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
}
其中:
- @Entity 是一個(gè)必選的注解茸歧,聲明這個(gè)類對(duì)應(yīng)了一個(gè)數(shù)據(jù)庫表倦炒。
- @Table(name = "AUTH_USER") 是一個(gè)可選的注解。聲明了數(shù)據(jù)庫實(shí)體對(duì)應(yīng)的表信息软瞎。包括表名稱逢唤、索引信息等。這里聲明這個(gè)實(shí)體類對(duì)應(yīng)的表名是 AUTH_USER涤浇。如果沒有指定鳖藕,則表名和實(shí)體的名稱保持一致。
- @Id 注解聲明了實(shí)體唯一標(biāo)識(shí)對(duì)應(yīng)的屬性只锭。
- @Column(length = 32) 用來聲明實(shí)體屬性的表字段的定義吊奢。默認(rèn)的實(shí)體每個(gè)屬性都對(duì)應(yīng)了表的一個(gè)字段。字段的名稱默認(rèn)和屬性名稱保持一致(并不一定相等)纹烹。字段的類型根據(jù)實(shí)體屬性類型自動(dòng)推斷页滚。這里主要是聲明了字符字段的長度。如果不這么聲明铺呵,則系統(tǒng)會(huì)采用 255 作為該字段的長度
以上配置全部正確裹驰,則這個(gè)時(shí)候運(yùn)行這個(gè)項(xiàng)目,我們就可以看到日志中如下的內(nèi)容:
Hibernate: drop table if exists auth_user
Hibernate: create table auth_user (id bigint not null, account varchar(32), name varchar(32), pwd varchar(64), primary key (id)) engine=InnoDB
系統(tǒng)自動(dòng)將數(shù)據(jù)表給我們建好了片挂。在數(shù)據(jù)庫中查看表及表結(jié)構(gòu)
以上過程和我們前使用 Hibernate 的過程基本類似幻林,無論是數(shù)據(jù)庫實(shí)體的聲明還是表的自動(dòng)創(chuàng)建贞盯。下面我們才正式進(jìn)入 Spring Data JPA 的世界,來看一看他有什么驚艷的表現(xiàn)
實(shí)現(xiàn)一個(gè)持久層服務(wù)
在 Spring Data JPA 的世界里沪饺,實(shí)現(xiàn)一個(gè)持久層的服務(wù)是一個(gè)非常簡單的事情躏敢。以上面的 UserDO 實(shí)體對(duì)象為例,我們要實(shí)現(xiàn)一個(gè)增加整葡、刪除件余、修改、查詢功能的持久層服務(wù)遭居,那么我只需要聲明一個(gè)接口啼器,這個(gè)接口繼承
org.springframework.data.repository.Repository<T, ID> 接口或者他的子接口就行。這里為了功能的完備俱萍,我們繼承了 org.springframework.data.jpa.repository.JpaRepository<T, ID> 接口端壳。其中 T 是數(shù)據(jù)庫實(shí)體類,ID 是數(shù)據(jù)庫實(shí)體類的主鍵枪蘑。
然后再簡單的在這個(gè)接口上增加一個(gè) @Repository 注解就結(jié)束了损谦。
package com.yanggaochao.springboot.learn.springbootjpalearn.security.dao;
import com.yanggaochao.springboot.learn.springbootjpalearn.security.domain.dao.UserDO;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* 用戶服務(wù)數(shù)據(jù)接口類
*
* @author 楊高超
* @since 2018-03-12
*/
@Repository
package com.yanggaochao.springboot.learn.springbootjpalearn.security.dao;
import com.yanggaochao.springboot.learn.springbootjpalearn.security.domain.dao.UserDO;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* 用戶服務(wù)數(shù)據(jù)接口類
*
* @author 楊高超
* @since 2018-03-12
*/
@Repository
public interface UserDao extends JpaRepository<UserDO, Long> {
}
一行代碼也不用寫。那么針對(duì) UserDO 這個(gè)實(shí)體類岳颇,我們已經(jīng)擁有下下面的功能
例如成翩,我們用下面的代碼就將一些用戶實(shí)體保存到數(shù)據(jù)庫中了。
package com.yanggaochao.springboot.learn.springbootjpalearn;
import com.yanggaochao.springboot.learn.springbootjpalearn.security.dao.UserDao;
import com.yanggaochao.springboot.learn.springbootjpalearn.security.domain.dao.UserDO;
import org.junit.After;
import org.junit.Before;
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.test.context.junit4.SpringRunner;
import java.util.Optional;
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserDOTest {
@Autowired
private UserDao userDao;
@Before
public void before() {
UserDO userDO = new UserDO();
userDO.setId(1L);
userDO.setName("風(fēng)清揚(yáng)");
userDO.setAccount("fengqy");
userDO.setPwd("123456");
userDao.save(userDO);
userDO = new UserDO();
userDO.setId(3L);
userDO.setName("東方不敗");
userDO.setAccount("bubai");
userDO.setPwd("123456");
userDao.save(userDO);
userDO.setId(5L);
userDO.setName("向問天");
userDO.setAccount("wentian");
userDO.setPwd("123456");
userDao.save(userDO);
}
@Test
public void testAdd() {
UserDO userDO = new UserDO();
userDO.setId(2L);
userDO.setName("任我行");
userDO.setAccount("renwox");
userDO.setPwd("123456");
userDao.save(userDO);
userDO = new UserDO();
userDO.setId(4L);
userDO.setName("令狐沖");
userDO.setAccount("linghuc");
userDO.setPwd("123456");
userDao.save(userDO);
}
@After
public void after() {
userDao.deleteById(1L);
userDao.deleteById(3L);
userDao.deleteById(5L);
}
}
這個(gè)是采用 Junit 來執(zhí)行測試用例赦役。@Before 注解在測試用例之前執(zhí)行準(zhǔn)備的代碼麻敌。這里先插入三個(gè)用戶信息。 執(zhí)行執(zhí)行這個(gè)測試掂摔,完成后术羔,查看數(shù)據(jù)庫就可以看到數(shù)據(jù)庫中有了 5 條記錄:
我們還可以通過測試用例驗(yàn)證通過標(biāo)識(shí)查找對(duì)象功能,查詢所有數(shù)據(jù)功能的正確性乙漓,查詢功能甚至可以進(jìn)行排序和分頁
@Test
public void testLocate() {
Optional<UserDO> userDOOptional = userDao.findById(1L);
if (userDOOptional.isPresent()) {
UserDO userDO = userDOOptional.get();
System.out.println("name = " + userDO.getName());
System.out.println("account = " + userDO.getAccount());
}
}
@Test
public void testFindAll() {
List<UserDO> userDOList = userDao.findAll(new Sort(Sort.Direction.DESC,"account"));
for (UserDO userDO : userDOList) {
System.out.println("name = " + userDO.getName());
System.out.println("account = " + userDO.getAccount());
}
}
可以看到级历,我們所做的全部事情僅僅是在 SpingBoot 工程里面增加數(shù)據(jù)庫配置信息,聲明一個(gè) UserDO 的數(shù)據(jù)庫實(shí)體對(duì)象叭披,然后聲明了一個(gè)持久層的接口寥殖,改接口繼承自 org.springframework.data.jpa.repository.JpaRepository<T, ID> 接口。然后涩蜘,系統(tǒng)就自動(dòng)擁有了豐富的增加嚼贡、刪除、修改同诫、查詢功能粤策。查詢功能甚至還擁有了排序和分頁的功能。
這就是 JPA 的強(qiáng)大之處误窖。除了這些接口外叮盘,用戶還會(huì)有其他的一些需求秩贰, JPA 也一樣可以滿足你的需求。
擴(kuò)展查詢
從上面的截圖 “UserDao 查詢實(shí)體刪除功能” 中柔吼,我們可以看到毒费,查詢功能是不盡人意的,很多我們想要的查詢功能還沒有愈魏。不過放心觅玻。JPA 有非常方便和優(yōu)雅的方式來解決
根據(jù)屬性來查詢
如果想要根據(jù)實(shí)體的某個(gè)屬性來進(jìn)行查詢我們可以在 UserDao 接口中進(jìn)行接口聲明。例如蝌戒,如果我們想根據(jù)實(shí)體的 account 這個(gè)屬性來進(jìn)行查詢(在登錄功能的時(shí)候可能會(huì)用到)。我們?cè)?com.yanggaochao.springboot.learn.springbootjpalearn.security.dao.UserDao 中增加一個(gè)接口聲明就可以了
UserDO findByAccount(String account);
然后增加一個(gè)測試用例
@Test
public void testFindByAccount() {
UserDO userDO = userDao.findByAccount("wentian");
if (userDO != null) {
System.out.println("name = " + userDO.getName());
System.out.println("account = " + userDO.getAccount());
}
}
運(yùn)行之后沼琉,會(huì)在日志中打印出
name = 向問天
account = wentian
這種方式非常強(qiáng)大北苟,不經(jīng)能夠支持單個(gè)屬性,還能支持多個(gè)屬性組合打瘪。例如如果我們想查找賬號(hào)和密碼同時(shí)滿足查詢條件的接口友鼻。那么我們?cè)?UserDao 接口中聲明
UserDO findByAccountAndPwd(String account, String pwd);
再例如,我們要查詢 id 大于某個(gè)條件的用戶列表闺骚,則可以聲明如下的接口
List<UserDO> findAllByIdGreaterThan(Long id);
這個(gè)語句結(jié)構(gòu)可以用下面的表來說明
自定義查詢
如果上述的情況還無法滿足需要彩扔。那么我們就可以通過通過 import org.springframework.data.jpa.repository.Query 注解來解決這個(gè)問題。例如我們想查詢名稱等于某兩個(gè)名字的所有用戶列表僻爽,則聲明如下的接口即可
@Query("SELECT O FROM UserDO O WHERE O.name = :name1 OR O.name = :name2 ")
List<UserDO> findTwoName(@Param("name1") String name1, @Param("name2") String name2);
這里是用 PQL 的語法來定義一個(gè)查詢虫碉。其中兩個(gè)參數(shù)名字有語句中的 : 后面的支付來決定
如果你習(xí)慣編寫 SQL 語句來完成查詢,還可以在用下面的方式實(shí)現(xiàn)
@Query(nativeQuery = true, value = "SELECT * FROM AUTH_USER WHERE name = :name1 OR name = :name2 ")
List<UserDO> findSQL(@Param("name1") String name1, @Param("name2") String name2);
這里在 @Query 注解中增加一個(gè) nativeQuery = true 的屬性胸梆,就可以采用原生 SQL 語句的方式來編寫查詢敦捧。
聯(lián)合主鍵
從 org.springframework.data.jpa.repository.JpaRepository<T, ID> 接口定義來看,數(shù)據(jù)實(shí)體的主鍵是一個(gè)單獨(dú)的對(duì)象碰镜,那么如果一個(gè)數(shù)據(jù)庫的表的主鍵是兩個(gè)或者兩個(gè)以上字段聯(lián)合組成的怎么解決呢兢卵。
我們擴(kuò)充一下前面的場景。假如我們有一個(gè)角色 Role 對(duì)象绪颖,有兩個(gè)屬性 一個(gè) id 秽荤,一個(gè) name ,對(duì)應(yīng)了 auth_role 數(shù)據(jù)表柠横,同時(shí)有一個(gè)角色用戶關(guān)系對(duì)象 RoleUser窃款,說明角色和用戶對(duì)應(yīng)關(guān)系,有兩個(gè)屬性 roleId,userId 對(duì)應(yīng) auth_role_user 表牍氛。那么我們需要聲明一個(gè) RoleDO 對(duì)象如下
package com.yanggaochao.springboot.learn.springbootjpalearn.security.domain.dao;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
/**
* 角色實(shí)體類
*
* @author 楊高超
* @since 2018-03-12
*/
@Entity
@Table(name = "AUTH_ROLE")
public class RoleDO {
@Id
private Long id;
@Column(length = 32)
private String name;
@Column(length = 64)
private String note;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
}
對(duì)于有多個(gè)屬性作為聯(lián)合主鍵的情況雁乡,我們一般要新建一個(gè)單獨(dú)的主鍵類,他的屬性和數(shù)據(jù)庫實(shí)體主鍵的字段一樣糜俗,要實(shí)現(xiàn) java.io.Serializable 接口踱稍,類聲明如下
package com.yanggaochao.springboot.learn.springbootjpalearn.security.domain.dao;
import java.io.Serializable;
/**
* 聯(lián)合主鍵對(duì)象
*
* @author 楊高超
* @since 2018-03-12
*/
public class RoleUserId implements Serializable {
private Long roleId;
private Long userId;
}
同樣的曲饱,我們聲明一個(gè) RoleUserDO 對(duì)象如下
package com.yanggaochao.springboot.learn.springbootjpalearn.security.domain.dao;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;
import java.io.Serializable;
/**
* 角色用戶關(guān)系實(shí)體類
*
* @author 楊高超
* @since 2018-03-12
*/
@Entity
@IdClass(RoleUserId.class)
@Table(name = "AUTH_ROLE_USER")
public class RoleUserDO {
@Id
private Long roleId;
@Id
private Long userId;
public Long getRoleId() {
return roleId;
}
public void setRoleId(Long roleId) {
this.roleId = roleId;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
}
這里因?yàn)閿?shù)據(jù)實(shí)體類和數(shù)據(jù)實(shí)體主鍵類的屬性一樣,所以我們可以刪除掉這個(gè)數(shù)據(jù)實(shí)體主鍵類珠月,然后將數(shù)據(jù)實(shí)體類的主鍵類聲明為自己即可扩淀。當(dāng)然,自己也要實(shí)現(xiàn) java.io.Serializable 接口啤挎。
這樣驻谆,我們?nèi)绻樵兡硞€(gè)角色下的所有用戶列表,就可以聲明如下的接口
@Query("SELECT U FROM UserDO U ,RoleUserDO RU WHERE U.id = RU.userId AND RU.roleId = :roleId")
List<UserDO> findUsersByRole(@Param("roleId") Long roleId);
當(dāng)然了庆聘,這種情況下胜臊,我們會(huì)看到系統(tǒng)自動(dòng)建立了 AUTH_ROLE 和 AUTH_ROLE_USER 表。他們的表結(jié)構(gòu)如下所示
注意這里 auth_role_user 表中伙判,屬性名 userId 轉(zhuǎn)換為了 user_id象对, roleId 轉(zhuǎn)換為了 role_id.
如果我們要用 SQL 語句的方式實(shí)現(xiàn)上面的功能,那么我們就把這個(gè)接口聲明修改為下面的形式宴抚。
@Query("SELECT U.* FROM AUTH_USER U ,AUTH_ROLE_USER RU WHERE U.id = RU.user_id AND RU.role_id = :roleId")
List<UserDO> findUsersByRole(@Param("roleId") Long roleId);
后記
這個(gè)樣例基本上講述了 JPA 使用過程中的一些細(xì)節(jié)勒魔。我們可以看出。使用 JPA 來完成關(guān)于關(guān)系數(shù)據(jù)庫增刪改查的功能是非常的方便快捷的菇曲。所有代碼已經(jīng)上傳到 github 的倉庫 springboot-jpa-learn 上了