SpringBoot系列(七) jpa的使用,以增刪改查為例


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM