Springboot整合JPA以及動態條件查詢的實現
前言:
為了學習JPA技術,我在網上翻閱了幾十篇關於Springboot整合JPA的文章,但文章內容由於作者水平良莠不均,非常影響類似我這樣的菜鳥的學習效率。同時也是為了鞏固並彙總 SpringBoot + JPA 的相關知識,才有了這篇部落格。
1.Demo展示第一階段:
首先展示Demo專案最終的目錄結構,如下圖:
1.1在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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.netops</groupId> <artifactId>sprintboot_jpa</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>sprintboot_jpa</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.7.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
1.2.在application.properties中配置JDBC,JPA
#JDBC配置 spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/springboot_jpa spring.datasource.username=root spring.datasource.password=root #JPA配置 spring.jpa.database=MySQL spring.jpa.show-sql=true spring.jpa.generate-ddl=true spring.jpa.hibernate.ddl-auto=update
1.3.配置實體類
1.3.1學生實體類
import lombok.Data; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Entity @Data public class Student { @Id @GeneratedValue private Integer id;//主鍵自增 private String name;//姓名 private Integer age;//年齡 private String sex;//性別 private Integer classNo;//班級號 private String phoneNum;//手機號碼 private String address;//住址 }
1.3.2老師實體類
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
@Data
public class Teacher {
@Id
@GeneratedValue
private Integer id;//主鍵
private String name;//姓名
private Integer age;//年齡
private String sex;//性別
private String subject;//所授學科名稱
private Integer classNo;//授課班級---注:一個老師可能同時在多個班級授課
private String phoneNum;//手機號
private String address;//地址
}
1.4配置Repository介面
1.4.1TeacherDao
import com.netops.sprintboot_jpa.entity.Teacher;
import org.springframework.data.jpa.repository.JpaRepository;
public interface TeacherRepository extends JpaRepository{
}
1.4.3StudentDao
import com.netops.sprintboot_jpa.entity.Student;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface StudentRepository extends JpaRepository {
List findByNameLike(String surname);
List findAll(Specification mySpec);
}
注:根據application.properties檔案中JDBC配置引數,在MySQL中新建資料庫(這裡我的資料庫名為springboot_jpa),然後啟動springboot專案,JPA就會自動幫我們建立兩張表(student,teacher)。
另外還需要說明的是:
1.由於匯入了Lombok框架的依賴包,在實體類上加入@Data註解,實現了程式執行時自動匯入Getter,Setter方法(其實還有:預設的構造器,equals(),hashCode()和toString()等方法)。如果不加該註解,又不手動新增Getter,Setter方法將會報異常。
2.Repository介面中兩個引數如TeacherDao的<Teacher,Integer>,第一個引數就是實體類的型別:Teacher,第二個引數是實體類主鍵的型別:Teacher的屬性id的型別的包裝類Integer。
2.Demo展示第二階段:
從第一階段的Demo展示,通過已經簡化得不能再簡化的配置,JPA就幫我們在資料庫中建立了表。這運用的是ORM技術,即Object Relational Mapping物件關係對映技術,將實體類Teacher與資料庫中的表teacher進行關聯,實體類Teacher的屬性與表teacher的欄位進行關聯。JPA不僅能夠幫我們建立表,更提供了眾多的介面,幫助我們進行CRUD(增加Create、讀取查詢Retrieve、更新Update、刪除Delete)操作。
2.1準備資料
2.1.1student.sql檔案如下:
/*
Navicat MySQL Data Transfer
Source Server : cloud
Source Server Version : 50622
Source Host : localhost:3306
Source Database : springboot_jpa
Target Server Type : MYSQL
Target Server Version : 50622
File Encoding : 65001
Date: 2017-09-25 18:28:37
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for `student`
-- ----------------------------
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵自增',
`address` varchar(255) DEFAULT NULL COMMENT '地址',
`age` int(11) DEFAULT NULL COMMENT '年齡',
`class_no` int(11) DEFAULT NULL COMMENT '班級號',
`name` varchar(255) DEFAULT NULL COMMENT '姓名',
`phone_num` varchar(255) DEFAULT NULL COMMENT '手機號',
`sex` varchar(255) DEFAULT NULL COMMENT '性別',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of student
-- ----------------------------
INSERT INTO `student` VALUES ('1', '蜀國', '54', '1', '劉備', '12306', '男');
INSERT INTO `student` VALUES ('2', '蜀國', '48', '1', '關羽', '88888', '男');
INSERT INTO `student` VALUES ('3', '蜀國', '46', '1', '張飛', '66666', '男');
INSERT INTO `student` VALUES ('4', '蜀國', '40', '1', '諸葛亮', '12581', '男');
INSERT INTO `student` VALUES ('5', '吳國', '45', '2', '孫權', '34543', '男');
INSERT INTO `student` VALUES ('6', '吳國', '32', '2', '周瑜', '99999', '男');
INSERT INTO `student` VALUES ('7', '吳國', '29', '2', '大喬', '56565', '女');
INSERT INTO `student` VALUES ('8', '吳國', '27', '2', '小喬', '12321', '女');
INSERT INTO `student` VALUES ('9', '魏國', '57', '3', '曹操', '74848', '男');
INSERT INTO `student` VALUES ('10', '魏國', '42', '3', '司馬懿', '58581', '男');
INSERT INTO `student` VALUES ('11', '魏國', '35', '3', '曹丕', '69393', '男');
INSERT INTO `student` VALUES ('12', '魏國', '30', '3', '甄姬', '98778', '女');
2.1.2teacher.sql檔案如下:
/*
Navicat MySQL Data Transfer
Source Server : cloud
Source Server Version : 50622
Source Host : localhost:3306
Source Database : springboot_jpa
Target Server Type : MYSQL
Target Server Version : 50622
File Encoding : 65001
Date: 2017-09-25 18:29:25
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for `teacher`
-- ----------------------------
DROP TABLE IF EXISTS `teacher`;
CREATE TABLE `teacher` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵自增',
`address` varchar(255) DEFAULT NULL COMMENT '地址',
`age` int(11) DEFAULT NULL COMMENT '年齡',
`class_no` int(11) DEFAULT NULL COMMENT '班級號-一個老師可能有多個班級號',
`name` varchar(255) DEFAULT NULL COMMENT '姓名',
`phone_num` varchar(255) DEFAULT NULL COMMENT '手機號',
`sex` varchar(255) DEFAULT NULL COMMENT '性別',
`subject` varchar(255) DEFAULT NULL COMMENT '老師授課學科',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of teacher
-- ----------------------------
INSERT INTO `teacher` VALUES ('1', '武漢', '43', '1', '易中天', '65432', '男', '語文');
INSERT INTO `teacher` VALUES ('2', '武漢', '43', '3', '易中天', '65432', '男', '語文');
INSERT INTO `teacher` VALUES ('3', '杭州', '40', '2', '于丹', '88858', '女', '政治');
INSERT INTO `teacher` VALUES ('4', '杭州', '40', '3', '于丹', '88858', '女', '政治');
INSERT INTO `teacher` VALUES ('5', '北京', '35', '1', '華羅庚', '74147', '男', '數學');
INSERT INTO `teacher` VALUES ('6', '北京', '35', '2', '華羅庚', '74147', '男', '數學');
2.2編寫Controller類如下:
2.2.1StudentController
import com.netops.sprintboot_jpa.entity.Student;
import com.netops.sprintboot_jpa.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/springboot_jpa")
public class StudentController {
@Autowired
private StudentService studentService;
//找出所有的學生
@RequestMapping("/student/findAll")
public List findAll(){
return studentService.findAll();
}
//找出所有姓 “surname”的學生
@RequestMapping("/student/findByNameLike")
public List findByNameLike(@RequestParam("surname") String surname){
return studentService.findByNameLike( surname );
}
//動態查詢:
@RequestMapping("/student/findByCases")
public List findByDynamicCases(){
return studentService.findByDynamicCases();
}
}
2.2.2TeacherController
import com.netops.sprintboot_jpa.entity.Teacher;
import com.netops.sprintboot_jpa.service.TeacherService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/springboot_jpa")
public class TeacherController {
@Autowired
private TeacherService teacherService;
//檢視所有的老師
@RequestMapping("/teacher/findAll")
public List findAll(){
return teacherService.findAll();
}
//根據id查詢對應的老師
@RequestMapping("/teacher/findOne/{id}")
public Teacher findOne(@PathVariable(value = "id")Integer id){
return teacherService.findOne(id);
}
}
2.3編寫Service類如下:
2.3.1StudentService
import com.netops.sprintboot_jpa.domain.StudentRepository;
import com.netops.sprintboot_jpa.entity.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import javax.persistence.criteria.*;
import java.util.List;
@Service
public class StudentService {
@Autowired
private StudentRepository studentRepository;
public List findAll() {
return studentRepository.findAll();
}
public List findByNameLike(String surname) {
return studentRepository.findByNameLike(surname + "%");//查詢條件為 where name like surname%
}
/*
動態查詢的條件:
條件1:性別為 男;
條件2:年齡在25-35之間;
條件3:吳國人;
*/
public List findByDynamicCases() {
return studentRepository.findAll( new Specification() {
@Override
public Predicate toPredicate(Root root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Predicate predicate1,predicate2,predicate3;
Path sex = root.get("sex");
Path age = root.get("age");
Path address = root.get("address");
predicate1 = cb.like(sex,"男");
predicate2 = cb.between(age,25,35);
predicate3 = cb.equal(address,"吳國");
query.where(predicate1,predicate2,predicate3);
return null;
}
});
}
}
2.3.2TeacherService
import com.netops.sprintboot_jpa.domain.TeacherRepository;
import com.netops.sprintboot_jpa.entity.Teacher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class TeacherService {
@Autowired
private TeacherRepository teacherRepository;
public List findAll() {
return teacherRepository.findAll();
}
public Teacher findOne(Integer id) {
return teacherRepository.findOne(id);
}
}
3.Demo測試、展示以及學習總結:
注:這裡再介紹一款火狐瀏覽器的外掛HttpRequester,主要幫助開發人員用來模擬http請求。可以在在火狐瀏覽器選單欄的“附加元件”中進行下載。(HttpRequester外掛已經被新版本的火狐禁用,這裡推薦使用Postman外掛來模擬http請求。該外掛可以在postman官網進行下載)
3.1teacher表的相關操作:
Url欄目內容:http://localhost:8080/springboot_jpa/teacher/findAll
點選Sumbit按鈕可以獲取json格式的結果集,效果如上圖。
Url欄目內容:http://localhost:8080/springboot_jpa/teacher/findOne/2
由於id屬性是用@PathVariable註解進行註釋的,所以直接將引數值在Url後拼接就可以進行查詢,效果如上圖。
3.2student表的相關操作
Url欄目內容:http://localhost:8080/springboot_jpa/student/findAll
效果如上圖,獲取到所有的學生的Json格式。
Url欄目內容:http://localhost:8080/springboot_jpa/student/findByNameLike;然後在Parameters輸入(name:surname,value:曹)
根據上圖可知,由於surname屬性是用@RequestParam註解註釋,所以需要再設定一個surname的引數,這裡設定的surname="曹",也就是查詢所有姓曹的Student;
Url欄目內容:http://localhost:8080/springboot_jpa/student/findByCases
這是一個動態查詢的案例,組合多個查詢條件,查詢結果如上圖。
4.總結
1.由於StudentRepository和TeacherRepository都繼承(extends)了JpaRepository。而JpaRepository介面中自定義瞭如下方法:
@NoRepositoryBean
public interface JpaRepository<T, ID extends Serializable> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
List<T> findAll();
List<T> findAll(Sort var1);
List<T> findAll(Iterable<ID> var1);
<S extends T> List<S> save(Iterable<S> var1);
void flush();
<S extends T> S saveAndFlush(S var1);
void deleteInBatch(Iterable<T> var1);
void deleteAllInBatch();
T getOne(ID var1);
<S extends T> List<S> findAll(Example<S> var1);
<S extends T> List<S> findAll(Example<S> var1, Sort var2);
}
介面JpaRepository又繼承了PagingAndSortingRepository和QueryByExampleExecutor,PagingAndSortingRepository繼承了CrudRepository。見名知意,其子介面StudentRepository和TeacherRepository不用自定義任何方法,就具備CRUD(增刪改查),Page(分頁),Sort(排序)等功能。所以可以直接呼叫findAll(),findOne(ID var1)等方法。
裡面的findOne(ID varl)方法,只能根據主鍵ID查詢具體的物件,如果要其他屬性查詢,需要自定義方法。方法命名規則詳見第二點總結。
2.僅僅在StudentRepository介面中定義抽象方法 List<Student> findByNameLike(String surname); JPA會根據我們自定義的方法名進行解析和拆分,得知該方法的功能。JPA對自定義方法名的解析規則如下:
根據JPA中的方法名命名規則, 我們可以根據業務需求定義相應的抽象方法,而不需要該方法的實現,就可以實現業務功能。
3.從第一點和第二點的總結,我們已經感覺到JPA的強大。從此我們只需要關注業務邏輯,至於如何操作資料,我們就不要關心,一切都交給了JPA。當然,如果JPA只提供了這些功能,JPA也不會如此流行。更重要的是,JPA也提供瞭如何組裝動態條件的查詢。具體實現可以看StudentService中的findByDynamicCases()方法的實現。僅僅只是為了演示動態查詢的效果,我這裡的作法如下:
1)在StudentRepository中定義方法List<Student> findAll(Speciation<Student> mySpec);其實,更加通用的作法是:直接讓StudentRepository繼承JpaSpecificationExcutor<Student>介面,我們可以看該介面的原始碼如下:
public interface JpaSpecificationExecutor {
T findOne(Specification var1);
List findAll(Specification var1);
Page findAll(Specification var1, Pageable var2);
List findAll(Specification var1, Sort var2);
long count(Specification var1);
}
我們可以看到裡面定義了很多方法。繼承了JpaSpecificationExcutor<Student>介面,我們不用自定義方法,直接在StudentService中實現進行具體方法的實現,就可以實現動態查詢。
2)在StudentService中findByDynamicCases()方法中具體的實現。這裡先解釋這幾個物件是什麼意思。
Specification:規則、標準。該物件主要是告訴JPA查詢的過濾規則是什麼。
Predicate:謂語、斷言。該物件主要是定義具體的判斷條件。如predicate1 = cb.like(sex,"男");即判斷條件為性別為男性。
Root: Root<Student> root就是定義引用root指向Student的包裝物件。Path<String> sex = root.get("sex");即通過root來獲取Student的具體屬性。
CriteriaQuery:查詢條件的組裝。query.where(predicate1,predicate2,predicate3);表示按條件predicate1 and predicate2 and predicate3進行組合條件查詢。
CriteriaBuilder:用來構建CritiaQuery的構建器物件;如:predicate2 = cb.between(age,25,35);表示判斷條件為Student.age between 25 and 25;
我這裡的實現是為了演示基於JPA動態查詢(性別為男,年齡在25-25之間,吳國人)我們具體該如何實現。很明顯的看到這段程式碼把查詢條件寫死了,不易於擴充套件。通常情況下是把Specification定義為工具類,每一個判斷條件Predicate定義為Spefication的一個方法,查詢時再將不同的Predicate進行組裝。有興趣的可以自己實現。