百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

【SpringBoot系列教程三】万字详解SpringBoot集成JPA

zhezhongyun 2025-02-17 14:59 23 浏览

JPA是Java Persistence API的简称,中文名为Java持久层API,是JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。

3.1 JPA简介

Sun引入新的JPA ORM规范出于两个原因:其一,简化现有Java EE和Java SE应用开发工作;其二,Sun希望整合ORM技术,实现天下归一。

JPA包括以下3方面的内容:

  1. 一套API标准。在javax.persistence的包下面,用来操作实体对象,执行CRUD操作,框架在后台替代我们完成所有的事情,开发者从烦琐的JDBC和SQL代码中解脱出来。
  2. 面向对象的查询语言:Java Persistence Query Language(JPQL)。这是持久化操作中很重要的一个方面,通过面向对象而非面向数据库的查询语言查询数据,避免程序的SQL语句紧密耦合。
  3. ORM(object/relational metadata)元数据的映射。JPA支持XML和JDK5.0注解两种元数据的形式,元数据描述对象和表之间的映射关系,框架据此将实体对象持久化到数据库表中。

JPA的宗旨是为POJO提供持久化标准规范,由此可见,经过这几年的实践探索,能够脱离容器独立运行,方便开发和测试的理念已经深入人心了。Hibernate3.2+、TopLink 10.1.3以及OpenJPA都提供了JPA的实现,以及最后的Spring的整合Spring Data JPA。目前互联网公司和传统公司大量使用了JPA的开发标准规范。

3.2 JPA和Mybatis

在前面的内容中,我们详细学习了Mybatis框架,那么Mybatis和JPA有什么区别呢?

3.2.1 Mybatis

MyBatis本是Apache的一个开源项目iBatis,2010年这个项目由Apache SoftwareFoundation迁移到了Google Code,并且改名为MyBatis。MyBatis着力于POJO与SQL之间的映射关系,可以进行更为细致的SQL,使用起来十分灵活,上手简单,容易掌握,所以深受开发者的喜欢,目前市场占有率最高,比较适合互联应用公司的API场景。

3.2.2 Spring Data JPA

可以理解为JPA规范的再次封装抽象,底层还是使用了Hibernate的JPA技术实现,引用JPQL(Java Persistence Query Language)查询语言,属于Spring整个生态体系的一部分。随着Spring Boot和Spring Cloud在市场上的流行,Spring Data JPA也逐渐进入大家的视野,它们组成有机的整体,使用起来比较方便,加快了开发的效率,使开发者不需要关心和配置更多的东西,完全可以沉浸在Spring的完整生态标准实现下。JPA上手简单,开发效率高,对对象的支持比较好,又有很大的灵活性,市场的认可度越来越高。

3.3 JPA的主要类和结构图

3.3.1 常用类及接口

7个Repository接口:

  1. Repository (org.springframework.data.repository)
  2. CrudRepository (org.springframework.data.repository)
  3. PagingAndSortingRepository (org.springframework.data.repository)
  4. QueryByExampleExecutor (org.springframework.data.repository.query)
  5. JpaRepository (org.springframework.data.jpa.repository)
  6. JpaSpecificationExecutor (org.springframework.data.jpa.repository)
  7. QueryDslPredicateExecutor (org.springframework.data.querydsl)

2个实现类:

  1. SimpleJpaRepository (org.springframework.data.jpa.repository.support)
  2. QueryDslJpaRepository (org.springframework.data.jpa.repository.support)

上述类和接口的关系图如下:

对于上述接口和类,在这里先有初步了解,在后面的小节中会详细讲解。

3.3.2 SpringBoot整合JPA

下面,我们使用SpringBoot整合JPA,选用Mysql数据库做一个实例:

第一步:创建数据库并插入数据:

CREATE DATABASE JPA_DEMO;
USE JPA_DEMO;
CREATE TABLE USER(USER_ID INT PRIMARY KEY AUTO_INCREMENT,USERNAME VARCHAR(20),MAILN VARCHAR(20));
INSERT INTO USER (USERNAME,MAIL) VALUES ('admin','admin@163.com')

第二步:新建SpringBoot项目并添加如下依赖:



	org.springframework.boot
	spring-boot-starter-thymeleaf



	org.springframework.boot
	spring-boot-starter-web



	org.springframework.boot
	spring-boot-starter-data-jpa



	mysql
	mysql-connector-java
	8.0.24



	org.projectlombok
	lombok
	1.18.20
	provided



	org.springframework.boot
	spring-boot-devtools
	runtime
	true

第三步:配置application.yaml,在文件中配置数据库连接

#Thymeleaf配置
spring:
    thymeleaf:
        mode: HTML5
        encoding: UTF-8
        cache: false
    devtools:
        restart:
            enabled: true
    datasource:
        url: jdbc:mysql://localhost:3306/jpa_demo
        username: root
        password: byte2020
    jpa:
        show-sql: true #打印SQL语句

第四步:在项目中新建cn.bytecollege.model包,并新建实体类,代码如下:

package cn.bytecollege.model;

import lombok.Data;
import javax.persistence.*;

@Data
@Entity
@Table(name = "user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int userId;
    @Column(name = "username")
    private String username;
    @Column(name = "mail")
    private String mail;
}

在实体类中出现了@Data,@Entity,@Table,@Id,@GeneratedValue等注解,这些注解分别属于lombok和JPA,将会在后续的内容详细了解。此处不做赘述

第五步:在项目中新建cn.bytecollege.repository包,并新建UserRepository接口,代码如下:

package cn.bytecollege.repository;
import cn.bytecollege.model.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository {
}

第六步:在项目中新建cn.bytecollege.service包,并新建UserService类,代码如下:

package cn.bytecollege.service;
import cn.bytecollege.model.User;
import cn.bytecollege.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    public List findAllUser(){
        return userRepository.findAll();
    }
}

第七步:在项目中新建cn.bytecollege.controller包,并新建IndexController类,代码如下:

package cn.bytecollege.controller;

import cn.bytecollege.model.User;
import cn.bytecollege.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@Controller
public class IndexController {
    @Autowired
    private UserService userService;
    @RequestMapping(value = "/")
    public String index(Model model){
        List list = userService.findAllUser();
        model.addAttribute("list",list);
        return "index";
    }
}

第八步:在项目下templates目录下新建名称为index.html的Thymeleaf模板文件,代码如下:




    
    首页


    
序号 ID 姓名 邮箱

运行项目的启动类,并在浏览器窗口访问http://localhost:8080/即可看到效果。

3.4 CrudRepository方法详解

从源码可以看到CrudRepository提供了公共的通用的CRUD方法。

public interface CrudRepository extends Repository {
    //保存单个对象
     S save(S entity);
    //保存多个对象
     Iterable saveAll(Iterable entities);
    //通过ID查询
    Optional findById(ID id);
    //通过ID判断是否存在
    boolean existsById(ID id);
    //查询所有数据
    Iterable findAll();
    //通过ID查询所有数据
    Iterable findAllById(Iterable ids);
    //获取总记录数
    long count();
    //根据ID删除数据
    void deleteById(ID id);
    //根据数据实体删除数据
    void delete(T entity);
    //通过ID删除多条数据
    void deleteAllById(Iterable ids);
    //迭代删除多条数据
    void deleteAll(Iterable entities);
    //删除所有数据
    void deleteAll();
}

该接口中定义了通用的增删改查方法,如果对对单表做简单的增删改查,在创建的Repository接口中继承该接口即可。

3.5 PagingAndSortingRepository接口

从该接口的字面意思就可以看出该接口跟分页和排序有关,查看该接口的源码,该接口只定义了2个方法:

  1. Iterable findAll(Sort sort):该方法会对查询的数据进行排序,排序规则需要通过Sort对象声明。
  2. Page findAll(Pageable pageable):对查询的数据进行分页,分页条数可在Pageable中声明。
public interface PagingAndSortingRepository extends CrudRepository {
    
    Iterable findAll(Sort sort);

    Page findAll(Pageable pageable);
}

3.5.1 Sort类

Sort是用于定于排序规则的工具类,Sort类的使用非常简介,Sort提供了以下 个方法用于构造排序条件,支持单字段排序,也支持多字段排序:

//该方法用于指定排序的字段,默认使用升序排序
public static Sort by(String... properties) {
	Assert.notNull(properties, "Properties must not be null!");
	return properties.length == 0 ? unsorted() : new Sort(DEFAULT_DIRECTION, Arrays.asList(properties));
}
//该方法用于指定排序规则和排序字段
public static Sort by(Sort.Direction direction, String... properties) {
	Assert.notNull(direction, "Direction must not be null!");
	Assert.notNull(properties, "Properties must not be null!");
	Assert.isTrue(properties.length > 0, "At least one property must be given!");
	return by((List)Arrays.stream(properties).map((it) -> {
		return new Sort.Order(direction, it);
	}).collect(Collectors.toList()));
 }

下面通过示例来演示该方法的使用,首先新建UserRepository接口,该接口继承
PagingAndSortingRepository接口。

@Repository
public interface UserRepository extends PagingAndSortingRepository {
}

在test目录下的测试用例中编写如下代码:

package cn.bytecollege.chapter03;
import cn.bytecollege.chapter03.model.User;
import cn.bytecollege.chapter03.repository.UserRepository;
import cn.bytecollege.chapter03.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Sort;

import java.util.List;

@SpringBootTest
class Chapter03ApplicationTests {
    @Autowired
    private UserRepository userRepository;
    @Test
    void contextLoads() {
        //指定字段升序排列
        Iterable users1 = userRepository.findAll(Sort.by("userId"));
        users1.forEach(user -> {
            System.out.println(user);
        });
        //指定字段以及排序方式
        Iterable users2 = userRepository.findAll(Sort.by(Sort.Direction.DESC,"userId"));
        users2.forEach(user -> {
            System.out.println(user);
        });
    }
}

需要注意的是Sort类提供了排序的枚举类Direction,其中Sort.Direction.Desc表示降序排序,Sort.Direction.ASC表示升序排序,如果不指定排序规则,则默认使用升序。

3.5.2 Pageable接口

Pageable主要用于设置分页信息,如页面和每页显示的数据条数等。先查看该接口的主要方法。

public interface Pageable {
    /**
     * 设置页数,可以看出该方法内部直接调用了PageRequest.of()方法
     * 指定了页码和页面条数
     */
    static Pageable ofSize(int pageSize) {
        return PageRequest.of(0, pageSize);
    }
    /**
     * 获取返回数据的条目数
     */
    int getPageSize();
    /**
     * 返回排序参数
     */
    Sort getSort();
    /**
     * 获取下一页数据
     */
    Pageable next();
}

3.5.3 Page接口


PagingAndSortingRepository接口的方法中可以看出分页查询的方法返回了Page对象。接下来,我们需要了解一下Page接口中的内容。源码如下:

public interface Page extends Slice {
     //省略部分代码
}

从接口中可以看出Page接口继承了Slice接口,继续查看Slice接口。源码核心方法如下:

public interface Slice extends Streamable {
    //获取当前页码
    int getNumber();
    //获取当前页面条数
    int getSize();
    //获取分页中的数据
    List getContent();
    //判断是否有内容
    boolean hasContent();
    //获取排序字段及规则
    Sort getSort();
    //是否第一页
    boolean isFirst();
    //是否最后一页
    boolean isLast();
    //是否有下一页
    boolean hasNext();
    //是否有前一页
    boolean hasPrevious();
    //获取下一页数据
    Pageable nextPageable();
    //获取前一页数据
    Pageable previousPageable();
  
}

下面,通过示例来学习分页查询的方法:

@SpringBootTest
class Chapter03ApplicationTests {
    @Autowired
    private UserRepository userRepository;
    @Test
    void contextLoads() {
        Page page = userRepository.findAll(PageRequest.of(1,5));
        List list = page.getContent();
        list.forEach(user -> System.out.println(user));
    }
}

在该示例中使用PageRequest规定了查询第一页数据,页面中总共有5条数据。

3.6 JpaRepository方法详解

JpaRepository开始是对关系型数据库进行抽象封装。从类图可以看得出来它继承了
PagingAndSortingRepository类,也就继承了其所有方法,并且实现类也是SimpleJpaRepository。从类图上还可以看出JpaRepository继承和拥有了QueryByExampleExecutor的相关方法。

@NoRepositoryBean
public interface JpaRepository extends PagingAndSortingRepository, QueryByExampleExecutor {
    //查询所有数据
    List findAll();
    //排序查询所有数据
    List findAll(Sort sort);
    //通过Id查询所有数据
    List findAllById(Iterable ids);
    //批量保存数据
     List saveAll(Iterable entities);
    //刷新数据库
    void flush();
    //保存并刷新数据库
     S saveAndFlush(S entity);
    //批量保存并刷新数据库
     List saveAllAndFlush(Iterable entities);
    //通过实体对象批量删除
    void deleteAllInBatch(Iterable entities);
    //通过ID批量删除
    void deleteAllByIdInBatch(Iterable ids);
    //批量删除
    void deleteAllInBatch();
    //根据ID查询数据
    T getById(ID id);
    //条件查询
     List findAll(Example example);
    //条件排序查询
     List findAll(Example example, Sort sort);
}

通过源码和CrudRepository相比较,它支持Query By Example,批量删除,提高删除效率,手动刷新数据库的更改方法,并将默认实现的查询结果变成了List。

在日常开发中,不需要去明确区分该继承那个接口,只需要直接继承JpaRepository接口即可。

JpaRepository的使用方法也一样,只需要继承它即可,例如:

package cn.bytecollege.repository;
import cn.bytecollege.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository {
}

3.7 查询方法的创建

Jpa内部基础架构中有个根据方法名的查询生成器机制,对于在存储库的实体上构建约束查询很有用。该机制方法的前缀有find…By、read…By、query…By、count…By和get…By,从这些方法可以分析它的其余部分(实体里面的字段)。引入子句可以包含其他表达式,例如在Distinct要创建的查询上设置不同的标志。然而,第一个By作为分隔符来指示实际标准的开始。在一个非常基本的水平上,你可以定义实体性条件,并与它们串联(And和Or)。

用一句话概括,待查询功能的方法名由查询策略(关键字)、查询字段和一些限制性条件组成。

@Repository
public interface UserRepository extends JpaRepository {
    //根据姓名和邮箱查询
    List findByUsernameAndMail(String username,String mail);
    //根据ID查询用户
    User findByUserId(int userId);
    //根据姓名查询并排序
    List findByUsernameOrderByUsernameDesc(String username);
}

表达式通常是可以连接的运算符的属性遍历。你可以使用组合属性表达式AND和OR。你还可以将运算关键字Between、LessThan、GreaterThan、Like作为属性表达式。受支持的操作员可能因数据存储而异,因此请参阅官方参考文档的相应部分内容。该方法解析器支持设置一个IgnoreCase标志个别特性(例如,findByLastnameIgnoreCase(…))或支持忽略大小写(通常是一个类型的所有属性为String的情况下,例如,
findByLastnameAndFirstnameAllIgnoreCase(…))。是否支持忽略示例可能会因存储而异,因此请参阅参考文档中的相关章节,了解特定于场景的查询方法。可以通过OrderBy在引用属性和提供排序方向(Asc或Desc)的查询方法中附加一个子句来应用静态排序。要创建支持动态排序的查询方法来影响查询结果。

3.7.1 关键字列表

关键字

示例

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

findByStarDateBetween

where x.startDate between ?1 and ?2

LessThan

findByAgeLessThan

where x.age < ?!

LessThanEqual

findByAgeLessThanEqual

where x.age<=?1

GreaterThan

findByAgeGreaterThan

where x.age>?1

GreaterThanEqual

findByAgeGreaterThanEqual

where x.age>=?!

After

findByStartDateAfter

where startDate>?1

Before

findByStartDateBefore

where x.startDate

IsNull

findByAgeIsNull

where x.age is null

IsNotNull

NotNull

findByAage(Is)NotNull

where x.age not null

Like

findByFirstnameLike

where x.firstname not like ?

NotLike

findByFirstnameNotLike

where x.firstname like ?

StartingWith

findByFirstnameStartingWith

where x.firstname like ?1

参数增加前缀%

EndingWith

findByFirstnameEndingWith

where x.lastname 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 ages)

where x.age in ?1

NotIn

findByAgeNotIn(Collection ages)

where x.age not int ?1

True

findByActiveTrue()

where x.active = true

False

findByActiveFalse()

where x.active = false

IgnoreCase

findByFirstnameIgnoreCase

where upper(x.firstname) = upper(?1)

注意,除了find的前缀之外,我们查看PartTree的源码,还有如下几种前缀:

private static final String QUERY_PATTERN = "find|read|get|query|search|stream";
private static final String COUNT_PATTERN = "count";
private static final String EXISTS_PATTERN = "exists";
private static final String DELETE_PATTERN = "delete|remove";

3.8 查询结果处理

3.8.1 参数选择分页和排序

Page findByUsername(String username,Pageable pageable);
Slice findByUsername(String username,Slice Slice);
List findByUsername(String username,Sort sort);
List findByUsername(String username,Pageable pageable);

第一种方法允许将
org.springframework.data.domain.Pageable实例传递给查询方法,以便动态地将分页添加到静态定义的查询中。Page知道可用的元素和页面的总数。它通过基础框架里面触发计数查询来计算总数。由于这可能是昂贵的,具体取决于所使用的场景,说白了,当用到Pageable的时候会默认执行一条cout语句。而Slice的作用是,只知道是否有下一个Slice可用,不会执行count,所以当查询较大的结果集时,只知道数据是足够的就可以了,而且相关的业务场景也不用关心一共有多少页。

排序选项也通过Pageable实例处理。如果只需要排序,那么在
org.springframework.data.domain.Sort参数中添加一个参数即可。正如你可以看到的,只返回一个List也是可能的。在这种情况下,Page将不会创建构建实际实例所需的附加元数据(这反过来意味着必须不被发布的附加计数查询),而仅仅是限制查询仅查找给定范围的实体。

3.8.3 限制结果查询

在查询方法上加限制查询结果的关键字first和top。

User findFirstOrderByUsernameAsc();
User findTopByOrderByAgeDesc();
Page queryFirst10ByUsername(String username,Pageable pageable);
Slice findTop3ByLastname(String lastname,Pageable pageable);
List findFirst10ByLastname(String lastname,Sort sort);
List findTop10ByLastname(Stirng lastname,Pageable pageable);

查询方法的结果可以通过关键字来限制first或top,其可以被互换地使用。可选的数值可以追加到顶部/第一个以指定要返回的最大结果大小。如果数字被省略,则假设结果大小为1。限制表达式也支持Distinct关键字。此外,对于将结果集限制为一个实例的查询,支持将结果包装到一个实例中的Optional中。如果将分页或切片应用于限制查询分页(以及可用页数的计算),则在限制结果中应用。

3.9 注解式查询

3.9.1 @Query的使用

使用命名查询为实体声明查询是一种有效的方法,对于少量查询很有效。一般只需要关心@Query里面的value和nativeQuery的值。使用声明式JPQL查询有一个好处,就是启动的时候就知道语法正确与否。

@Query注解源码如下:

public @interface Query {
    //指定的JPQL语句。(nativeQuery=true的时候,是原生的SQL语句)
    String value() default "";
    //指定count的JPQL语句,如果不指定将根据query自动生成
    //nativeQuery=true的时候,是原生的SQL语句
    String countQuery() default "";
    //根据哪个字段count,一般默认即可
    String countProjection() default "";
    //默认false,表示value里面不是原生的sql语句
    boolean nativeQuery() default false;
    //可以指定一个query的名字,必须唯一,如果不指定,则自动生成,
    //生成规则:{$domainClass}.${queryMethodName}
    String name() default "";
    //可以指定一个count的query名字,必须唯一,如果不指定,则自动生成
    //生成规则:{$domainClass}.${queryMethodName}count
    String countName() default "";
}

下面,我们来演示该注解的使用。

@Repository
public interface UserRepository extends JpaRepository {
    @Query(value = "select u from User u where u.username=:username")
    List findByUsername(@Param("username")String username);
}

在上面的代码中使用了@Query注解指定了该方法的查询语句,该语句就是JPQL语句。

从该语句中可以看出,虽然JPQL语句和SQL语句有相似之处,但是在JPQL语句中,操作的都是和表建立了映射关系的实体,以及和表字段有映射关系的实体属性。而不是直接对表进行操作,这就屏蔽了SQL语句操作的复杂性,直接操作对应的对象即可。需要注意的是,当在做查询时,要为实体对象起别名,在select后也是对象别名,不能直接写实体名,否则会抛出异常。

此外,不管是JPQL语句也好,还是SQL语句也好,通常需要向JPQL或者SQL语句传递参数,那么如何接收呢,在上面的代码中参数位置处使用了":方法参数名"的方式(注意:@Parma注解的作用是将方法参数名称和sql参数名称绑定,如果方法参数名称和sql语句参数名称一致,可以忽略该注解),JPA还提供了一种方式:“?x”,这里的x是指参数的索引,从1开始,也就是说,上面@Query中的语句接收参数也可使用以如下方式:

@Query(value = "select u from User u where u.username=?1")

@Query中同样支持原生sql语句,但是需要nativeQuery=true,示例如下:

@Query(value="select * from User",nativeQuery=true)

在SpringBoot测试类中进行测试:

