Spring Boot简明教程之数据访问(二):JPA(超详细)

Spring Boot简明教程之数据访问(二):JPA(超详细)

创建项目

创建的过程和我们的第一篇文章:SpringBoot简明教程之快速创建第一个SpringBoot应用大致相同,差别只是我们在挑选所需要的组件时,除了Web组件外,我们需要添加如下三个组件:JPA、MySQL、JDBC

在这里插入图片描述

或者,我们按照第一次创建完成后,手动在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-jdbc</artifactId>
</dependency>
<dependency>
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<scope>runtime</scope>
</dependency>

我们发现,与我们直接使用JDBC template不同的是,这次我们引入了一个新的包:spring-boot-starter-data-jpa,我们进一步查看,就会发现,其主要是引入了一个spring-data-jpa的包。

在这里插入图片描述

Spring Data简介

Spring Data是为了简化构建基于 Spring 框架应用的数据访问技术,包括关系、非关系数据库、
云数据服务等等。SpringData为我们提供使用统一的API来对数据访问层进行操作,让我们在使用关系型或者非关系型数据访问技术时都基于Spring提供的统一标准,标准包含了CRUD(创建、获取、更新、删除)、查询、
排序和分页的相关操作,同时为我们提供了统一的数据访问类的模板,例如:MongoTemplate、RedisTemplate等。

JPA简介

JPA(Java Persistence API)定义了一系列对象持久化的标准,它的出现主要是为了简化现有的持久化开发工作和整合 ORM 技术,目前实现了这一规范的有 Hibernate、TopLink、JDO 等 ORM 框架。

Spring Data 与JPA

我们可以将Spring-data-jpa理解为Spring Boot对于JPA的再次封装,使得我们通过Spring-data-jpa即实现常用的数据库操作:

  • JpaRepository实现基本功能: 编写接口继承JpaRepository既有crud及分页等基本功能
  • 定义符合规范的方法命名: 在接口中只需要声明符合规范的方法,即拥有对应的功能
  • 支持自定义查询: @Query自定义查询,定制查询SQL
  • Specifications查询(Spring Data JPA支持JPA2.0的Criteria查询)

使用Spring Data JPA的基本流程

创建实体类(entity)

package cn.newtol.springboot07.entity;
import javax.persistence.*;

/**
 * @Author: 公众号:Newtol
 * @Description:  JPA使用示例:使用JPA注解配置映射关系
 * @Date: Created in 18:45 2018/9/24
 */

@Entity   //表示一个实体类,和数据表进行映射
@Table(name = "t_user")   //所映射的表的名字,可省略,默认为实体类名
public class User {


    @Id //设置为主键
    @GeneratedValue(strategy = GenerationType.IDENTITY) //定义为自增主键
    private Integer id;

    @Column(name = "t_name",nullable = false)  //设置该字段在数据表中的列名,并设置该字段设置为不可为空
    private String name;

    @Column     //默认则字段名就为属性名
    private Integer phone;

    @Transient   //增加该注解,则在数据表中不会进行映射
    private String address;
    
    // //省略getter settet方法、构造方法,创建时自行补上
}

创建Dao层(Repository)

package cn.newtol.springboot07.repository;

import cn.newtol.springboot07.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * @Author: 公众号:Newtol
 * @Description:
 * @Date: Created in 19:35 2018/9/24
 */
//定义为接口
public interface UserRepository extends JpaRepository<User,Integer>{
	//直接继承即可,不用编写任何方法
}

编写配置文件

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springboot?useSSL=false
    username: root
    password:

  jpa:
    hibernate:
      ddl-auto: create
    show-sql: true

创建Controller

package cn.newtol.springboot07.controller;

import cn.newtol.springboot07.entity.User;
import cn.newtol.springboot07.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.Optional;

/**
 * @Author: 公众号:Newtol
 * @Description:
 * @Date: Created in 19:37 2018/9/24
 */


@RestController
public class UserController {

    @Autowired
    UserRepository userRepository;

    //根据Id查询用户
    @GetMapping("/user/{id}")
    public User getUser(@PathVariable("id") Integer id){
        User user = userRepository.getOne(id);
        return user;
    }

    //插入用户
    @GetMapping("/user")
    public User addUser(User user){
        userRepository.save(user);
        return user;   //返回插入的对象及其自增ID
    }
}

直接启动项目,看到Spring Boot自动帮我们在数据库中自动创建了t_user表:

在这里插入图片描述

并且因为我们之前对address属相使用了@Transient注解,不进行映射,所以我们看到表中没有address字段。现在我们将@Transient注释后,再次启动项目:

在这里插入图片描述

address就也被成功的创建。

接着,我们尝试着往数据库中插入一条数据,我们在浏览器中输入:localhost:8080/user?name=zhangsan&phone=123&address=beijing

在这里插入图片描述

再到数据库中进行查看:

在这里插入图片描述
插入成功。

现在我们来查询刚刚插入的数据,浏览器输入:http://localhost:8080/user/1

在这里插入图片描述

成功查询到刚才插入的数据,并且我们可以在控制台看到Spring Boot为我们打印的刚才执行的查询语句:

在这里插入图片描述

JPA常用注解说明

虽然我们在上面的示例中已经使用了常用了的注解,但是为了方便和理解,我们在这个地方归纳一下关于JPA的一些常用的注解及其对应的参数。

  • @Entity:表示该类是一个的实体类。@Entity标注是必需的 ,name属性为可选;需要注意的是:@Entity标注的实体类至少需要有一个无参的构造方法。

  • @Table:表示该实体类所映射的数据表信息,需要标注在类名前,不能标注在方法或属性前。参数如下:

    参数 说明
    name 实体所对应表的名称,默认表名为实体名
    catalog 实体指定的目录名
    schema 表示实体指定的数据库名
    uniqueConstraints 该实体所关联的唯一约束条件,一个实体可以有多个唯一的约束,默认没有约束条件。创建方式:uniqueConstraints = {@uniqueConstraint(columnNames = {“name”,“phone”})}
    indexes 该实体所关联的索引。创建方式:indexes = { @Index(name = “index_name”, columnList = “t_name”)}
  • @Column:表示该属性所映射的字段,此标记可以标注在Getter方法或属性前。它有如下参数:

    参数 说明
    unique 字段是否为唯一标识,默认为false
    nullable 字段是否可以为null值,默认为true
    insertable 使用“INSERT” SQL语脚本插入数据时,是否需要插入该字段的值。
    updatable 使用“UPDATE”脚本插入数据时,是否需要更新该字段的值
    table 当映射多个表时,指定表中的字段。默认值为主表的表名。
    length 当字段的类型为varchar时的字段的长度,默认为255个字符。
    precision 用于表示精度,数值的总长度
    scale 用于表示精度,小数点后的位数。
    columnDefinition 用于创建表时添加该字段所需要另外执行的SQL语句
  • @Id:表示该属性为主键,每一个实体类至少要有一个主键(Primary key)。

    参数 说明
    strategy 表示生成主键的策略 ,有4种类型:GenerationType.TABLE 、 GenerationType.SEQUENCE 、 GenerationType.IDENTITY 、 GenerationType.AUTO 默认为:AUTO,表示自动生成。
    generator 生成规则名,不同的策略有不同的配置
  • @Basic: 实体属性设置加载方式为惰性加载

    参数 说明
    fetch 表示获取值的方式,它的值定义的枚举型,可选值为LAZY(惰性加载)、EAGER(即时加载),默认为即时加载。
    optional 属性是否可以为null,不能用于java基本数据型( byte、int、short、long、boolean、char、float、double )

自定义查询

我们刚刚在上面使用的是JPA已经封装好的一些默认的查询方式,但是我们在实际的项目中,可能需要自定义一些符合实际需求的查询,那么我们就需要用到自定义查询

关键字查询

JPA已经帮我们做好了关键字,我们只需要将这些关键字组成方法名,就可以实现相对应的查询。

关键字 示例 同功能JPQL
And findByLastnameAndFirstname where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname where x.lastname = ?1 or x.firstname = ?2
Is,Equals findByFirstname,findByFirstnameIs,findByFirstnameEquals where x.firstname = 1?
Between findByStartDateBetween where x.startDate between 1? and ?2
LessThan findByAgeLessThan where x.age < ?1
LessThanEqual findByAgeLessThanEqual where x.age <= ?1
GreaterThan findByAgeGreaterThan where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual where x.age >= ?1
After findByStartDateAfter where x.startDate > ?1
Before findByStartDateBefore where x.startDate < ?1
IsNull findByAgeIsNull where x.age is null
IsNotNull,NotNull findByAge(Is)NotNull where x.age not null
Like findByFirstnameLike where x.firstname like ?1
NotLike findByFirstnameNotLike where x.firstname not like ?1
StartingWith findByFirstnameStartingWith where x.firstname like ?1 (参数前面加 %)
EndingWith findByFirstnameEndingWith where x.firstname like ?1 (参数后面加 %)
Containing findByFirstnameContaining where x.firstname like ?1 (参数两边加 %)
OrderBy findByAgeOrderByLastnameDesc where x.age = ?1 order by x.lastname desc
Not findByLastnameNot where x.lastname <> ?1
In findByAgeIn(Collection<Age> ages) where x.age in ?1
NotIn findByAgeNotIn(Collection<Age> age) where x.age not in ?1
True findByActiveTrue() where x.active = true
False findByActiveFalse() where x.active = false
IgnoreCase findByFirstnameIgnoreCase where UPPER(x.firstame) = UPPER(?1)

我们除了使用find关键字以外,还可以使用countdeleteget等。

例如,我们在Controller中加入如下方法得:

/**
 * @Author: 公众号:Newtol
 * @Description: 自定义关键字查询
 * @Date: Created in 19:37 2018/9/24
 */
@GetMapping("/test/{address}/{phone}")
public List<User> getUserByAddressAndPhone (@PathVariable("address") String address, @PathVariable("phone") Integer phone){
	return userRepository.findByAddressEqualsAndPhoneNot(address,phone);
}
    

在userRepository中鸡加入如下方法:


/**
 * @Author: 公众号:Newtol
 * @Description:
 * @Date: Created in 19:35 2018/9/24
 */
public interface UserRepository extends JpaRepository<User,Integer>{
    public List<User> findByAddressEqualsAndPhoneNot (String address, Integer phone);
}

在数据库中插入下面两条数据:

INSERT INTO `t_user` VALUES ('2', 'beijing', 'zhangsi', '456');
INSERT INTO `t_user` VALUES ('3', 'beijing', 'wangwu', '123');

在浏览器输入:http://localhost:8080/test/beijing/456

我们就可以看到返回了如下数据,查询成功。

在这里插入图片描述

自定义SQL语句

通常如果使用JPA提供给我们的关键字组成的查询方法仍然无法满足需求,那么我们就可以使用@Query来实现自定的SQL语句。

原生SQL

JPA中支持使用原生的SQL语句,例如:

在userRepository中加入下面的方法:

//自定义SQL查询
    @Query(value = "select * from t_user WHERE phone = ? " ,nativeQuery = true)
    List<User> getAllUser (Integer phone);

在controller中加入以下方法:

 //自定义SQL查询
    @GetMapping("/test/{phone}")
    public List<User> getAllUser(@PathVariable("phone") Integer phone){
        return userRepository.getAllUser(phone);
    }

在浏览器中输入:http://localhost:8080/test/123

返回数据,查询成功。在这里插入图片描述

但是,如果我们需要执行UPDATEDELETE操作时,除了使用@Query外,还需要使用@Modifying
@Transactional两个注解。

例如:

在userRepository中加入下面的方法:

 //自定义SQL删除
    @Query(value = "delete from t_user WHERE phone = ? ",nativeQuery = true)
    @Modifying
    @Transactional
    void deleteUser(Integer phone);

在controller中加入以下方法:

//自定义SQL删除
    @GetMapping("/del/{phone}")
    public void deleteUser(@PathVariable("phone") Integer phone){
       userRepository.deleteUser(phone);
    }

在浏览器中输入:http://localhost:8080/del/456,我们就会发现数据库中phone为456的数据就已经被删除了。

JPQL查询

在userRepository中加入下面的方法:

//JPQL查询
    @Query("select u from User u where u.name = ?1")
    User getUserByName(String name);

在controller中加入以下方法:

//JPQL查询
    @GetMapping("/get/{name}")
    public User getUserByName(@PathVariable("name") String name){
        return userRepository.getUserByName(name);
    }

浏览器输入:http://localhost:8080/get/zhangsan,数据返回,查询成功:

在这里插入图片描述

复杂查询

分页查询

我们经常会遇到进行分页查询的情况,而JPA就很好的替我们解决了这一个问题。只需要几行代码就可以解决:

例如:在controller中加入以下方法:

//分页查询
@GetMapping("/userList/{page}/{size}")
public Page<User> getUserList(@PathVariable("page") Integer page, @PathVariable("size") Integer size) {
	Sort sort = new Sort(Sort.Direction.DESC,"id");
    PageRequest pageRequest = PageRequest.of(page,size,sort);
    return  userRepository.findAll(pageRequest);
 }

浏览器输入:http://localhost:8080/userList/0/3

返回的数据格式为:

{
    "content": [
        {
            "id": 3, 
            "name": "wangwu", 
            "phone": 123, 
            "address": "beijing"
        }, 
        {
            "id": 2, 
            "name": "lisi", 
            "phone": 789, 
            "address": "shanghai"
        }, 
        {
            "id": 1, 
            "name": "zhangsan", 
            "phone": 123, 
            "address": "beijing"
        }
    ], 
    "pageable": {
        "sort": {
            "sorted": true, 
            "unsorted": false
        }, 
        "offset": 0, 
        "pageSize": 3, 
        "pageNumber": 0, 
        "paged": true, 
        "unpaged": false
    }, 
    "totalPages": 1, 
    "last": true, 
    "totalElements": 3, 
    "number": 0, 
    "size": 3, 
    "sort": {
        "sorted": true, 
        "unsorted": false
    }, 
    "numberOfElements": 3, 
    "first": true
}

限制查询

除了限制查询以外,对于排行榜类的数据,或许我们只需要取出前面几名即可,Jpa也对这种情况做了很好的支持:

例如:在userRepository中加入下面的方法:

//限制查询 
    List<User> findFirst2ByAddressOrderByIdDesc (String address);

在controller中加入以下方法:

@GetMapping("/top/{address}")
public List<User> getTop2User(@PathVariable("address") String address){
	return userRepository.findFirst2ByAddressOrderByIdDesc(address);
 }

在浏览器输入:http://localhost:8080/top/beijing

返回的数据为:

[{"id":3,"name":"wangwu","phone":123,"address":"beijing"},{"id":1,"name":"zhangsan","phone":123,"address":"beijing"}]

常见问题及其解决方案:

  1. 如果提示:No identifier specified for entity: xxx

    解决方案:这是因为在创建实体类时,导入的jar包错误导致的。我们将import org.springframework.data.annotation.*;更改为:import javax.persistence.*;即可

  2. 如果提示: No default constructor for entity: :

    解决方案:这是因为在实体类中创建了含参数的构造方法造成的,这是因为在使用类反射机制 Class.newInstance()方法创建实例时,需要有一个默认的无参数构造方法,否则会执出实例化异常(InstantiationException),所以将构造方法更改为无参数即可。

  3. 如果提示:Caused by: org.springframework.dao.InvalidDataAccessApiUsageException: Executing an update/delete query; nested exception is javax.persistence.TransactionRequiredException: Executing an update/delete query

    解决方案:这是因为在JPA中执行DELETE/UPDATE操作时,需要使用@Modifying
    @Transactional两个注解,补上即可。

  4. 如果提示:Can not issue data manipulation statements with executeQuery()。

    解决方案:如果提示这个错误,就需要查询检查自己的SQL语句是否书写正确,如果正确,检查是否加上@Modifying@Transactional两个注解

  5. 如果提示:Ambiguous handler methods mapped for HTTP path

    解决方案:这是由于URL映射重复引起的,将你所请求的URL重新进行定义即可。

  6. 如果在使用PageRequest方法显示过期

    解决方案:将构造方法更改为:PageRequest.of即可。在2.0版本以后,就使用of(…) 方法代替 PageRequest(…)构造器。

总结

我们首先介绍了spring Data,然后演示了一遍使用JPA的基本流程,最后详细的介绍了我们主要使用的几个注解、注意事项等;以及除了使用默认的查询方式外,如何去使用自定义查询和复杂查询。

源码地址

源码地址

联系作者

有关转载、错误指正、问题咨询等事宜请扫码关注个人公众号进行联系,更有大量视频学习资源分享
原文地址:https://www.cnblogs.com/newtol/p/10159081.html