1. 程式人生 > >Springboot整合JPA以及動態條件查詢的實現

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進行組裝。有興趣的可以自己實現。