@SpringBootTest
class Chapter03ApplicationTests {
    @Autowired
    private UserRepository userRepository;
    @Test
    void contextLoads() {
        List list = userRepository.findByUsername("admin");
        list.forEach(user -> {
            System.out.println(user);
        });
    }
}

3.9.2 @Query排序

@Query在JPQL下想实现排序,直接用PageRequest或者直接用Sort参数都可以。在排序实例中实际使用的属性需要与实体模型里面的字段相匹配,这意味着它们需要解析为查询中使用的属性或别名。示例如下:

public interface UserRepository extends JpaRepository {
    @Query("select u from User u where u.username like ?1%")
    List findByUsernameAndSort(String username,Sort sort);
}
//调用上述方法
userRepository.findByAndSort("admin",Sort.by("username"));

3.9.3 @Query分页

如果要使用@Query分页,直接使用Page对象接收参数,参数直接用Pageable的实现类即可。

public interface UserRepository extends JpaRepository {
    @Query("select u from User u where u.username like ?1%")
    Page findByAndSort(String username,Pageable pageable);
}
//调用上述方法
userRepository.findByAndSort("admin",PageRequest.of(1,10));

3.9.4 @Modifying注解

@Modifying通常是用于标明该操作属于增加、删除、修改。

首先查看@Modifying注解源码:

public @interface Modifying {
    boolean flushAutomatically() default false;
    //如果配置了一级缓存,这是时候配置该属性为true,会刷新一级缓存,否则在同一接口中
    //更新了对象,接着查询这个对象,查出来的对象时未更新之前的状态。
    boolean clearAutomatically() default false;
}

下面示例@Modifying注解的用法:

@Repository
public interface UserRepository extends JpaRepository {
    @Modifying
    @Query(value = "DELETE FROM User u where u.userId=?1")
    int deleteById(int userId);
}
@SpringBootTest
class Chapter03ApplicationTests {
    @Autowired
    private UserRepository userRepository;
    @Test
    void contextLoads() {
        int k = userRepository.deleteById(1);
        System.out.println(k);
    }
}

3.10 @Entity实例中常用注解

实体中的基本注解包括:@Entity、@Table、@Id、@IdClass、@GeneratedValue、@Basic、@Transient、@Column、@Temporal、@Enumerated、@Lob。

在本小节内将详细讲解上述注解:

3.10.1 @Entity

@Entity定义对象将会成为被JPA管理的实体,将映射到指定的数据库表。这个注解是必须的。

@Entity
@Table(name = "user")
public class User {
    //省略部分代码
}

3.10.2 @Table

该注解的作用是将对象实体和数据库表进行关联,该注解通常和@Entity搭配使用。查看该注解源码:

public @interface Table {
    //用于指定该对象实体对应的数据库表名
    String name() default "";
    //通常用于设置表所属的数据库,可不用设置
    String catalog() default "";
    //作用类似于catelog,可不用设置
    String schema() default "";
    //用于设置唯一约束的列,可不设置
    UniqueConstraint[] uniqueConstraints() default {};
    //用于指定索引列,可不设置
    Index[] indexes() default {};
}

3.10.3 @Id

@Id定义属性为数据库的主键,一个实体中必须要添加该属性。

3.10.4 @GeneratedValue

该注解用于定义主键的生成策略,注解源码如下:

public @interface GeneratedValue {
    //主键的生成策略
    GenerationType strategy() default GenerationType.AUTO;
    //通过Sequence生成的ID,常见于Oracle数据库ID生成规则,需要配合@SequenceGenerator使用
    String generator() default "";
}

GenerationType是一个枚举类,共有4个值

public enum GenerationType {
    //通过表产生主键,框架有表模拟序列产生主键,该策略可以更方便的迁移数据库
    TABLE,
    //通过序列产生主键,通过@SequenceGenerator注解指定序列化名,MySQL不支持
    SEQUENCE,
    //采用数据库ID自增长,一般用于MySQL数据库
    IDENTITY,
    //JPA自动选择合适的策略,默认选项
    AUTO;
}

3.10.4 @Column

@Column用于定义对象实体中属性对应的数据库列名。首先查看该注解源码:

public @interface Column {
    //数据库中表的列名,可选,如果不填写则认为字段名和实体属性名一样。如果属性名和字段名不一致
    //可以使用该属性指定
    String name() default "";
    //是否唯一,可选,默认false
    boolean unique() default false;
    //是否允许为空,可选,默认false
    boolean nullable() default true;
    //执行insert操作时是否包含此字段,默认为true,可选
    boolean insertable() default true;
    //执行update操作时是否包含此字段,默认为true,可选
    boolean updatable() default true;
    //表示该字段在数据库中的实例类型
    String columnDefinition() default "";
    //用于指定表名
    String table() default "";
    //字段默认长度
    int length() default 255;
    //指定字段精度
    int precision() default 0;
    //指定字段精度
    int scale() default 0;
}

3.11 关联关系注解

关联关系注解包括@JoinColumn、@OneToOne、@OneToMany、@ManyToOne、@ManyToMany、@JoinTable、@OrderBy。

3.11.1 @JoinColumn

该注解用于指定外键关联的字段名称,该注解源码如下:

public @interface JoinColumn {
    //目标表的字段名,必填属性
    String name() default "";
    //本实体的字段名,非必填,默认是本表的ID
    String referencedColumnName() default "";
    //外键字段是否唯一
    boolean unique() default false;
    //外键字段是否可以为空
    boolean nullable() default true;
    //是否跟随一起新增
    boolean insertable() default true;
    //是否跟随一起更新
    boolean updatable() default true;
    //表示该字段在数据库中的实例类型
    String columnDefinition() default "";
    //指定表名
    String table() default "";
}

@JoinColumn主要配合@OneToOne、@ManyToOne、@OneToMany一起使用,单独使用没有意义。

3.11.2 @OneToOne

该注解用于表名表与表中的数据存在一对一关系,其源码如下:

public @interface OneToOne {
    //关系目标实体,非必填
    Class targetEntity() default void.class;
    //级联操作策略
    CascadeType[] cascade() default {};
    //数据获取方式,EAGER(立即加载)/LAZY(懒加载)
    FetchType fetch() default FetchType.EAGER;
    //是允许为空
    boolean optional() default true;
    //关联关系被谁维护,非必填,一般不需要特别指定
    String mappedBy() default "";
    //是否级联删除,和CascadeType.REMOVE的效果一样,只要配置了两种中的一种就会自动级联删除
    boolean orphanRemoval() default false;
}

级联操作的枚举源码如下:

public enum CascadeType { 
    //包括以下所有选项
    ALL, 
    //级联新建
    PERSIST, 
    //级联更新
    MERGE, 
    //级联删除
    REMOVE,
    //级联刷洗
    REFRESH,
}

可以看出该注解的配置比较简单,需要注意的是mappedBy属性,只有关系维护方才能操作两者的关系,被维护方及时设置了维护方属性进行存储也不会更新外键关联:

  1. mappedBy不能与@JoinColum或者@JoinTable同时使用。
  2. mappedBy的值是指另一方的实体里面属性的字段,而不是数据库的字段,也不是实体的对象的名字,即另一方配置了@JoinColumn或者@JoinTable注解的属性的字段名称。

