JPA是Java Persistence API的簡稱,Java持久層API,是JDK 5.0注解或XML描述對象-關系表的映射關系,並將運行期的實體對象持久化到數據庫中。
它是SUN公司推出的一套基於ORM的規范。ORM又是什么?全英文名為Object-Relational Mapping:對象關系映射,簡單來說為了不用JDBC那一套原始方法來操作數據庫,ORM框架橫空出世。
簡單介紹下,Spring中的Spring-data-jpa技術,Spring Data JPA 是 Spring Data 系列的一部分,可以輕松實現基於 JPA 的存儲庫。該模塊在處理對基於 JPA 的數據訪問層的有所增強。使用數據訪問技術快速構建應用變得更加容易。
特征:
- 支持基於spring和JPA構建存儲數據業務
- 支持基於實體映射的XML
- 提供使用原生sql能力的@Query注解
- 分頁支持、動態查詢
本文的目錄結構:

表示層:controller
業務層:Entity、Service、Vo
數據訪問層:Repository
下面,我們通過一個完整地項目來展現JPA地能力:
1、創建數據庫和表:
我們使用Navicat for MySQL工具,執行建表語句,創建一個student表
student建表語句
CREATE TABLE `student` (
`student_id` varchar(50) NOT NULL,
`name` varchar(50) NOT NULL DEFAULT 'cavan',
`classroom` varchar(50) NOT NULL DEFAULT '0',
`sex` varchar(10) NOT NULL DEFAULT '',
`email` varchar(50) NOT NULL,
`phone` varchar(20) NOT NULL DEFAULT '',
`created_by` varchar(20) NOT NULL DEFAULT 'cavan',
`last_modified_by` varchar(20) NOT NULL DEFAULT 'cavan',
`last_modified_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`student_id`),
UNIQUE KEY (`email`,`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
查看創建后的表:

2、引入依賴包
pom.xml的配置信息
pom.xml
<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>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.1.14</version>
</dependency>
3、配置application.properties連接數據庫信息
application.properties
# 應用名稱
spring.application.name=jpa
# 數據庫驅動:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 數據源名稱
spring.datasource.name=defaultDataSource
# 數據庫連接地址
spring.datasource.url=jdbc:mysql://121.43.225.17:3306/test?serverTimezone=UTC
# 數據庫用戶名&密碼:
spring.datasource.username=root
spring.datasource.password=
# 應用服務 WEB 訪問端口
server.port=8888
4、業務層
4.1 定義entity實體類(StudentEntity類)
StudentEntity類
package com.cavan.jpa.entity;
import org.hibernate.annotations.Generated;
import org.hibernate.annotations.GenerationTime;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.sql.Timestamp;
/**
* @program: jpa
* @description: <description>
* @author: cavan
* @create: 2021-12-03 20:44
*/
@Entity
@Table(name = "student")
@GenericGenerator(name = "uuid", strategy = "uuid")
public class StudentEntity {
private String id;
private String name;
private String classroom;
private String sex;
private String email;
private String phone;
private String createdBy;
private String lastModifiedBy;
private Timestamp lastModifiedDate;
private Timestamp createdDate;
@Id
@GeneratedValue(generator = "uuid")
@Column(name = "student_id")
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getClassroom() {
return classroom;
}
public void setClassroom(String classroom) {
this.classroom = classroom;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getCreatedBy() {
return createdBy;
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
public String getLastModifiedBy() {
return lastModifiedBy;
}
public void setLastModifiedBy(String lastModifiedBy) {
this.lastModifiedBy = lastModifiedBy;
}
@Column(name = "last_modified_date",
columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP",
insertable = false,
updatable = false)
@Generated(GenerationTime.ALWAYS)
public Timestamp getLastModifiedDate() {
return lastModifiedDate;
}
public void setLastModifiedDate(Timestamp lastModifiedDate) {
this.lastModifiedDate = lastModifiedDate;
}
@Column(name = "created_date",
columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP",
insertable = false,
updatable = false)
@Generated(GenerationTime.ALWAYS)
public Timestamp getCreatedDate() {
return createdDate;
}
public void setCreatedDate(Timestamp createdDate) {
this.createdDate = createdDate;
}
}
一些常用注解(本文用到不多):
| 注解 | 作用 | 常用屬性 |
|---|---|---|
| @Entity | 指定當前類是實體類 | |
| @Table | 指定實體類和表之間的對應關系 | name:指定數據庫表的名稱 |
| @EntityListeners | 在實體類增刪改的時候監聽,為創建人/創建時間等基礎字段賦值 | value:指定監聽類 |
| @Id | 指定當前字段是主鍵 | |
| @SequenceGenerator | 指定數據庫序列別名 | sequenceName:數據庫序列名,name:取的別名 |
| @GeneratedValue | 指定主鍵的生成方式 | strategy :指定主鍵生成策略 generator:選擇主鍵別名 |
| @Column | 指定實體類屬性和數據庫表之間的對應關系 | name:指定數據庫表的列名稱, unique:是否唯一, nullable:是否可以為空,nserttable:是否可以插入,updateable:是否可以更新,columnDefinition: 定義建表時創建此列的DDL |
| @CreatedBy | 自動插入創建人 | |
| @CreatedDate | 自動插入創建時間 | |
| @LastModifiedBy | 自動修改更新人 | |
| @LastModifiedDate | 自動修改更新時間 | |
| @JsonFormat | 插入/修改/讀取的時間轉換成想要的格式 | pattern:展示格式,timezone:國際時間 |
注意:
有了@EntityListeners(AuditingEntityListener.class)這個注解,@CreatedBy、@CreatedDate 、@LastModifiedBy 、@LastModifiedDate才生效哦,而且創建人和更新人需要另作注入操作。
4.2 Vo請求/響應類
由於我們在進行增刪改查操作時所需要的請求入參信息是不一樣的,接口響應需要返回的信息也不太一樣,所以我們定義了不同的請求類,用以接收請求消息,傳遞給業務使用。這里我們分別定義了StudentVo類(創建、更新使用)、StudentIdVo類(刪除使用)、StudentQueryVo類(查詢使用)。
實際上,當你嚴格校驗你的請求參數時,創建和更新請求類應該創建兩個類加以區分,這個在請求入參校驗實踐會詳細講解。
創建更新請求Vo類:
StudentVo類
package com.cavan.jpa.vo;
/**
* @program: jpa
* @description: <description>
* @author: cavan
* @create: 2021-12-03 22:02
*/
public class StudentVo {
private String id;
private String name;
private String classroom;
private String sex;
private String email;
private String phone;
private String createdBy;
private String lastModifiedBy;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getClassroom() {
return classroom;
}
public void setClassroom(String classroom) {
this.classroom = classroom;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getCreatedBy() {
return createdBy;
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
public String getLastModifiedBy() {
return lastModifiedBy;
}
public void setLastModifiedBy(String lastModifiedBy) {
this.lastModifiedBy = lastModifiedBy;
}
}
刪除請求類:
StudentIdVo類
package com.cavan.jpa.vo;
/**
* @program: jpa
* @description: <description>
* @author: cavan
* @create: 2021-12-04 11:55
*/
public class StudentIdVo {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
查詢請求類:
StudentQueryVo類
package com.cavan.jpa.vo;
/**
* @program: jpa
* @description: <description>
* @author: cavan
* @create: 2021-12-05 11:25
*/
public class StudentQueryVo {
private Integer pagesize;
private Integer pageNum;
private String id;
private String name;
private String classroom;
private String sex;
public Integer getPagesize() {
return pagesize;
}
public void setPagesize(Integer pagesize) {
this.pagesize = pagesize;
}
public Integer getPageNum() {
return pageNum;
}
public void setPageNum(Integer pageNum) {
this.pageNum = pageNum;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getClassroom() {
return classroom;
}
public void setClassroom(String classroom) {
this.classroom = classroom;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
}
4.3 service服務類(分為 StudentService 服務接口類和 StudentServiceImpl 服務實現類)
業務邏輯層的核心就是service服務及其實現類,一些重要的邏輯都會寫到impl中,而不是直接寫在controller中。
我們對學生實現最簡單的新增、刪除、修改、查詢功能。
代碼如下:
服務接口類:
StudentService類
package com.cavan.jpa.service;
import com.cavan.jpa.entity.StudentEntity;
import com.cavan.jpa.vo.StudentIdVo;
import com.cavan.jpa.vo.StudentQueryVo;
import com.cavan.jpa.vo.StudentVo;
import java.util.List;
public interface StudentService {
/**
* create student
*
* @return student
*/
String create(StudentVo studentVo);
/**
* delete student
*
* @return student
*/
String delete(StudentIdVo studentIdVo);
/**
* update student
*
* @return student
*/
String update(StudentVo studentVo);
/**
* query student
*
* @return student
*/
List<StudentEntity> query(StudentQueryVo studentQueryVo);
}
服務實現類:
StudentServiceImpl類
package com.cavan.jpa.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import com.cavan.jpa.entity.StudentEntity;
import com.cavan.jpa.repository.StudentRepository;
import com.cavan.jpa.service.StudentService;
import com.cavan.jpa.vo.StudentIdVo;
import com.cavan.jpa.vo.StudentQueryVo;
import com.cavan.jpa.vo.StudentVo;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
/**
* @program: jpa
* @description: <description>
* @author: cavan
* @create: 2021-12-03 21:37
*/
@Service
@Transactional(rollbackFor = Exception.class)
public class StudentServiceImpl implements StudentService {
@Autowired
private StudentRepository studentRepository;
@Override
public String create(StudentVo studentVo) {
StudentEntity studentEntity = new StudentEntity();
BeanUtils.copyProperties(studentVo, studentEntity);
studentRepository.save(studentEntity);
return "create ok";
}
@Override
public String delete(StudentIdVo studentIdVo) {
studentRepository.deleteById(studentIdVo.getId());
return "delete ok";
}
@Override
public String update(StudentVo studentVo) {
StudentEntity studentEntity = studentRepository.findById(studentVo.getId()).get();
BeanUtil.copyProperties(studentVo, studentEntity, true,
CopyOptions.create().setIgnoreNullValue(true).setIgnoreError(true));
studentRepository.save(studentEntity);
return "update ok";
}
@Override
public List<StudentEntity> query(StudentQueryVo studentQueryVo) {
Integer pageNum = studentQueryVo.getPageNum();
List<Sort.Order> orders = new ArrayList<>();
orders.add(new Sort.Order(Sort.Direction.DESC, "last_modified_date"));
Sort sort = Sort.by(orders);
Pageable pageable = PageRequest.of(pageNum > 0 ? pageNum - 1 : pageNum, studentQueryVo.getPagesize(), sort);
Page<StudentEntity> page = studentRepository.queryStudent(studentQueryVo.getId(),
studentQueryVo.getName(),
studentQueryVo.getSex(),
studentQueryVo.getClassroom(), pageable);
if (page.getTotalPages() < pageNum && page.getTotalPages() > 0) {
page = studentRepository.queryStudent(studentQueryVo.getId(),
studentQueryVo.getName(),
studentQueryVo.getSex(),
studentQueryVo.getClassroom(), pageable);
}
return page.getContent();
}
}
5、表示層
我們這里全部使用的是POST請求,分別定義了以下功能接口:
- 創建接口,用來創建一條學生信息數據
- 刪除接口,根據學生的學號刪除學生信息
- 更新接口,根據學生的學號更新學生信息
- 查詢接口,根據學生姓名、性別、班級等進行查詢操作
StudentManageController類
package com.cavan.jpa.controller;
import com.cavan.jpa.entity.StudentEntity;
import com.cavan.jpa.service.StudentService;
import com.cavan.jpa.vo.StudentIdVo;
import com.cavan.jpa.vo.StudentQueryVo;
import com.cavan.jpa.vo.StudentVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @program: jpa
* @description: <description>
* @author: cavan
* @create: 2021-12-03 20:22
*/
@RestController
@RequestMapping("/student")
public class StudentManageController {
@Autowired
private StudentService service;
/**
* 創建學生
*
* @param studentVo 學生信息
* @return create OK
*/
@PostMapping("/create")
public String create(@RequestBody StudentVo studentVo) {
return service.create(studentVo);
}
/**
* 根據學生的學號刪除學生信息
*
* @param studentIdVo 學號
* @return delete OK
*/
@PostMapping("/delete")
public String delete(@RequestBody StudentIdVo studentIdVo) {
return service.delete(studentIdVo);
}
/**
* 根據學生的學號更新學生信息
*
* @param studentVo 學號
* @return update OK
*/
@PostMapping("/update")
public String update(@RequestBody StudentVo studentVo) {
return service.update(studentVo);
}
/**
* 根據學生姓名、性別、班級等查詢
*
* @return query OK
*/
@PostMapping("/query")
public List<StudentEntity> query(@RequestBody StudentQueryVo studentQueryVo) {
return service.query(studentQueryVo);
}
}
6、數據連接層
這里只定義了原生sql的查詢方法,其他基本的增刪改查操作使用JPA內部提供的即可,只用繼承 JpaRepository,不需要另外聲明。
StudentRepository接口
package com.cavan.jpa.repository;
import com.cavan.jpa.entity.StudentEntity;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
@Repository
public interface StudentRepository extends JpaRepository<StudentEntity, String> {
/**
* query
*/
@Query(value = "select * from student where IF(ifnull(?1,'')!='',student_id=?1,1=1) " +
"and IF(ifnull(?2,'')!='',name like CONCAT('%',?2,'%'),1=1) " +
"and IF(ifnull(?3,'')!='',sex like CONCAT('%',?3,'%'),1=1) " +
"and IF(ifnull(?4,'')!='',classroom like CONCAT('%',?4,'%'),1=1)",
countQuery = "select count(1) from student where IF(ifnull(?1,'')!='',student_id=?1,1=1) " +
"and IF(ifnull(?2,'')!='',name like CONCAT('%',?2,'%'),1=1) " +
"and IF(ifnull(?3,'')!='',sex like CONCAT('%',?3,'%'),1=1) " +
"and IF(ifnull(?4,'')!='',classroom like CONCAT('%',?4,'%'),1=1)", nativeQuery = true)
Page<StudentEntity> queryStudent(String id, String name, String sex, String classroom, Pageable pageable);
}
問題記錄:
(1)第一個問題
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2021-12-03 22:42:34.389 ERROR 15628 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter :
APPLICATION FAILED TO START
Description:
Field service in com.cavan.jpa.controller.StudentManageController required a bean of type 'com.cavan.jpa.service.StudentService' that could not be found.
The injection point has the following annotations:
- @org.springframework.beans.factory.annotation.Autowired(required=true)
Action:
Consider defining a bean of type 'com.cavan.jpa.service.StudentService' in your configuration.
問題分析:
此問題是由於我在使用注解時錯誤的將
@Service
@Transactional(rollbackFor = Exception.class)
這兩個注解放在了StudentService接口類上,實際應該定義在實現類上。

(2)第二個問題
2021-12-03 23:10:39.864 ERROR 15240 --- [nio-8888-exec-1] o.h.engine.jdbc.spi.SqlExceptionHelper : Unknown column 'created_by' in 'field list'
2021-12-03 23:10:39.917 ERROR 15240 --- [nio-8888-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.InvalidDataAccessResourceUsageException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.SQLGrammarException: could not execute statement] with root cause
java.sql.SQLSyntaxErrorException: Unknown column 'created_by' in 'field list'
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:120) ~[mysql-connector-java-8.0.22.jar:8.0.22]
問題分析:
在使用建表語句時,我們創建的Entity實體類中的createdBy字段,映射到數據時會自動識別小駝峰,將其對應成created_by字段,所以建表時,我們要使用下划線方式,創建字段。
錯誤建表字段:

(3)第三個問題

2021-12-03 23:21:02.685 ERROR 14972 --- [nio-8888-exec-2] o.h.engine.jdbc.spi.SqlExceptionHelper : Column 'classroom' cannot be null
2021-12-03 23:21:02.744 ERROR 14972 --- [nio-8888-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement] with root cause
java.sql.SQLIntegrityConstraintViolationException: Column 'classroom' cannot be null
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:117) ~[mysql-connector-java-8.0.22.jar:8.0.22]
問題分析:
未在請求參數前加 @RequestBody 注解導致請求傳過去的參數都為null值

(4)第四個問題
java.sql.SQLSyntaxErrorException: FUNCTION test.count does not exist. Check the 'Function Name Parsi
問題分析:
在使用原生sql的時候,多打了一個空格,導致報錯。在使用原生sql這里最容易出錯,可以通過在Navicat本地來測試語句的正確性。

本文代碼Gitee鏈接:
https://gitee.com/cavan2021/springboot/tree/master/jpa