@OneToOne需要配合@JoinColumn一起使用。注意:可以双向关联,也可以只配置一方,需要视实际需求而定。

下面我们通过一个示例来学习该注解的用法:

首先创建数据表,分别为部门表和雇员表,雇员和部门表之间存在一对一的关系。

#创建部门表
CREATE TABLE DEPARTMENT(
  DEPARTMENT_ID INT PRIMARY KEY AUTO_INCREMENT,
  DEPARTMENT_NAME VARCHAR(20),
  CREATE_TIME DATETIME);
#创建雇员表
CREATE TABLE EMPLOYEE(
  EMPLOYEE_ID INT PRIMARY KEY AUTO_INCREMENT,
  NAME VARCHAR(20),
  AGE INT,
  DEPARTMENT_ID INT);
#新增数据
INSERT INTO DEPARTMENT (DEPARTMENT_NAME,CREATE_TIME) VALUES ("技术部",now());
INSERT INTO EMPLOYEE (NAME,AGE,DEPARTMENT_ID) VALUES ("Jack",28,1);

新建实体类

@Data
@Entity
@Table(name = "department")
public class Department {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int departmentId;
    @Column(name = "department_name")
    private String departmentName;
    @Column(name = "create_time")
    @UpdateTimestamp
    private Date createTime;
}
@Data
@Entity
@Table(name = "employee")
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int employeeId;
    @Column(name = "name")
    private String name;
    @Column(name = "age")
    private int age;
    private int departmentId;
    @OneToOne
    @JoinColumn(name = "departmentId",insertable = false,updatable = false)
    private Department department;
}

departmentId是指引用对象的主键名称。

新建EmployeeRepository.java

public interface EmployeeRepository extends JpaRepository {
}

在测试类中进行测试:

@SpringBootTest
class Chapter03ApplicationTests {
    @Autowired
    private EmployeeRepository employeeRepository;

    @Test
    void contextLoads() {
        Employee employee = employeeRepository.findById(1).get();
        System.out.println(employee);
    }
}

3.11.3 @OneToMany与@ManyToOne关联关系

@OnToMany与@ManyToOne可以相对存在,也可以只存在一方。

public @interface OneToMany {
    //同OneToOne
    Class targetEntity() default void.class;
    //级联操作,如果不填,默认关系不会产生任何影响
    CascadeType[] cascade() default {};
    //获取数据方式,默认懒加载
    FetchType fetch() default LAZY;
    //关系被谁维护
    String mappedBy() default "";
    //是否级联删除
    boolean orphanRemoval() default false;
}

继续以上一小节中的两张表为例,员工对于部门属于一对一的关系,而部门对于员工则是一对多的关系,对实体类进行修改如下:

@Data
@Entity
@Table(name = "employee")
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int employeeId;
    @Column(name = "name")
    private String name;
    @Column(name = "age")
    private int age;
    @Column(name = "departmentId")
    private int departmentId;
}
@Data
@Entity
@Table(name = "department")
public class Department {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int departmentId;
    @Column(name = "department_name")
    private String departmentName;
    @Column(name = "create_time")
    @UpdateTimestamp
    private Date createTime;
    @OneToMany(fetch = FetchType.EAGER)
    @JoinColumn(name = "departmentId")
    private List list;
}

新建DepartmentRepository.java

@Repository
public interface DepartmentRepository extends JpaRepository {
}

测试类中进行测试:

@SpringBootTest
class Chapter02ApplicationTests {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private EmployeeRepository employeeRepository;
    @Autowired
    private DepartmentRepository departmentRepository;
    @Test
    void contextLoads() {
        List list = departmentRepository.findAll();
        list.forEach(e-> System.out.println(e));
    }
}

3.11.4 @OrderBy关联查询时排序

该注解通常和@OneToMany一起使用。源码如下:

public @interface OrderBy {

   /**
    * 排序的字段格式如下:
    *    orderby_list::= orderby_item [,orderby_item]*
    *    orderby_item::= [property_or_field_name] [ASC | DESC]
    * 字段也可以是实体属性,也可以使数据库字段,默认ASC
    */
    String value() default "";
}

以上一小节的Department对象为例,用法如下:

public class Department {
    //省略部分字段
    @OneToMany(fetch = FetchType.EAGER)
    @JoinColumn(name = "departmentId")
    @OrderBy("age DESC")
    private List list;
}

3.11.5 @JoinTable关联表

如果对象与对象之间有一个关联关系表的时候,就需要用到@JoinTable,一般和@ManyToMany一起使用。

首先查看@ManyToMany源码:

public @interface JoinTable {

    /**
     * 中间关联表表名
     */
    String name() default "";

    /** 
     * 表catalog
     */
    String catalog() default "";

    /** 
     * 表schema
     */
    String schema() default "";

    /**
     * 主连接表的字段
     */
    JoinColumn[] joinColumns() default {};
}

@ManyToMany源码如下:

public @interface ManyToMany {
    Class targetEntity() default void.class;
    CascadeType[] cascade() default {};
    FetchType fetch() default LAZY;
    String mappedBy() default "";
}

@ManyToMany表示多对多,和@OneToOne、@ManyToOne一样也有单向、双向之分。单向双向和注解没有关系,只看实体类之间是否相互引用。

以学生选课为例,一个学生可以选择多门课程,一门课程也可以被多个学生选择,因此学生和课程之间是多对多的关系,新建数据表,代码如下:

CREATE TABLE STUDENT (
  STUDENT_ID INT PRIMARY KEY AUTO_INCREMENT,
  STUDENT_NAME VARCHAR(20),
  STUDENT_AGE INT,
  STUDENT_GENDER CHAR(2)
);
CREATE TABLE COURSE(
  COURSE_ID INT PRIMARY KEY AUTO_INCREMENT,
  COURSE_NAME VARCHAR(20)
);
CREATE TABLE COURSE_RECORD(
   RECORD_ID INT PRIMARY KEY AUTO_INCREMENT,
   STUDENT_ID INT,
   COURSE_ID INT 
);
INSERT INTO STUDENT VALUES (1,'张三',18,'男');
INSERT INTO STUDENT VALUES (2,'李四',19,'男');
INSERT INTO STUDENT VALUES (3,'王五',17,'男');
INSERT INTO STUDENT VALUES (4,'赵六',18,'男');

INSERT INTO COURSE VALUES (1,'语文');
INSERT INTO COURSE VALUES (2,'数学');
INSERT INTO COURSE VALUES (3,'英语');

INSERT INTO COURSE_RECORD VALUES (1,1,1);
INSERT INTO COURSE_RECORD VALUES (2,1,2);
INSERT INTO COURSE_RECORD VALUES (3,1,3);
INSERT INTO COURSE_RECORD VALUES (4,2,1);
INSERT INTO COURSE_RECORD VALUES (5,2,2);
INSERT INTO COURSE_RECORD VALUES (6,2,3);
INSERT INTO COURSE_RECORD VALUES (7,3,1);
INSERT INTO COURSE_RECORD VALUES (8,3,2);
INSERT INTO COURSE_RECORD VALUES (9,3,3);

新建实体类,代码如下:

package cn.bytecollege.chapter03.model;

import lombok.Data;
import javax.persistence.*;
import java.util.List;

@Data
@Entity
@Table(name = "student")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer studentId;
    private String studentName;
    private Integer studentAge;
    private String studentGender;
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "course_record",
            joinColumns = @JoinColumn(name = "studentId"),
            inverseJoinColumns = @JoinColumn(name = "courseId"))
    private List courses;
}
package cn.bytecollege.chapter03.model;

import lombok.Data;
import javax.persistence.*;

@Data
@Entity
@Table(name = "course")
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer courseId;
    private String courseName;
}
package cn.bytecollege.chapter03.model;

import lombok.Data;
import javax.persistence.*;

@Data
@Entity
@Table(name = "course_record")
public class CourseRecord {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer recordId;
    private Integer studentId;
    private Integer courseId;
}

新建StudentRepository接口,代码如下:

public interface StudentRepository extends JpaRepository {
}

测试类中进行测试:

@SpringBootTest
class Chapter03ApplicationTests {
    @Autowired
    private StudentRepository studentRepository;
    @Test
    void contextLoads() {
        List list = studentRepository.findAll();
        list.forEach(System.out::println);
    }
}

3.12 连接查询和@EntityGraph

当使用@ManyToMany、@ManyToOne、@OneToMany、@OneToOne关联关系的时候,FetchType怎么配置LAZY或者EAGER。SQL真正执行的时候是由一条主表查询和N条子表查询组成的。这种查询效率一般比较低下,比如子对象有N个就会执行N+1条SQL。例如在上一小节中的示例会发现打印了N+1条SQL语句。

有时候我们需要用到Left Join或者Inner Join来提高效率,只能通过@Query的JQPL语法实现,后面我们将讲到的Criteria API也可以做到。Spring Data JPA为了简单地提高查询率,引入了EntityGraph的概念,可以解决N+1条SQL的问题。

3.12.1 @EntityGraph

JPA 2.1推出来的@EntityGraph、@NamedEntityGraph用来提高查询效率,很好地解决了N+1条SQL的问题。两者需要配合起来使用,缺一不可。@NamedEntityGraph配置在@Entity上面,而@EntityGraph配置在Repository的查询方法上面。我们来看一下实例。

  1. 先在Entity里面定义@NamedEntityGraph,其他都不变。其中,@NamedAttributeNode可以有多个,也可以有一个。
@Data
@NamedEntityGraph(name = "Student.courses",attributeNodes = @NamedAttributeNode("courses"))
@Entity
@Table(name = "student")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer studentId;
    private String studentName;
    private Integer studentAge;
    private String studentGender;
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "course_record",
            joinColumns = @JoinColumn(name = "studentId"),
            inverseJoinColumns = @JoinColumn(name = "courseId"))
    private List courses;
}
  1. 只需要在查询方法上加@EntityGraph注解即可,其中value就是@NamedEntityGraph中的Name。实例配置如下:
public interface StudentRepository extends JpaRepository {
    @EntityGraph(value = "Student.courses")
    @Override
    List findAll();
}

再次运行测试类会发现控制台只打印了1条SQL语句。

3.13 QueryByExampleExecutor的使用

按示例查询(QBE)是一种用户友好的查询技术,具有简单的接口。它允许动态查询创建,并且不需要编写包含字段名称的查询。从UML图中,可以看出继承JpaRepository接口后,自动拥有了按“实例”进行查询的诸多方法。可见SpringData的团队已经认为了QBE是Spring JPA的基本功能了,继承QueryByExampleExecutor和继承JpaRepository都会有这些基本方法。

3.13.1 QueryByExampleExecutor详细配置

QueryByExampleExecutor的源码如下:

public interface QueryByExampleExecutor {
    //根据example查找一个对象
     Optional findOne(Example example);
    //根据example查找一批对象
     Iterable findAll(Example example);
    //根据example查找一批对象,且排序
     Iterable findAll(Example example, Sort sort);
    //根据example查找一批对象,并且分页
     Page findAll(Example example, Pageable pageable);
    //根据example查找,返回符合条件的对象个数
     long count(Example example);
    //根据example查询,判断是否有符合条件的对象
     boolean exists(Example example);
}

接下来查看Example的源码,了解Example的用法:

public interface Example {
    static  Example of(T probe) {
        return new TypedExample(probe, ExampleMatcher.matching());
    }

    static  Example of(T probe, ExampleMatcher matcher) {
        return new TypedExample(probe, matcher);
    }

    T getProbe();

    ExampleMatcher getMatcher();

    default Class getProbeType() {
        return ProxyUtils.getUserClass(this.getProbe().getClass());
    }
}

从源码中可以看出Example主要包含三部分内容:

  • Probe:这是具有填充字段的域对象的实际实体类,即查询条的封装类。必填。
  • ExampleMatcher:ExampleMatcher有关于如何匹配特定字段的匹配规则,它可以重复使用在多个示例。必填。如果不填,用默认的。

Example:Example由探针和ExampleMatcher组成。它用于创建查询。它提供了两个of()方法来创建Example对象:

  • of(T probe):以probe对象创建最简单的Example对象
  • of(T probe,ExampleMatcher matcher):以probe对象创建Example对象,并使用matcher指定匹配规则。

3.13.1 QueryByExampleExecutor使用示例

以上面章节中的数据表employee为例。

@SpringBootTest
class Chapter03ApplicationTests {
    @Autowired
    private EmployeeRepository employeeRepository;
    @Test
    void contextLoads() {
        //创建查询条件数据对象
        Employee employee = new Employee();
        employee.setAge(28);
        //创建匹配器,忽略空属性,如果不忽略会将空属性拼接到SQL语句中。
        ExampleMatcher matcher = ExampleMatcher.matching();
        Example employeeExample = Example.of(employee,matcher);

        List list = employeeRepository.findAll(employeeExample);

        list.forEach(e->{
            System.out.println(e);
        });
    }
}

上面例子中,是这样创建“实例”的:Example ex =Example.of(employeea, matcher);我们看到,Example对象,由customer和matcher共同创建,为讲解方便,我们先来明确一些定义。

  1. Probe:实体对象,在持久化框架中与Table对应的域对象,一个对象代表数据库表中的一条记录,如上例中Employee对象。在构建查询条件时,一个实体对象代表的是查询条件中的“数值”部分。如:要查询年龄“28”的雇员,实体对象只能存储条件值“28”。
  2. ExampleMatcher:匹配器,它是匹配“实体对象”的,表示了如何使用“实体对象”中的“值”进行查询,它代表的是“查询方式”,解释了如何去查的问题。如上例中ExampleMatcher.matching().取了基本的匹配器,即等值判断,注意:实体类中的属性应该使用包装类或者引用类型,尽量避免使用基本类型。
  3. Example:实例对象,代表的是完整的查询条件。由实体对象(查询条件值)和匹配器(查询方式)共同创建。

再来理解“实例查询”,顾名思义,就是通过一个例子来查询。要查询的是Employee对象,查询条件也是一个Employee对象,通过一个现有的雇员对象作为例子,查询和这个例子相匹配的对象。

3.13.2 QueryByExampleExecutor的特点及约束

  1. 支持动态查询。即支持查询条件个数不固定的情况,如:客户列表中有多个过滤条件,用户使用时在“地址”查询框中输入了值,就需要按地址进行过滤,如果没有输入值,就忽略这个过滤条件。对应的实现是,在构建查询条件Customer对象时,将address属性值设置为具体的条件值或设置为null。
  2. 不支持过滤条件分组。即不支持过滤条件用or(或)来连接,所有的过滤查件,都是简单一层的用and(并且)连接。如firstname = ?0 or (firstname = ?1and lastname = ?2)。
  3. 仅支持字符串的开始/包含/结束/正则表达式匹配和其他属性类型的精确匹配。查询时,对一个要进行匹配的属性(如:姓名name),只能传入一个过滤条件值,如以Customer为例,要查询姓“刘”的客户,“刘”这个条件值就存储在表示条件对象的Customer对象的name属性中,针对于“姓名”的过滤也只有这么一个存储过滤值的位置,没办法同时传入两个过滤值。正是由于这个限制,有些查询是没办法支持的,例如要查询某个时间段内添加的客户,对应的属性是addTime,需要传入“开始时间”和“结束时间”两个条件值,而这种查询方式没有存两个值的位置,所以就没办法完成这样的查询。

3.13.3 ExampleMatcher详解

ExampleMatcher 提供了如下静态方法来创建实例。

  • static ExampleMatcher matching():创建一个需要所有属性都匹配的匹配器。
  • static ExampleMatcher matchingAny():创建一个只要任意一个属性匹配的匹配器。
  • static ExampleMatcher matchingAlI():它完全等同于 matchin()方法。

假如传入的样本 Student 包含 3 个非空属性:name、gender、address,如果使用 matching()或matchingAl()方法创建的 ExampleMatcher,那么就查询 name、gender、address 属性全都匹配的记录。

但如果使用 matchingAny()方法创建的 ExampleMatcher,那么就查询只要 name、gender、address任意一个属性能匹配的记录。

简单来说,matching()或 matchingAII()方法创建的 ExampleMatcher使用 AND 作为查询条件的连接符,而 matchingAny()方法创建的 ExampleMatcher 使用 OR 作为查询条件的连接符。

此外,ExampleMatcher 还可通过如下方法来指定对特定属性的匹配规则。

  • withIgnoreCase():指定属性匹配时默认不区分大小写。
  • withlgnoreCase(String...propertyPaths):指定 propertyPaths 参数列出的属性匹配时不区分大小写。
  • withlgnoreNullValues():指定不比较 Example 对象中属性值为 null 的属性。
  • withlgnorePaths(String...ignoredPaths):指定忽略 ignoredPaths 参数列出的属性,也就是这些属性不参与匹配。
  • withncludeNullValues():强行指定要比较 Example 对象中属性值为 null 的属性。
  • withMatcher(String propertyPath,比较器):对 propertyPath 参数指定的属性使用专门的匹配规则。

最后一个方法允许通过“比较器”参数来指定匹配规则,该参数既可使用 GenericPropertyMatcher对象,也可使用 Lambda 表达式。

ExampleMatcher接口的实现类只有TypedExampleMatcher核心源码如下:

class TypedExampleMatcher implements ExampleMatcher {
    private final NullHandler nullHandler;
    private final StringMatcher defaultStringMatcher;
    private final PropertySpecifiers propertySpecifiers;
    private final Set ignoredPaths;
    private final boolean defaultIgnoreCase;
    private final MatchMode mode;
    
    TypedExampleMatcher() {

		this(NullHandler.IGNORE, StringMatcher.DEFAULT, new PropertySpecifiers(), Collections.emptySet(), false,
				MatchMode.ALL);
	}
}

关键属性分析:

  1. nullHandler:Null值处理方式,枚举类型,有2个可选值:INCLUDE(包括)IGNORE(忽略)标识作为条件的实体对象中,一个属性值(条件值)为Null时,表示是否参与过滤。当该选项值是INCLUDE时,表示仍参与过滤,会匹配数据库表中该字段值是Null的记录;若为IGNORE值,表示不参与过滤。
  2. defaultStringMatcher:默认字符串匹配方式,枚举类型,有6个可选值:DEFAULT(默认,效果同EXACT)EXACT(相等)STARTING(开始匹配)ENDING(结束匹配)CONTAINING(包含,模糊匹配)REGEX(正则表达式)该配置对所有字符串属性过滤有效,除非该属性在propertySpecifiers中单独定义自己的匹配方式。
  3. defaultIgnoreCase:默认大小写忽略方式,布尔型,当值为false时,即不忽略,大小不相等。该配置对所有字符串属性过滤有效,除非该属性在propertySpecifiers中单独定义自己的忽略大小写方式。
  4. propertySpecifiers:各属性特定查询方式,描述了各个属性单独定义的查询方式,每个查询方式中包含4个元素:属性名、字符串匹配方式、大小写忽略方式、属性转换器。如果属性未单独定义查询方式,或单独查询方式中,某个元素未定义(如:字符串匹配方式),则采用ExampleMatcher中定义的默认值,即上面介绍的defaultStringMatcher和defaultIgnoreCase的值。
  5. ignoredPaths:忽略属性列表,忽略的属性不参与查询过滤。

字符串匹配举例:

字符串匹配方式

对应的JPQL写法

Default&不忽略大小写

firstname=?1

Exact&忽略大小写

LOWER(firstname)=LOWER(?1)

Staring&忽略大小写

Lower(firstname) like lower(?1)+'%'

Ending&不忽略大小写

firstname like '%'+?1

Containing不忽略大小写

firstname like '%'+?1+'%'

3.14 JpaSpecificationExecutor的详细使用

JpaSpecificationExecutor是JPA 2.0提供的Criteria API,可以用于动态生成query。Spring Data JPA支持Criteria查询,可以很方便地使用,足以应付工作中的所有复杂查询的情况了,可以对JPA实现最大限度的扩展。

3.14.1 JpaSpecificationExecutor的方法

public interface JpaSpecificationExecutor {
    //根据Specification条件查询单个对象
    Optional findOne(@Nullable Specification spec);
    //根据Specification条件查询多个对象
    List findAll(@Nullable Specification spec);
    //根据Specification条件分页查询多个对象
    Page findAll(@Nullable Specification spec, Pageable pageable);
    //根据Specification条件查询多个对象并排序
    List findAll(@Nullable Specification spec, Sort sort);
    ////根据Specification条件分页查询对象数量
    long count(@Nullable Specification spec);
}

这个接口基本是围绕着Specification接口来定义的,Specification接口中只定义了如下一个方法:

public interface Specification extends Serializable {
    @Nullable
    Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder criteriaBuilder);
}

所以可看出,JpaSpecificationExecutor是针对Criteria API进行了predicate标准封装,帮我们封装了通过EntityManager的查询和使用细节,操作Criteria更加便利了一些。

3.14.2 Criteria简介

  1. Rootroot:代表了可以查询和操作的实体对象的根。如果将实体对象比喻成表名,那root里面就是这张表里面的字段。这不过是JPQL的实体字段而已。通过里面的Pathget(String attributeName)来获得我们操作的字段。
  2. CriteriaQueryquery:代表一个specific的顶层查询对象,它包含着查询的各个部分,比如:select、from、where、group by、order by等。CriteriaQuery对象只对实体类型或嵌入式类型的Criteria查询起作用,简单理解,它提供了查询ROOT的方法。常用的方法有:
public interface CriteriaQuery extends AbstractQuery {
     
    CriteriaQuery select(Selection selection);

    CriteriaQuery where(Expression restriction);

    CriteriaQuery where(Predicate... restrictions);

    CriteriaQuery groupBy(Expression... grouping);

    CriteriaQuery groupBy(List> grouping);

    CriteriaQuery having(Expression restriction);

    CriteriaQuery having(Predicate... restrictions);

    CriteriaQuery orderBy(Order... o);

    CriteriaQuery orderBy(List o);

    CriteriaQuery distinct(boolean distinct);

}
  1. CriteriaBuilder cb:用来构建CritiaQuery的构建器对象,其实就相当于条件或者是条件组合,以谓语即Predicate的形式返回。构建简单的Predicate示例:
Predicate predicate1 = cb.equal(join.get("id"),1);
Predicate predicate2 = cb.like(join.get("departmentName"),"技术部");

构建组合的Predicate示例

cb.and(predicate1,predicate2);
cb.or(predicate1,predicate2);

其实JpaSpecificationExecutor帮我们提供了一个高级的入口和结构,通过这个入口,可以使用底层JPA的Criteria的所有方法,其实就可以满足了所有业务场景。但实际工作中,需要注意的是,如果一旦我们写的实现逻辑太复杂,第二个人一般看不懂的时候,那一定是有问题的,我们要寻找更简单的,更易懂的,更优雅的方式。

3.14.3 JpaSpecificationExecutor示例

以3.11.3小节中部门表和员工表为例,演示JpaSpecificationExecutor使用。

@Data
@Entity
@Table(name = "employee")
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer employeeId;
    @Column(name = "name")
    private String name;
    @Column(name = "age")
    private Integer age;
    private Integer departmentId;
}
@Data
@Entity
@Table(name = "department")
public class Department {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    @Column(name = "department_name")
    private String departmentName;
    @Column(name = "create_time")
    @UpdateTimestamp
    private Date createTime;
    @OneToMany(fetch = FetchType.EAGER)
    @JoinColumn(name = "departmentId")
    private List employees;
}

DepartmentRepository接口需要继承JpaSpecificationExecutor

@Repository
public interface DepartmentRepository extends JpaRepository, JpaSpecificationExecutor {
}

测试类中添加如下代码:

@SpringBootTest
class Chapter03ApplicationTests {
    @Autowired
    private DepartmentRepository departmentRepository;
    @Test
    void contextLoads() {
        Specification specification = (root,query,cb)->{
            //cb用来构造查询条件,如where,like,and,or等
            Root employeeRoot = query.from(Employee.class);
            //连接表,第一个参数是封装到实体中的实体,即连接表对应的实体对象名
            //第二个参数是连接类型,如果不写则默认使用内链接
            Join join = root.join("employees", JoinType.LEFT);

            return join.getOn();
        };
        List list = departmentRepository.findAll(specification);
        list.forEach(System.out::println);
    }
}

3.15 审计功能

相关推荐

JavaScript中常用数据类型,你知道几个?

本文首发自「慕课网」,想了解更多IT干货内容,程序员圈内热闻,欢迎关注!作者|慕课网精英讲师Lison这篇文章我们了解一下JavaScript中现有的八个数据类型,当然这并不是JavaScr...

踩坑:前端的z-index 之bug一二(zh1es前端)

IE6下浮动元素bug给IE6下的一个div设置元素样式,无论z-index设置多高都不起作用。这种情况发生的条件有三个:1.父标签position属性为relative;2.问题标签无posi...

两栏布局、左边定宽200px、右边自适应如何实现?

一、两栏布局(左定宽,右自动)1.float+margin即固定宽度元素设置float属性为left,自适应元素设置margin属性,margin-left应>=定宽元素宽度。举例:HTM...

前端代码需要这样优化才是一个标准的网站

  网站由前端和后端组成,前端呈现给用户。本文将告诉您前端页面代码的优化,当然仍然是基于seo优化的。  就前端而言,如果做伪静态处理,基本上是普通的html代码,正常情况下,这些页面内容是通过页面模...

网页设计如何自学(初学网页设计)

1在Dreamweaver中搭建不同的页面,需要掌握HTML的语句了,通过调整各项数值就可以制作出排版漂亮的页面,跟着就可以学习一些可视化设计软件。下面介绍网页设计如何自学,希望可以帮助到各位。Dre...

1、数值类型(数值类型有)

1.1数据类型概览MySQL的数据类型可划分为三大类别:数值类型:旨在存储数字(涵盖整型、浮点型、DECIMAL等)。字符串类型:主要用于存储文本(诸如CHAR、VARCHAR之类)。日期/...

网页设计的布局属性(网页设计的布局属性是什么)

布局属性是网站设计中必不可少的一个重要的环节,主要用来设置网页的元素的布局,主要有以下属性。1、float:该属性设置元素的浮动方式,可以取none,left和right等3个值,分别表示不浮动,浮在...

Grid网格布局一种更灵活、更强大的二维布局模型!

当涉及到网页布局时,display:flex;和display:grid;是两个常用的CSS属性,它们都允许创建不同类型的布局,但有着不同的用法和适用场景。使用flex布局的痛点当我们使...

React 项目实践——创建一个聊天机器人

作者:FredrikStrandOseberg转发链接:https://www.freecodecamp.org/news/how-to-build-a-chatbot-with-react/前言...

有趣的 CSS 数学函数(css公式)

前言之前一直在玩three.js,接触了很多数学函数,用它们创造过很多特效。于是我思考:能否在CSS中也用上这些数学函数,但发现CSS目前还没有,据说以后的新规范会纳入,估计也要等很久。然...

web开发之-前端css(5)(css前端设计)

显示控制一个元素的显示方式,我们可以使用display:block;display:inline-block;display:none;其中布局相关的还有两个很重要的属性:display:flex;和...

2024最新升级–前端内功修炼 5大主流布局系统进阶(分享)

获课:keyouit.xyz/14642/1.前端布局的重要性及发展历程前端布局是网页设计和开发的核心技能之一,它决定了页面元素如何组织和呈现。从早期的静态布局到现代的响应式布局,前端布局技术经历了...

教你轻松制作自动换行的CSS布局,轻松应对不同设备!

在网页设计中,自动换行的CSS布局是非常常见的需求,特别是在响应式设计中。它可以让网页内容自动适应不同屏幕尺寸,保证用户在不同设备上都能够获得良好的浏览体验。本文将介绍几种制作自动换行的CSS布局的方...

晨光微语!一道 CSS 面试题,伴你静享知识治愈时光

当第一缕阳光温柔地爬上窗台,窗外的鸟鸣声清脆悦耳,空气中弥漫着清新的气息。在这宁静美好的清晨与上午时光,泡一杯热气腾腾的咖啡,找一个舒适的角落坐下。前端的小伙伴们,先把工作的疲惫和面试的焦虑放在一边,...

2023 年的响应式设计指南(什么是响应式设计优缺点)

大家好,我是Echa。如今,当大家考虑构建流畅的布局时,没有再写固定宽度和高度数值了。相反,小编今天构建的布局需要适用于几乎任何尺寸的设备。是不是不可思议,小编仍然看到网站遵循自适应设计模式,其中它有...