美文网首页
SpringBoot2(五):Spring Data JPA 使

SpringBoot2(五):Spring Data JPA 使

作者: 帅气滴糟老头 | 来源:发表于2019-01-26 02:02 被阅读0次

    前言

    Spring Data JPA是Spring Data的一个子项目,通过提供基于JPA的Repository极大的减少了JPA作为数据访问方案的代码量,你仅仅需要编写一个接口集成下SpringDataJPA内部定义的接口即可完成简单的CRUD操作。
    本文从构建项目到对JPA的详细使用,争取能够尽量全的演示JPA的相关应用,大体内容如下:

    • JPA环境搭建、配置
    • 表关系配置演示:多对多、多对一、一对多
    • 基本CRUD操作
    • JPA实体对象的4种状态详解
    • Example查询
    • 接口规范方法名查询
    • @Query注解使用
    • Criteria查询
    • 性能问题解决(循环引用、N+1查询)

    一、构建项目

    引入依赖

    新建springboot项目,在pom文件中引入jpa的相关依赖,如下:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    

    另外我们在引入另外的web及数据库连接相关的依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- alibaba的druid数据库连接池 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>
    
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    

    数据源及jpa配置

    在application.yml文件中加入如下配置:

    server:
      port: 8080
    
    spring:
      datasource:
        name: mysql_test
        #基本属性
        url: jdbc:mysql://localhost:3306/test?useSSL=false&useUnicode=true&characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=TRUE&serverTimezone=UTC&allowMultiQueries=true
        username: root
        password: root
        type: com.alibaba.druid.pool.DruidDataSource
        #druid相关配置
        druid:
          #监控统计拦截的filters
          filters: stat
          #配置初始化大小/最小/最大
          initial-size: 1
          min-idle: 1
          max-active: 20
          #获取连接等待超时时间
          max-wait: 60000
          #间隔多久进行一次检测,检测需要关闭的空闲连接
          time-between-eviction-runs-millis: 60000
          #一个连接在池中最小生存的时间
          min-evictable-idle-time-millis: 300000
          validation-query: SELECT 'x'
          test-while-idle: true
          test-on-borrow: false
          test-on-return: false
          #打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false
          pool-prepared-statements: false
          max-pool-prepared-statement-per-connection-size: 20
    
      jpa:
        show-sql: true
        # 指定生成表名的存储引擎为InnoDBD
        database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
        hibernate:
          # 自动创建|更新|验证数据库表结构配置
          ddl-auto: update
    
      jackson:
        date-format: yyyy-MM-dd HH:mm:ss
    
    logging:
      level:
        com.along: debug
    

    jpa相关的配置跟简单,这里特别要说明spring.jpa.hibernate.ddl-auto这个配置,该配置有四个可选值,下面是详细说明:

    • create:每次运行该程序,没有表格会新建表格,表内有数据会清空
    • create-drop:每次程序结束的时候会清空表
    • update:每次运行程序,没有表格会新建表格,表内有数据不会清空,只会更新
    • validate:运行程序会校验数据与数据库的字段类型是否相同,不同会报错
      线上环境我们validate,开发环境一般用update

    定义实体类

    为了后面演示多表关系操作,这里设计了三张表,分别是用户表(User)、权限表(Role)和文章表(Article),用户与权限是多对多关系,用户与文章是一对多关系。

    为了代码的简洁,我们先创建一个基类BaseData,在这个类里写每个表的共了有字段,继承Serializable、主键id、创建时间、更新时间,后面三个表都继承这个类,内容如下:

    @MappedSuperclass
    public abstract class BaseData implements Serializable {
    
        private static final long serialVersionUID = -3013776712039356819L;
    
        @Id
        @GeneratedValue(generator = "uuid")
        @GenericGenerator(name = "uuid", strategy = "uuid2")
        private String id;
    
        @Temporal(TemporalType.TIMESTAMP)
        private Date createTime;
    
        @Temporal(javax.persistence.TemporalType.TIMESTAMP)
        private Date updateTime;
    
        @PrePersist
        void createdAt() {
            this.createTime = this.updateTime = new Date();
        }
    
        @PreUpdate
        void updatedAt() {
            this.updateTime = new Date();
        }
    
        // getter setter...
    }
    

    下面定义用户表(User.class)

    package com.along.model.entity;
    
    
    import com.fasterxml.jackson.annotation.JsonFormat;
    import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
    
    import javax.persistence.*;
    import java.util.Date;
    import java.util.List;
    import java.util.Set;
    
    /**
     * @Description: 用户实体类
     * @Author along
     * @Date 2019/1/8 16:50
     */
    @Entity
    @Table(name = "user") //对应数据库中的表名
    public class User extends BaseData {
    
        private static final long serialVersionUID = -5103936306962248929L;
    
        private String name;
    
        private String password;
    
        private Integer sex; // 1:男;0:女
    
        private Integer status = 1; //-1:删除;0 禁用 1启用
    
        private String email;
    
        @Temporal(TemporalType.DATE)
        @JsonFormat(pattern = "yyyy-MM-dd")
        private Date birthday;
    
        /**
         * 一对多配置演示
         * 级联保存、更新、删除、刷新;延迟加载。当删除用户,会级联删除该用户的所有文章
         * 拥有mappedBy注解的实体类为关系被维护端
         * mappedBy="user"中的user是Article中的user属性
         */
        @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
        private List<Article> articleList; // 文章
    
        /**
         * 多对多配置演示
         */
        @ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.LAZY)
        @JoinTable(
                name = "user_role", // 定义中间表的名称
                joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, // 定义中间表中关联User表的外键名
                inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")} // 定义中间表中关联role表的外键名
        )
        private Set<Role> roles; // 角色外键
    
        // getter and setter...
    

    配置说明:这里采用实实体类自动生成数据库表,字段名会和数据库字列名一样,这样就可以省略@Column(name = "")注解,默认每个字段都是可以为空的,如果需要不能为空,就加@Column(nullable = false)注解。同时上面代码改做了多对多和一对多的配置,有详细的注解说明。

    下面定义权限表(Role.class)

    package com.along.model.entity;
    
    import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
    
    import javax.persistence.*;
    import java.util.Set;
    
    /**
     * @Description: 角色实体类
     * @Author along
     * @Date 2019/1/8 16:56
     */
    @Entity
    @Table(name = "role")
    public class Role extends BaseData {
    
        private static final long serialVersionUID = 5012235295240129244L;
    
        private String roleName; // 角色名
    
        private Integer roleType; // 1: 超级管理员   2: 系统管理员 3:一般用户
    
        private Integer state; // 0禁用  1 启用
    
        @ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.LAZY)
        @JoinTable(
                name = "user_role",
                joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")},
                inverseJoinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}
        )
        private Set<User> users; // 与用户多对多
    
    
        // getter and setter ...
    }
    

    下面是文章表(Article.class)

    package com.along.model.entity;
    
    import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
    
    import javax.persistence.*;
    import javax.validation.constraints.NotEmpty;
    import javax.validation.constraints.Size;
    
    /**
     * @Description: 文章实体类
     * @Author along
     * @Date 2019/1/8 17:38
     */
    @Entity
    @Table(name = "article")
    public class Article extends BaseData{
    
        private static final long serialVersionUID = -4817984675096952797L;
    
        @NotEmpty(message = "标题不能为空")
        @Column(nullable = false, length = 50)
        private String title;
    
        @Lob // 大对象,映射 MySQL 的 Long Text 类型
        @Basic(fetch = FetchType.LAZY) // 懒加载
        @Column(nullable = false) // 映射为字段,值不能为空
        private String content; // 文章全文内容
    
        /**
         * 多对一配置演示:
         * 可选属性optional=false,表示sysUser不能为空
         * 配置了级联更新(合并)和刷新,删除文章,不影响用户
         */
        @ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH}, optional = false)
        @JoinColumn(name = "user_id") // 设置在article表中的关联字段(外键)名
        private User user; // 所属用户
    
    
        // getter and setter ...
    }
    

    写到这里我们就可以启动项目了,运行启动类就能在数据库中生成和实体类对应的表。

    DAO层编写,使用JpaRepository接口

    我们创建Dao接口继承JpaRepository接口,JpaRepository需要泛型接口参数,第一个参数是实体,第二则是主键的类型,也可以是Serializable。下面只给出UserDao的代码,剩下两个类似

    /**
     * @Description: 用户表dao
     * @Author along
     * @Date 2019/1/9 14:07
     */
    public interface UserDao extends JpaRepository<User, String> {
    
    }
    

    继承了JpaRepository后会自动被spring注册成为bean,这样用户表的dao层就编写好了,可以进行基本的crud操作。

    JpaRepository为我们做了什么?来看下JpaRepository的源码

    @NoRepositoryBean
    public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
    
        List<T> findAll(); // 查询所有
    
        List<T> findAll(Sort sort); // 查询所有,带排序 
    
        List<T> findAllById(Iterable<ID> ids); // 根据id列表查询
    
        <S extends T> List<S> saveAll(Iterable<S> entities); // 批量保存
    
        void flush(); // 立即写入数据库,正常情况下在事务提交的时候,JPA会自动执行flush()一次性保存所有数据。
    
        <S extends T> S saveAndFlush(S entity); // 插入数据并且立即将更改写入数据库
    
        void deleteInBatch(Iterable<T> entities); // 批量删除
    
        void deleteAllInBatch(); // 删除批量调用中的所有实体(清空表)
    
        T getOne(ID id); // 根据id得到一个对象
    
        @Override
        <S extends T> List<S> findAll(Example<S> example); // 实例查询
    
        @Override
        <S extends T> List<S> findAll(Example<S> example, Sort sort); // 实例查询,排序
    }
    

    可以看到JpaRepository实现了基本的crud操作,JpaRepository同时继承了PagingAndSortingRepository和QueryByExampleExecutor接口
    PagingAndSortingRepository接口包含了全表查询时的分页查询和排序,源码如下:

    @NoRepositoryBean
    public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
    
        Iterable<T> findAll(Sort sort); // 查询多有,带排序
    
        Page<T> findAll(Pageable pageable); // 分页查询多有
    }
    

    QueryByExampleExecutor接口是用来做复杂查询的,十分好用,会在下文详细介绍,下面是该接口的源码:

    public interface QueryByExampleExecutor<T> {
    
        <S extends T> Optional<S> findOne(Example<S> example); // 根据实例查询一个实体
    
        <S extends T> Iterable<S> findAll(Example<S> example); // 查询所有符合给定实例的实体
    
        <S extends T> Iterable<S> findAll(Example<S> example, Sort sort); // 查询所有符合给定实例的实体,带排序
    
        <S extends T> Page<S> findAll(Example<S> example, Pageable pageable); // 分页查询所有符合给定实例的实体
    
        <S extends T> long count(Example<S> example); // 得到符合给定实例的数量
    
        <S extends T> boolean exists(Example<S> example); // 判断是否存在
    }
    

    现在我们对继承了JpaRepository的UserDao可以做到多少数据库操作已经有了大概的认识。


    二、JPA的使用

    1. 基本CRUD操作

    下面我们编写UserService来演示基本的crud操作,代码如下:

    @Service(value = "userService")
    @Transactional
    public class UserService {
    
        private UserDao userDao;
    
        @Autowired
        public UserServiceImpl(UserDao userDao) {
            this.userDao = userDao;
        }
    
        /**
         * 保存
         */
        public User save(User user) {
    
            return userDao.save(user);
        }
    
        /**
         * 批量添加
         */
        public List<User> saveAll(List<User> list) {
           
            return userDao.saveAll(list);
        }
    
        /**
         * 分页查询所有,带排序功能
         */
        public Page<User> findAll() {
            //分页+排序查询演示:
            //Pageable pageable = new PageRequest(page, size);//2.0版本后,该方法以过时
            Sort sort = new Sort(Sort.Direction.DESC, "updateTime","createTime");
            Pageable pageable = PageRequest.of(page, size, sort);
            Page<User> users = userService.findAll(pageable);
            return userDao.findAll(pageable);
        }
    
        /**
         * 更新
         */
        public Boolean update(User user) {
            Optional<User> u = userDao.findById(user.getId());
            if (u.isPresent()) {
                User oldUser = u.get();
                oldUser.setName(user.getName());
                oldUser.setRoles(user.getRoles());
                oldUser.setBirthday(user.getBirthday());
                oldUser.setEmail(user.getEmail());
                oldUser.setUpdateTime(new Date());
                userDao.save(oldUser);
                return Boolean.TRUE;
            }
            return Boolean.FALSE;
        }
    
        /**
         * 删除
         */
        @Override
        public void delete(String id) {
            userDao.deleteById(id);
        }
    }
    

    上面代码举例了简单的几个crud操作,这里要对分页查询和更新操作做特别的说明:

    分页查询:

    分页查询的关键在于创建Pageable对象,一般通过实现类PageRequest创建,早先的版本我们同伙new的方式创建Pageable对象,如下

    Pageable pageable = new PageRequest(page, size)
    

    但是在2.0版本后该方法已经过时,我们转而使用PageRequest的of方法创建Pageable实例,下面是源码片段:

        /**
         * 不带排序
         */
        public static PageRequest of(int page, int size) {
            return of(page, size, Sort.unsorted());
        }
    
        /**
         * 带排序
         */
        public static PageRequest of(int page, int size, Sort sort) {
            return new PageRequest(page, size, sort);
        }
    
        /**
         * 带排序信息
         */
        public static PageRequest of(int page, int size, Direction direction, String... properties) {
            return of(page, size, Sort.by(direction, properties));
        }
    

    下面是几个模拟场景举例:

    • 第1页每页显示20条
    Pageable pageable = PageRequest.of(0, 20);
    
    • 第1页显示20条,倒序排序,按创建时间字段排序,如果创建时间相同,按更新时间排序

    方式一:构建Sort对象方式创建

    Sort sort = new Sort(Sort.Direction.DESC, "updateTime","createTime");
    Pageable pageable = PageRequest.of(page, size, sort);
    

    方式二:直接传入排序信息

    Pageable pageable = 
              PageRequest.of(page, size, Sort.Direction.DESC, "updateTime","createTime");
    
    更新:

    JpaRepository并没有提供专门的update方法,而是将更新操作放在save中完成了,下面是save方法的源码实现:

    @Transactional
    public <S extends T> S save(S entity) {
    
        if (entityInformation.isNew(entity)) {
            em.persist(entity);
            return entity;
        } else {
            return em.merge(entity);
        }
    }
    

    我们看到调用save方法传入一个实例,首先会通过entityInformation.isNew(entity)来判断该实体是否是一个新的对象,具体的是先判断有无id,如果有就通过id在数据库中查找是否存在对应的数据,如果存在就是更新操作,会调用EntityManagermerge()方法执行更新,如果不存在就说明是插入操作,会调用EntityManagerpersist()方法执行插入。

    EntityManager管理器
    通过源码我们可以看到save方法实质上是调用的EntityManager的方法完成的数据库操作,所以这里有必要介绍下EntityManager接口,在此之前得了解jpa中实体对象拥有的四种状态:

    • 瞬时状态(new/transient):没有主键,不与持久化上下文关联,即 new 出的对象(但不能指定id的值,若指定则是游离态而非瞬时态)
    • 托管状态(persistent):使用EntityManager进行find或者persist操作返回的对象即处于托管状态,此时该对象已经处于持久化上下文中(被EntityManager监控),任何对该实体的修改都会在提交事务时同步到数据库中。
    • 游离状态(detached):有主键,但是没有跟持久化上下文关联的实体对象。
    • 删除状态 (deleted):当调用EntityManger对实体进行remove后,该实体对象就处于删除状态。其本质也就是一个瞬时状态的对象。

    下面的图清晰的表示了各个状态间的转化关系:

    下面介绍下EntityManager接口的几个常用方法:

    • persist():将临时状态(无主键)的对象转化为托管状态。由于涉及数据库增删改,执行该语句前需启用事务
    entityManager.persist(modelObject);
    
    • merge():将游离状态(有主键)的对象转化为托管托管状态,不同于persist(),merger()对于操作的对象,如果对象存在于数据库则对对象进行修改,如果对象在数据库中不存在,则将该对象作为一条新记录插入数据库。
    entityManager.merge(modelObject);
    
    • find()与getReference():从数据库中查找对象。不同点:当对象不存在时,find()会返回null,getReference()则会抛出javax.persistence.EntityNotFoundException异常。
    // 参数一:实体类的class,参数二:实体主键值
    entityManager.find(Class<T> ModelObject.class , int key);
    
    • remove():将托管状态的对象转化为删除状态。由于涉及数据库增删改,执行该语句前需启用事务
    entityManager.remove(entityManager.getReference(ModelObject.class, key));
    
    • refresh(Object obj):重新从数据库中读取数据。可以保证当前的实例与数据库中的实例的内容一致。该方法用来操作托管状态的对象。
    • contains(Object obj):判断对象在持久化上下文(不是数据库)中是否存在,返回true/false。
    • flush():立即将对托管状态对象所做的修改(包括删除)写入数据库。
      从上面内容我们发现通过EntityManager对实体对象所做的操作实质是让对象在不同的状态间转换,而这些修改是在执行flush()后才会真正的写入数据库。正常情况下不需要手动执行flash(),在事务提交的时候,JPA会自动执行flush()一次性保存所有数据。
      如果要立即保存修改,可以手动执行flush()。
      同时我们可以通过setFlushModel()方法修改EntityManager的刷新模式。默认为AUTO,这种模式下,会在执行查询(指使用JPQL语句查询前,不包括find()和getReference()查询)前或事务提交时自动执行flush()。通过entityManager.setFlushMode(FlushModeType.COMMIT)设置为COMMIT模式,该模式下只有在事务提交时才会执行flush()。
    • clear():把实体管理器中所有的实体对象(托管状态)变成游离状态,clear()之后,对实体类所做的修改也会丢失。

    现在我们再回到更新方法,为了方便查看,我们摘取出上面写好的更新方法实现

    public Boolean update(User user) {
        Optional<User> u = userDao.findById(user.getId());
        if (u.isPresent()) {
            User oldUser = u.get();
            oldUser.setName(user.getName());
            oldUser.setRoles(user.getRoles());
            oldUser.setBirthday(user.getBirthday());
            oldUser.setEmail(user.getEmail());
            oldUser.setUpdateTime(new Date());
            userDao.save(oldUser);
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }
    

    如果你已经理解了上文中的JPA的四种状态,那你应该就能看出这段代码存在的问题,oldUser是从数据库中查出来的,是托管状态对象,受EntityManager管理,我们后面对该对象所做的修改会在事务提交时自动调用flush()将修改写入数据库完成更新,所以并不需要再调用save()方法执行更新,这样显得多此一举。当然还要注意这样实现更新需要在方法上加@Transactional启动事务。

    下面是修改后的代码实现:

    @Transactional
    public Boolean update(User user) {
        Optional<User> u = userDao.findById(user.getId());
        if (u.isPresent()) {
            User oldUser = u.get();
            oldUser.setName(user.getName());
            oldUser.setRoles(user.getRoles());
            oldUser.setBirthday(user.getBirthday());
            oldUser.setEmail(user.getEmail());
            oldUser.setUpdateTime(new Date());
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }
    

    2. Example查询

    上文中说到JpaRepository继承了QueryByExampleExecutor接口,利用该接口可以实现相对复杂的实例查询,下面再来看看QueryByExampleExecutor接口的源码:

    public interface QueryByExampleExecutor<T> {
    
        <S extends T> Optional<S> findOne(Example<S> example); // 根据实例查询一个实体
    
        <S extends T> Iterable<S> findAll(Example<S> example); // 查询所有符合给定实例的实体
    
        <S extends T> Iterable<S> findAll(Example<S> example, Sort sort); // 查询所有符合给定实例的实体,带排序
    
        <S extends T> Page<S> findAll(Example<S> example, Pageable pageable); // 分页查询所有符合给定实例的实体
    
        <S extends T> long count(Example<S> example); // 得到符合给定实例的数量
    
        <S extends T> boolean exists(Example<S> example); // 判断是否存在
    }
    

    可以看到所有api都需要传入Example对象,下面是Example api的组成:

    • Probe:实体对象,比如我们要查询User表,那User对象就是可以作为Probe。
    • ExampleMatcher:匹配器,用来详细描述实体对象中的内容的查询方式,如规定某个属性为模糊查询。
    • Example:实例,代表的是完整的查询条件,由Probe和ExampleMatcher共同创建。

    写一个简单的测试方法来感受下实例查询
    模拟需求:查询姓名为along和性别为男的用户

    @Autowired
    private UserDao userDao;
    
    @Test
    public void test() {
        // 创建查询条件数据对象
        User user = new User();
        user.setName("along");
        user.setSex(1);
    
        // 创建实例
        Example<User> example = Example.of(user);
        // 查询
        List<User> users = userDao.findAll(example);
    
        //输出结果
        System.out.println("数量:" + users.size());
        for (User u : users) {
            System.out.println(u);
        }
    }
    

    先创建实体对象user,因为我们是根据姓名和性别查询,所以为user对象的姓名和性别属性复制内容,然后同通过Example.of()方法创建Example实例最后执行查询。这里我们调用Example.of()只传入了一个实体对象user,这样jpa会在处理时默认传入一个默认的ExampleMatcher
    下面是默认ExampleMatcher规定的查询方式:

    • 忽略空值,只将实体对象中中不为空的字段作为查询条件
    • 所有查询条件都采用精确匹配
    • 查询条件大小写敏感

    运行测试,控制台输出如下内容:

    Hibernate: select 
                  user0_.id as id1_2_, 
                  user0_.create_time as create_t2_2_, 
                  user0_.update_time as update_t3_2_, 
                  user0_.birthday as birthday4_2_, 
                  user0_.email as email5_2_, 
                  user0_.name as name6_2_, 
                  user0_.password as password7_2_, 
                  user0_.sex as sex8_2_, 
                  user0_.status as status9_2_ 
                from 
                      user user0_ 
                where 
                      user0_.sex=1 and user0_.name=? and user0_.status=1
    数量:4
    along
    along
    along
    along
    

    成功查询出了4条数据,我们接下来观察打印出来的sql,查询条件正是name、sex和status,而且都是精确查询,这时候发现多了一个查询条件status,因为实例查询默认会将实例中不为空的内容作为查询条件,而我们在定义User实体类的时候为status属性设了默认值1。
    如果我们需要查询条件只有name和sex,这时候就要定义ExampleMatcher来忽略status属性。我们修改测试代码如下

    @Autowired
    private UserDao userDao;
    
    @Test
    public void test() {
        // 创建查询条件数据对象
        User user = new User();
        user.setName("along");
        user.setSex(1);
    
        // 创建匹配器,即规定如何使用查询条件
        ExampleMatcher matcher = ExampleMatcher.matching() // 构建对象
                .withIgnorePaths("status"); // 忽略status属性
    
        // 创建实例
        Example<User> example = Example.of(user, matcher);
        // 查询
        List<User> users = userDao.findAll(example);
    
        //输出结果
        System.out.println("数量:" + users.size());
        for (User u : users) {
            System.out.println(u.getName());
        }
    
    }
    

    运行测试,控制带输出sql如下,可以看到查询条件已经没有status了

    select 
        user0_.id as id1_2_, 
        user0_.create_time as create_t2_2_, 
        user0_.update_time as update_t3_2_, 
        user0_.birthday as birthday4_2_, 
        user0_.email as email5_2_, 
        user0_.name as name6_2_, 
        user0_.password as password7_2_, 
        user0_.sex as sex8_2_, 
        user0_.status as status9_2_ 
    from
        user user0_ 
    where 
        user0_.sex=1 and user0_.name=?
    

    理解ExampleMatcher
    我们可以通过下面代码创建一个默认的ExampleMatcher对象

    ExampleMatcher matcher = ExampleMatcher.matching();
    

    下面是ExampleMatcher的实现类的部分源码,展示了 ExampleMatcher的六个配置项

    class TypedExampleMatcher implements ExampleMatcher {
        private final NullHandler nullHandler; // Null值处理方式
        private final StringMatcher defaultStringMatcher; // 默认字符串匹配方式
        private final PropertySpecifiers propertySpecifiers; // 各个属性特定查询方式
        private final Set<String> ignoredPaths; // 忽略属性列表
        private final boolean defaultIgnoreCase; // 默认大小写忽略方式
        private final MatchMode mode; // 默认为all,目前没看出有什么特别的作用
    
        /**
         * 空参构造,为上面的属性设置默认值
         * null处理方式为忽略
         * 默认字符串匹配方式为default(精确匹配)
         * 各属性特定查询方式默认为空
         * 忽略属性列表默认为空列表
         * 默认大小写忽略方式为false,不忽略
         * MatchMode默认为all
         */
        TypedExampleMatcher() {
            this(NullHandler.IGNORE, StringMatcher.DEFAULT, new PropertySpecifiers(), Collections.emptySet(), false, MatchMode.ALL);
        }
        ......
    }
    

    从源码我们可以看出ExampleMatcher的默认配置如下:

    • nullHandler:IGNORE。Null值处理方式:忽略
    • defaultStringMatcher:DEFAULT。默认字符串匹配方式:默认(相等)
    • defaultIgnoreCase:false。默认大小写忽略方式:不忽略
    • propertySpecifiers:空。各属性特定查询方式,空。
    • ignoredPaths:空列表。忽略属性列表,空列表。

    但是只创建一个默认的ExampleMatcher没有什么意义,上文说到就算你创建Example实例时不传ExampleMatcher对象,jpa也会自动加上默认的ExampleMatcher。

    下面对每个配置项进行详细讲解

    • nullHandler:null值处理方式,枚举类型,两个可选值:INCLUDE(包括)IGNORE(忽略),默认为IGNORE
      通过下面代码改变默认的null值处理方式:
    // 默认就是忽略,所以再设置为忽略就没有意义了,下面设置为不忽略
    ExampleMatcher matcher = ExampleMatcher.matching()
                    .withNullHandler(ExampleMatcher.NullHandler.INCLUDE) // 方式一
                    .withIncludeNullValues(); // 方式二
    
    • defaultStringMatcher:默认字符串匹配方式,枚举类型,六个可选值:DEFAULT(默认,效果同EXACT)EXACT(精确匹配,即 = )STARTING(开始匹配,即 ?% )ENDING(结束匹配,即 %? )CONTAINING(包含,模糊匹配,即 %?% )REGEX(正则表达式匹配)
      下面是改变默认字符串匹配方式的代码示例:
    ExampleMatcher matcher = ExampleMatcher.matching()
                    //下面只用配置一个
                    .withStringMatcher(ExampleMatcher.StringMatcher.STARTING) // 开始匹配 %?
                    .withStringMatcher(ExampleMatcher.StringMatcher.ENDING) // 结束匹配 ?%
                    .withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING) // 包含,模糊匹配 %?%
                    .withStringMatcher(ExampleMatcher.StringMatcher.REGEX); // 正则匹配
    
    • propertySpecifiers:各属性特定查询方式,描述了各个属性单独定义的查询方式,每个查询方式中包含4个元素:属性名、字符串匹配方式、大小写忽略方式、属性转换器。如果属性未单独定义查询方式,或单独查询方式中,某个元素未定义(如:字符串匹配方式),则采用 ExampleMatcher 中定义的默认值,即上面介绍的 defaultStringMatcher 和 defaultIgnoreCase 的值。
      一个属性的特定查询方式,包含了3个信息:字符串匹配方式、大小写忽略方式、属性转换器,存储在 propertySpecifiers 中,操作时用 GenericPropertyMatcher 类来传递配置信息。
      下面是该属性配置的代码示例:
    ExampleMatcher matcher = ExampleMatcher.matching()
                    //方式一:单独设置name字段为模糊查询方式
                    .withMatcher("name", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING))
                    //方式二:设置name字段为模糊查询,忽略大小写
                    .withMatcher("name", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING, true))
                    //方式三(推荐):链式设置
                    .withMatcher("name", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase());
    
    • ignoredPaths:忽略属性列表,忽略的属性不参与查询过滤。
      下面是添加忽略属性的代码示例:
    ExampleMatcher matcher = ExampleMatcher.matching()
                    .withIgnorePaths("status", "sex");
    
    • defaultIgnoreCase:默认大小写忽略方式,布尔型,当值为false时,即不忽略,大小不相等。该配置对所有字符串属性过滤有效,除非该属性在 propertySpecifiers 中单独定义自己的忽略大小写方式。
      下面是改变默认大小写忽略方式的代码示例:
    ExampleMatcher matcher = ExampleMatcher.matching()
                    // 下面两个等价
                    .withIgnoreCase()
                    .withIgnoreCase(true)
                    // 单独为name属性设置忽略大小写,可设置多个值
                    .withIgnoreCase("name")
    

    实例查询的限制

    • 不支持过滤条件分组。即不支持过滤条件用 or(或) 来连接,所有的过滤查件,都是简单一层的用 and(并且) 连接。
    • 灵活匹配只支持字符串类型,其他类型只支持精确匹配

    参考文章:https://blog.csdn.net/zhao_tuo/article/details/78604324

    3. 接口规范方法名查询(最让我惊喜的查询方式)

    根据可读性极强的方法名就能创建查询,初次接触时会让你觉得很不可思议

    说明:按照Spring data 定义的规则,查询方法以find|read|get开头,涉及条件查询时,条件的属性用条件关键字连接,整个查询方法名按驼峰式命名。

    我们来看下面代码

    /**
     * @Description: 接口规范方法名查询示例
     * @Author along
     * @Date 2019/1/9 14:07
     */
    public interface UserDao extends JpaRepository<User, String>, JpaSpecificationExecutor<User> {
    
        /**
         * and条件查询
         * 对应sql:select u from User u where u.name = ?1 and u.email = ?2
         * 参数名大写,条件名首字母大写,并且接口名中参数出现的顺序必须和参数列表中的参数顺序一致
         */
        User findByNameAndEmail(String name, String email);
    
        /**
         * or条件查询
         * 对应sql:select u from User u where u.name = ?1 or u.password = ?2
         */
        List<User> findByNameOrPassword(String name, String password);
    
        /**
         * between查询
         * 对应sql:select u from User u where u.id between ?1 and ?2
         */
        List<User> findByCreateTimeBetween(Date startTime, Date endTime);
    
        /**
         * less查询
         * 对应sql:select u from User u where u.id < ?1
         */
        List<User> findByCreateTimeLessThan(Date time);
    
        /**
         * greater查询
         * 对应sql:select u from User u where u.id > ?1
         */
        List<User> findByCreateTimeGreaterThan(Date time);
    
        /**
         * is null查询
         * 对应sql:select u from User u where u.name is null
         */
        List<User> findByNameIsNull();
    
        /**
         * Is Not Null查询
         * 对应sql:select u from User u where u.name is not null
         */
        List<User> findByNameIsNotNull();
    
        /**
         * like模糊查询
         * 这里的模糊查询并不会自动在name两边加"%",需要手动对参数加"%"
         * 对应sql:select u from User u where u.name like ?1
         */
        List<User> findByNameLike(String name);
    
        /**
         * Not Like模糊查询
         * 对应sql:select u from User u where u.name not like ?1
         */
        List<User> findByNameNotLike(String name);
    
        /**
         * 倒序排序查询
         * 对应sql:select u from User u where u.password = ?1 order by u.id desc
         */
        List<User> findByPasswordOrderByCreateTimeDesc(String password);
    
        /**
         * <>查询
         * 对应sql:select u from User u where u.name <> ?1
         */
        List<User> findByNameNot(String name);
    
        /**
         * in 查询,方法的参数可以是 Collection 类型,也可以是数组或者不定长参数
         * 对应sql:select u from User u where u.id in ?1
         */
        List<User> findByIdIn(List<String> ids);
    
        /**
         * not in 查询,方法的参数可以是 Collection 类型,也可以是数组或者不定长参数
         * 对应sql:select u from User u where u.id not in ?1
         */
        List<User> findByIdNotIn(List<String> ids);
    
        /**
         * 分页查询,方法的参数可以是 Collection 类型,也可以是数组或者不定长参数
         * 对应sql:select u from User u where u.id not in ?1 limit ?
         */
        Page<User> findByIdNotIn(List<String> ids, Pageable pageable);
    
    }
    

    dao编写完毕,只需在service中调用即可,是不是感觉比上面的Example要优雅很多!而且可以应付大多数的查询需求。如果你用idea,idea还会在你编写方法是提供智能提示,简直不要太贴心。

    这里简单地介绍下原理:jpa框架在进行方法名解析时,如果遇到以 find、findBy、read、readBy、get、getBy为前缀的方法名,会忽略前缀,对剩下部分进行解析,再后面会识别如And、Or这样的关键字,来判断以何种方式连接查询关键字。而且如果方法的最后一个参数是 Sort 或 Pageable 类型,就会提取相关的信息,以便按规则进行排序或者分页查询。

    4. @Query创建查询

    比起接口规范方法名查询,@Query显得稍微麻烦一点,需要自己写JPQL查询语句,但是却更加强大,对方法名没有要求,可以准确控制JPQL语句,而且不局限于查询,还可以和@Modifying一起使用实现跟新操作,你甚至可以使用@Query来指定本地查询,写真正的sql语句,只要设置nativeQuery=true(但是个人建议不要用本地查询,这样就失去了JPA的优势,如果喜欢写sql,为什么不直接用mybatis呢?)
    下面是示例代码

    /**
     * @Description: @Query示例
     * @Author along
     * @Date 2019/1/9 14:07
     */
    public interface UserDao extends JpaRepository<User, String> {
    
        /**
         * 根据name查询,支持命名参数
         */
        @Query("select u from User u where u.name = :mame")
        List<User> findUserByName(@Param("name")String name);
    
        /**
         * 根据sex查询,缩影参数
         */
        @Query("select u from User u where u.sex = ?1")
        List<User> findUsersBySex(Integer sex);
    
        /**
         * 模糊查询
         */
        @Query("select u from User u where name like concat('%',?1,'%') ")
        List<User> findByName(String name);
    
        /**
         * 本地查询
         */
        @Query(value = "select * from user where name like CONCAT('%',?1,'%')", nativeQuery = true)
        List<User> findByNameLocal(String name);
    
        /**
         * 跟新密码,需要加@Modifying
         */
        @Modifying
        @Query("update user u set u.password = ?1 where u.id = ?2")
        Integer updatePasswordById(String password, String id);
    
    }
    

    注意模糊查询的 JPQL 写法,不要写成like '%?1%',这样是查不出来东西的。
    用命名参数需要在参数前面用@Param()注解制定参数名,不然会查询失败。
    跟新需要加上@Modifying,而且Modifying queries的返回值只能为void或者是int/Integer,调用更新方法前需要开启事务,否则会跟新失败。

    5. Criteria查询

    上文介绍的查询方法面简单的查询需求已经足够了,但是如果给定的查询条件是不固定的,需要动态的创建查询语句,那上文的方法都就都不适用了,这里可以用Criteria查询解决。
    Criteria API查询是通过面向对象的方式构建查询,相比于传统的基于字符串的JPQL查询,优势是类型安全,更加的面向对象。
    这里推荐一篇文章,对 Criteria API 讲解的十分详细:详解JPA 2.0动态查询机制:Criteria API

    下面我们先用 Criteria API 写一个service方法来实现一个简单的查询需求

    @Autoware
    private EntityManager entityManager;
    
    public List<User> findUserByNameAndSex0(String name, Integer sex) {
        // 1. 利用entityManager构建出CriteriaQuery类型的参数
        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        CriteriaQuery<User> query = builder.createQuery(User.class);
        // 2. 获取User的Root,也就是包装对象
        Root<User> root = query.from(User.class);
        // 3. 构建查询条件,这里相当于where user.id = id;
        Predicate predicate = builder.and(
                builder.like(root.get("name").as(String.class), "%" + name+ "%"),
                builder.equal(root.get("sex").as(Integer.class), sex)
        );
        query.where(predicate); // 到这里一个完整的动态查询就构建完成了
        // 指定查询结果集,相当于“select id,name...”,如果不设置,默认查询root中所有字段
        query.select(root);
        // 4. 执行查询,获取查询结果
        TypedQuery<User> typeQuery = entityManager.createQuery(query);
        List<User> resultList = typeQuery.getResultList();
    
        return resultList;
    }
    

    上面代码完整的实现了一次通过Criteria API查询的过程,下面编写的测试方法测试下查询结果

    @Test
    public void queryTest() {
        List<User> userList = userService.findUserByNameAndSex0("along", 1);
        System.out.println(userList.size());
        for (User user : userList) {
            System.out.println(user.getName());
        }
    }
    

    下面是运行后控制台打印出来的查询sql

    select 
        user0_.id as id1_2_, 
        user0_.create_time as create_t2_2_, 
        user0_.update_time as update_t3_2_, 
        user0_.birthday as birthday4_2_, 
        user0_.email as email5_2_, 
        user0_.name as name6_2_, 
        user0_.password as password7_2_, 
        user0_.sex as sex8_2_, 
        user0_.status as status9_2_ 
    from 
        user user0_ 
    where 
        (user0_.name like ?) and user0_.sex=1
    

    sql中结果集并不包括articleList和roles,原因是我们再User实体中做表关联配置时将这两个字段定义为了懒加载(fetch = FetchType.LAZY)

    尽管这达到了我们的目的,但这未免太麻烦了,创建一个查询需要这么多步骤,完全可以利用JPQL语句实现相同的需求,如下:

    @Query("select u from User u where u.name like concat('%',:name,'%') and u.sex=:sex")
    List<User> findUserByNameAndSex(@Param("name")String name,@Param("sex")Integer sex);
    

    幸运的是JPA为我们考虑到了这一点,我们可以发现上面完成一次查询一共有四个步骤,其中除了第3步构建查询条件,其他的都是固定的代码,JPA提供了JpaSpecificationExecutor接口帮我们实现了步骤1、2、4,我们在编写代码时只需要实现第3步即可。
    接下来我们重新实现下上面的需求。
    首先是要让UserDao继承JpaSpecificationExecutor接口,如下:

    public interface UserDao extends JpaRepository<User, String>, JpaSpecificationExecutor<User> {
    
    }
    

    JpaSpecificationExecutor接口并不在JpaRepository接口体系中,需要额外继承,而且Spring data JPA不会自动扫描识别,所以要和任意一个Repository的子接口一起使用。

    接下来我们在UserService中编写实现方法,如下

    @Autowired
    private UserDao userDao;
    
    public List<User> findUserByNameAndSex(String name, Integer sex) {
        return userDao.findAll(new Specification<User>() {
            @Override
            public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
                // 构建查询条件并返回
                return criteriaBuilder.and(
                        criteriaBuilder.equal(root.get("name"), name),
                        criteriaBuilder.equal(root.get("sex"), sex)
                );
            }
        });
    }
    

    我们只需要重写Sepcfication接口的toPredicate方法,而toPredicate方法自动携带了我们需要的三个参数,这都是JPA提前为我们创建好的,不需要我们手动创建,我们只需要在方法中构建一个Predicate即可,也就是我们自开始的实现的第3步的代码。是不是简洁了很多?

    下面深入源码看看JPA是怎么帮我们实现的,首先要看JpaSpecificationExecutor接口的源码

    public interface JpaSpecificationExecutor<T> {
    
        Optional<T> findOne(@Nullable Specification<T> spec);
    
        List<T> findAll(@Nullable Specification<T> spec);
    
        Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);
    
        List<T> findAll(@Nullable Specification<T> spec, Sort sort);
    
        long count(@Nullable Specification<T> spec);
    }
    

    可以看出改接口都是围绕着Specification构建的,每个方法的功能也一目了然,接下来看看Specification的源码

    @SuppressWarnings("deprecation")
    public interface Specification<T> extends Serializable {
    
        long serialVersionUID = 1L;
    
        /**
         * 否定给定的{@link Specification}。
         */
        static <T> Specification<T> not(Specification<T> spec) {
            return Specifications.negated(spec);
        }
    
        /**
         * 简单的静态工厂方法,在{@link Specification}周围添加一些语法糖。
         */
        static <T> Specification<T> where(Specification<T> spec) {
            return Specifications.where(spec);
        }
    
        /**
         * 将给定的{@link Specification}与当前的一个进行对比。
         *
         * @param other can be {@literal null}.
         * @return The conjunction of the specifications
         * @since 2.0
         */
        default Specification<T> and(Specification<T> other) {
            return Specifications.composed(this, other, AND);
        }
    
        /**
         * 将给定的规范与当前规范进行或运算。
         */
        default Specification<T> or(Specification<T> other) {
            return Specifications.composed(this, other, OR);
        }
    
        /**
         * 为给定的{@link Predicate}形式的被引用实体的查询创建WHERE子句
         */
        @Nullable
        Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
    }
    

    我们只需要把关注点放到最下面的toPredicate方法上,这也是唯一需要我们手动实现的方法。

    我们再来看findAll()方法的具体实现:

    public List<T> findAll(@Nullable Specification<T> spec) {
        return getQuery(spec, Sort.unsorted()).getResultList();
    }
    

    因为我们没有传入排序对象Sort,这里生成了一个默认的空的排序对象。再进入getQuery方法

    protected TypedQuery<T> getQuery(@Nullable Specification<T> spec, Sort sort) {
        return getQuery(spec, getDomainClass(), sort);
    }
    

    这里通过getDomainClass()方法得到了当前要查询的User实体的字节码类型,下面再进入getQuery()方法

    protected <S extends T> TypedQuery<S> getQuery(
                    @Nullable Specification<S> spec, Class<S> domainClass, Sort sort) {
        // 1. 构建出CriteriaQuery类型的参数
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<S> query = builder.createQuery(domainClass);
        // 2. 获取User的Root,也就是包装对象
        Root<S> root = applySpecificationToCriteria(spec, domainClass, query);
        // 指定查询结果集
        query.select(root);
    
        if (sort.isSorted()) {
            query.orderBy(toOrders(sort, root, builder));
        }
        // 4. 执行查询,获取查询结果
        return applyRepositoryMethodMetadata(em.createQuery(query));
    }
    

    注意我在源码中加的1、2、4注释,再进入applySpecificationToCriteria方法

    private <S, U extends T> Root<U> applySpecificationToCriteria(
         @Nullable Specification<U> spec, Class<U> domainClass, CriteriaQuery<S> query) {
    
        Assert.notNull(domainClass, "Domain class must not be null!");
        Assert.notNull(query, "CriteriaQuery must not be null!");
        // 2. 获取User的Root,也就是包装对象
        Root<U> root = query.from(domainClass);
    
        if (spec == null) {
            return root;
        }
    
        CriteriaBuilder builder = em.getCriteriaBuilder();
        // 重点!!!!在这里执行我们实现的toPredicate()方法
        Predicate predicate = spec.toPredicate(root, query, builder);
    
        if (predicate != null) {
            // 到这里一个完整的动态查询就构建完成了
            query.where(predicate); 
        }
    
        return root;
    }
    

    看到这你会发现我们最开始的原始实现中的1、2、4步JPA都做了实现,并且源码中spec参数调用了toPredicate方法,也就是我们自己在service中做的实现,而且在调动toPredicate方法之前Root、CriteriaQuery和CriteriaBuilder都已经创建好了。

    到这里我们已经知道JpaSpecificationExecutor底层是如何实现的封装,下面我们来探究下具体的使用。

    动态语句查询
    下面我们利用JpaSpecificationExecutor实现一个动态语句查询,并实现分页排序功能

    public Page<User> findUser(User user, int page, int size) {
    
        Sort sort = new Sort(Sort.Direction.DESC, "updateTime","createTime");
        Pageable pageable = PageRequest.of(page, size, sort);
    
        return userDao.findAll(new Specification<User>() {
            @Override
            public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
                List<Predicate> predicateList = new ArrayList<>();
    
                if (!StringUtils.isEmpty(user.getId())) {
                    predicateList.add(criteriaBuilder.equal(root.get("id"), user.getId()));
                }
                if (!StringUtils.isEmpty(user.getName())) {
                    predicateList.add(criteriaBuilder.like(root.get("name"), user.getName()));
                }
                if (null != user.getCreateTime()) {
                    predicateList.add(criteriaBuilder.greaterThan(root.get("createTime"), user.getCreateTime()));
                }
                if (null != user.getUpdateTime()) {
                    predicateList.add(criteriaBuilder.lessThanOrEqualTo(root.get("updateTime"), user.getUpdateTime()));
                }
    
                Predicate[] predicateArr = new Predicate[predicateList.size()];
                return criteriaBuilder.and(predicateList.toArray(predicateArr));
            }
        }, pageable);
    }
    

    效果是不是跟mybatis的xml实现的动态查询有些类似?

    多表关联查询
    User表关联了两张表,分别是Article表和Role表,和Article表是一对多关系,和Role表是多对多关系。下面是关联查询代码示例

    /**
     * 表关联查询
     * @param articleId
     * @param roleId
     * @return
     */
    public List<User> findUserByArticleAndRole(String articleId, String roleId) {
        return userDao.findAll(new Specification<User>() {
            @Override
            public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
                // 方式1
                ListJoin<User, Article> articleJoin = root.join(root.getModel().getList("articleList", Article.class), JoinType.LEFT);
                SetJoin<User, Role> roleJoin = root.join(root.getModel().getSet("roles", Role.class), JoinType.LEFT);
                // 方式2
                //Join<User, Article> articleJoin = root.join("articleList", JoinType.LEFT);
                //Join<User, Role> roleJoin = root.join("roles", JoinType.LEFT);
    
                Predicate predicate = criteriaBuilder.or(
                        criteriaBuilder.equal(articleJoin.get("id"), articleId),
                        criteriaBuilder.equal(roleJoin.get("id"), roleId)
                );
                return predicate;
            }
        });
    }
    

    下面是运行结果生成的sql

    select 
        user0_.id as id1_2_, 
        user0_.create_time as create_t2_2_, 
        user0_.update_time as update_t3_2_, 
        user0_.birthday as birthday4_2_, 
        user0_.email as email5_2_, 
        user0_.name as name6_2_, 
        user0_.password as password7_2_, 
        user0_.sex as sex8_2_, 
        user0_.status as status9_2_ 
    from 
        user user0_ 
    left outer join 
        article articlelis1_  // 关联article
    on 
        user0_.id=articlelis1_.user_id 
    left outer join 
        user_role roles2_  // 关联中间表user_role
    on
        user0_.id=roles2_.user_id 
    left outer join 
        role role3_  // 关联role
    on 
        roles2_.role_id=role3_.id 
    where 
        articlelis1_.id=? or role3_.id=?
    

    Criteria查询到这里也就介绍完了,能力有限,介绍的比较粗略。
    如果你熟练地掌握了 Criteria API 的使用,你几乎可以使用该方式来应对所有的查询需求,但是面对简单的查询需求,还是建议使用更加简洁的规范方法名查询,只有在其他方式不方便解决时再考虑用 Criteria API 来解决。具体实际开发中如何选择,还要根据实际开发情况而定。


    三、性能问题解决

    上文为了单纯地介绍jpa的用法,隐藏了很多问题,在这一节中集中介绍并处理。

    循环引用问题

    再来回顾下User、Role、Article的关系,User与Role是多对多关系,User与Article是一对多关系。因为文章较长,再往上翻回顾比较麻烦,这里再放上这三个类的源码,如下

    /**
     * 用户实体类
     */
    @Entity
    @Table(name = "user") //对应数据库中的表名
    public class User extends BaseData {
    
        private static final long serialVersionUID = -5103936306962248929L;
    
        private String name;
    
        private String password;
    
        private Integer sex; // 1:男;0:女
    
        private Integer status = 1; //-1:删除;0 禁用 1启用
    
        private String email;
    
        @Temporal(TemporalType.DATE)
        @JsonFormat(pattern = "yyyy-MM-dd")
        private Date birthday;
    
        @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
        private List<Article> articleList; // 文章
    
        @ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.LAZY)
        @JoinTable(
                name = "user_role", // 定义中间表的名称
                joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, // 定义中间表中关联User表的外键名
                inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")} // 定义中间表中关联role表的外键名
        )
        private Set<Role> roles; // 角色外键
    
        // getter and setter...
    }
    
    /**
     * 角色实体类
     */
    @Entity
    @Table(name = "role")
    public class Role extends BaseData {
    
        private static final long serialVersionUID = 5012235295240129244L;
    
        private String roleName; // 角色名
    
        private Integer roleType; // 1: 超级管理员   2: 系统管理员 3:一般用户
    
        private Integer state; // 0禁用  1 启用
    
        @ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.LAZY)
        @JoinTable(
                name = "user_role",
                joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")},
                inverseJoinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}
        )
        private Set<User> users; // 与用户多对多
    
        // getter and setter ...
    }
    
    /**
     * 文章实体类
     */
    @Entity
    @Table(name = "article")
    public class Article extends BaseData{
    
        private static final long serialVersionUID = -4817984675096952797L;
    
        @NotEmpty(message = "标题不能为空")
        @Column(nullable = false, length = 50)
        private String title;
    
        @Lob // 大对象,映射 MySQL 的 Long Text 类型
        @Basic(fetch = FetchType.LAZY) // 懒加载
        @Column(nullable = false) // 映射为字段,值不能为空
        private String content; // 文章全文内容
    
        @ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH}, optional = false)
        @JoinColumn(name = "user_id") // 设置在article表中的关联字段(外键)名
        private User user; // 所属用户
    
        // getter and setter ...
    }
    

    现在我们编写查询代码来看看这样存在什么问题,在userDao中编写查询方法

    List<User> findByName(String name);
    

    下面是UserService中的实现方法

    public List<User> findByName(String name) {
        List<User> users = userDao.findByName(name);
        return users;
    }
    

    在编写UserController

    @GetMapping("/findByName/{name}")
    public ResultMapper findByName(@PathVariable String name) {
        List<User> users = userService.findByName(name);
        return ResultMapperUtil.success(users);
    }
    

    最后在浏览器输入http://localhost:8080/user/findByName/along 发起查询请求。

    虽然查询出了结果,但是结果集非常巨大,并且控制台会报两个异常,分别是IllegalStateExceptionStackOverflowError,第一个是无效状态异常,重点是第二个异常,栈溢出,数据库中名字为along的用户只有几条,结果却发生了栈溢出,为什么会这样?

    我们再看看打印的sql日志

    Hibernate: 
    select 
        user0_.id as id1_2_, 
        user0_.create_time as create_t2_2_, 
        user0_.update_time as update_t3_2_, 
        user0_.birthday as birthday4_2_, 
        user0_.email as email5_2_, 
        user0_.name as name6_2_, 
        user0_.password as password7_2_, 
        user0_.sex as sex8_2_, 
        user0_.status as status9_2_ 
    from 
        user user0_ where user0_.name=?
    
    Hibernate: 
    select 
        articlelis0_.user_id as user_id6_0_0_, 
        articlelis0_.id as id1_0_0_, 
        articlelis0_.id as id1_0_1_, 
        articlelis0_.create_time as create_t2_0_1_, 
        articlelis0_.update_time as update_t3_0_1_, 
        articlelis0_.content as content4_0_1_, 
        articlelis0_.title as title5_0_1_, 
        articlelis0_.user_id as user_id6_0_1_ 
    from 
        article articlelis0_ 
    where 
        articlelis0_.user_id=?
    

    共打印了两条sql,第一条sql正是我们所希望的,根据name查询user,由于user关联了article,jpa于是再发起sql,去查询article,又由于article中又关联着user,于是又会再查一遍user,如此循环反复,根本停不下来,很快就栈溢出了。

    按这样说,你可能还会有疑问,这样的话不是应该打印3条sql吗?应该该有一条sql去查询role,为什么只有两条呢?原因是第二条sql查询article时就已经进入了死循环并报了异常,所以也就不会再发起第三条sql去查询role了。

    解决方案一:使用@JsonIgnore注解

    @JsonIgnore是Jackson提供的注解,在实体类的属性上加上该注解,这样在json序列化时会将java bean中的对应的属性忽略掉,同样jpa在查询时也会忽略对应的属性,如此便可以解决循环查询的问题。

    @JsonIgnore的使用十分灵活,你可以只在User中使用,这样在查询User时结果集中就不会包含添加了改注解的属性,你也可以只在Role和Article实体中与User关联的属性上加@JsonIgnore,这样在查询User时还是会关联查询Role和Article,但是不会发生循环查询。

    这里只在User中加上@JsonIgnore注解,改造后的User类代码如下

    /**
     * 用户实体类
     */
    @Entity
    @Table(name = "user") //对应数据库中的表名
    public class User extends BaseData {
    
        private static final long serialVersionUID = -5103936306962248929L;
    
        private String name;
    
        private String password;
    
        private Integer sex; // 1:男;0:女
    
        private Integer status = 1; //-1:删除;0 禁用 1启用
    
        private String email;
    
        @Temporal(TemporalType.DATE)
        @JsonFormat(pattern = "yyyy-MM-dd")
        private Date birthday;
    
        @JsonIgnore
        @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
        private List<Article> articleList; // 文章
    
        @JsonIgnore
        @ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.LAZY)
        @JoinTable(
                name = "user_role", // 定义中间表的名称
                joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, // 定义中间表中关联User表的外键名
                inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")} // 定义中间表中关联role表的外键名
        )
        private Set<Role> roles; // 角色外键
    
        // getter and setter...
    }
    

    再次查询,成功查询出结果,结果中不包含article和role,控制台也只打印出了一条查询user的sql。

    解决方案二:使用@JsonIgnoreProperties注解

    同样是Jackson提供的注解,@JsonIgnoreProperties和@JsonIgnore用法差不多,@JsonIgnoreProperties可以更加细粒度的选择忽略关联实体中的属性。也就是说如果你需要关联查询,但是又想控制关联查询的类的属性,那么可以使用该注解。

    改造后的User类如下

    /**
     * 用户实体类
     */
    @Entity
    @Table(name = "user") //对应数据库中的表名
    public class User extends BaseData {
    
        private static final long serialVersionUID = -5103936306962248929L;
    
        private String name;
    
        private String password;
    
        private Integer sex; // 1:男;0:女
    
        private Integer status = 1; //-1:删除;0 禁用 1启用
    
        private String email;
    
        @Temporal(TemporalType.DATE)
        @JsonFormat(pattern = "yyyy-MM-dd")
        private Date birthday;
    
        @JsonIgnoreProperties(value = {"user", "content"}) //解决循环引用问题,content内容大,不加载
        @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
        private List<Article> articleList; // 文章
    
        @JsonIgnoreProperties(value = {"users"}) //解决循环引用问题
        @ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.LAZY)
        @JoinTable(
                name = "user_role", // 定义中间表的名称
                joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, // 定义中间表中关联User表的外键名
                inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")} // 定义中间表中关联role表的外键名
        )
        private Set<Role> roles; // 角色外键
    
        // getter and setter...
    }
    

    上面我在关联属性aritcleList和roles上加上@JsonIgnoreProperties注解忽略了会引发循环引用的属性,article中的content是大文本,也将其忽略。

    再次发起查询,没有发生循环查询,控制台打印的sql如下:

    Hibernate: select user0_.id as id1_2_, user0_.create_time as create_t2_2_, user0_.update_time as update_t3_2_, user0_.birthday as birthday4_2_, user0_.email as email5_2_, user0_.name as name6_2_, user0_.password as password7_2_, user0_.sex as sex8_2_, user0_.status as status9_2_ from user user0_ where user0_.name=?
    Hibernate: select articlelis0_.user_id as user_id6_0_0_, articlelis0_.id as id1_0_0_, articlelis0_.id as id1_0_1_, articlelis0_.create_time as create_t2_0_1_, articlelis0_.update_time as update_t3_0_1_, articlelis0_.content as content4_0_1_, articlelis0_.title as title5_0_1_, articlelis0_.user_id as user_id6_0_1_ from article articlelis0_ where articlelis0_.user_id=?
    Hibernate: select roles0_.user_id as user_id2_3_0_, roles0_.role_id as role_id1_3_0_, role1_.id as id1_1_1_, role1_.create_time as create_t2_1_1_, role1_.update_time as update_t3_1_1_, role1_.role_name as role_nam4_1_1_, role1_.role_type as role_typ5_1_1_, role1_.state as state6_1_1_ from user_role roles0_ inner join role role1_ on roles0_.role_id=role1_.id where roles0_.user_id=?
    

    共打印了3条sql,分别查询对应的三个表。

    解决方案三:返回自定义包装类

    上面我们的查询接口都是直接用实体类来接收查询结果并返回,但是更加规范的是创建一个Vo类来接收查询结果,Vo中的属性名跟实体类相同,这样我们可以实现不改变实体类就能决定返回结果的内容。

    下面我们定义一个Vo

    /**
     * @Description: 返回前端的类,vo可以控制返回的字段,也是解决循环引用的一种方案
     * @Author along
     * @Date 2019/1/10 13:29
     */
    public interface UserVo {
    
        String getId();
    
        String getName();
    
        String getPassword();
    
        Integer getSex(); // 1:男;0:女
    
        Integer getStatus(); //-1:删除;0 禁用 1启用
    
        String getEmail();
    
        Date getBirthday();
    
        //@JsonIgnoreProperties(value = {"user", "content"}) //解决循环引用问题
        //List<Article> getArticleList(); // 文章列表
    
        //@JsonIgnoreProperties(value = {"users"}) //解决循环引用问题
        //Set<Role> getRoles(); // 角色外键
    }
    

    注意该 UserVo 是一个接口,里面的属性是实体类中对应属性的 get 方法。使用Vo可以灵活决定返回结果的字段。

    下面我们改造userDao中的查询方法,改用UserVo来接收

    List<UserVo> findByName(String name);
    

    下面是UserService中的实现方法,同样改用UserVo来接收

    public List<UserVo> findByName(String name) {
        List<UserVo> userVos = userDao.findByName(name);
        return userVos;
    }
    

    再下面是UserController接口

    @GetMapping("/findByName/{name}")
    public ResultMapper findByName(@PathVariable String name) {
        List<UserVo> userVos = userService.findByName(name);
        return ResultMapperUtil.success(userVos);
    }
    

    调用查询,可以实现与方案一和方案二相同的效果,使用也更加灵活简单,个人最推荐该方案。

    N+1查询问题

    上面虽然解决了循环引用的问题,但随后又出现一个更加头疼的问题,也是Jpa使用了表关联属性后出现的N+1查询问题,具体这是个什么现象呢,项目中会有很多的实体类,实体类之间通常又会有一对多、多对多的关联,通常我们会将多的一方设置成懒加载,这样我们在查询一个实体时只会查询出该实体的基本属性(不包括被设置为懒加载的属性),然后当我们需要关联对象的某些属性时,ORM就会再次发出sql语句查询关联的属性。这也就解释了为什么上文中查询一个user,结果却发出了三条sql语句。

    数据小时不会有明显的问题,可当查询的数据量变大时,查询发出的sql数量也会非常大,会引发严重的性能问题。

    如下面的例子,我们调用userDao.findAll()方法查询所有用户数据,并且设置了分页,查询20条,结果控制台打印的sql日志如下

    Hibernate: select user0_.id as id1_2_, user0_.create_time as create_t2_2_, user0_.update_time as update_t3_2_, user0_.birthday as birthday4_2_, user0_.email as email5_2_, user0_.name as name6_2_, user0_.password as password7_2_, user0_.sex as sex8_2_, user0_.status as status9_2_ from user user0_ order by user0_.update_time desc, user0_.create_time desc limit ?
    Hibernate: select articlelis0_.user_id as user_id6_0_0_, articlelis0_.id as id1_0_0_, articlelis0_.id as id1_0_1_, articlelis0_.create_time as create_t2_0_1_, articlelis0_.update_time as update_t3_0_1_, articlelis0_.content as content4_0_1_, articlelis0_.title as title5_0_1_, articlelis0_.user_id as user_id6_0_1_ from article articlelis0_ where articlelis0_.user_id=?
    Hibernate: select roles0_.user_id as user_id2_3_0_, roles0_.role_id as role_id1_3_0_, role1_.id as id1_1_1_, role1_.create_time as create_t2_1_1_, role1_.update_time as update_t3_1_1_, role1_.role_name as role_nam4_1_1_, role1_.role_type as role_typ5_1_1_, role1_.state as state6_1_1_ from user_role roles0_ inner join role role1_ on roles0_.role_id=role1_.id where roles0_.user_id=?
    Hibernate: select articlelis0_.user_id as user_id6_0_0_, articlelis0_.id as id1_0_0_, articlelis0_.id as id1_0_1_, articlelis0_.create_time as create_t2_0_1_, articlelis0_.update_time as update_t3_0_1_, articlelis0_.content as content4_0_1_, articlelis0_.title as title5_0_1_, articlelis0_.user_id as user_id6_0_1_ from article articlelis0_ where articlelis0_.user_id=?
    Hibernate: select roles0_.user_id as user_id2_3_0_, roles0_.role_id as role_id1_3_0_, role1_.id as id1_1_1_, role1_.create_time as create_t2_1_1_, role1_.update_time as update_t3_1_1_, role1_.role_name as role_nam4_1_1_, role1_.role_type as role_typ5_1_1_, role1_.state as state6_1_1_ from user_role roles0_ inner join role role1_ on roles0_.role_id=role1_.id where roles0_.user_id=?
    Hibernate: select articlelis0_.user_id as user_id6_0_0_, articlelis0_.id as id1_0_0_, articlelis0_.id as id1_0_1_, articlelis0_.create_time as create_t2_0_1_, articlelis0_.update_time as update_t3_0_1_, articlelis0_.content as content4_0_1_, articlelis0_.title as title5_0_1_, articlelis0_.user_id as user_id6_0_1_ from article articlelis0_ where articlelis0_.user_id=?
    Hibernate: select roles0_.user_id as user_id2_3_0_, roles0_.role_id as role_id1_3_0_, role1_.id as id1_1_1_, role1_.create_time as create_t2_1_1_, role1_.update_time as update_t3_1_1_, role1_.role_name as role_nam4_1_1_, role1_.role_type as role_typ5_1_1_, role1_.state as state6_1_1_ from user_role roles0_ inner join role role1_ on roles0_.role_id=role1_.id where roles0_.user_id=?
    Hibernate: select articlelis0_.user_id as user_id6_0_0_, articlelis0_.id as id1_0_0_, articlelis0_.id as id1_0_1_, articlelis0_.create_time as create_t2_0_1_, articlelis0_.update_time as update_t3_0_1_, articlelis0_.content as content4_0_1_, articlelis0_.title as title5_0_1_, articlelis0_.user_id as user_id6_0_1_ from article articlelis0_ where articlelis0_.user_id=?
    Hibernate: select roles0_.user_id as user_id2_3_0_, roles0_.role_id as role_id1_3_0_, role1_.id as id1_1_1_, role1_.create_time as create_t2_1_1_, role1_.update_time as update_t3_1_1_, role1_.role_name as role_nam4_1_1_, role1_.role_type as role_typ5_1_1_, role1_.state as state6_1_1_ from user_role roles0_ inner join role role1_ on roles0_.role_id=role1_.id where roles0_.user_id=?
    Hibernate: select articlelis0_.user_id as user_id6_0_0_, articlelis0_.id as id1_0_0_, articlelis0_.id as id1_0_1_, articlelis0_.create_time as create_t2_0_1_, articlelis0_.update_time as update_t3_0_1_, articlelis0_.content as content4_0_1_, articlelis0_.title as title5_0_1_, articlelis0_.user_id as user_id6_0_1_ from article articlelis0_ where articlelis0_.user_id=?
    Hibernate: select roles0_.user_id as user_id2_3_0_, roles0_.role_id as role_id1_3_0_, role1_.id as id1_1_1_, role1_.create_time as create_t2_1_1_, role1_.update_time as update_t3_1_1_, role1_.role_name as role_nam4_1_1_, role1_.role_type as role_typ5_1_1_, role1_.state as state6_1_1_ from user_role roles0_ inner join role role1_ on roles0_.role_id=role1_.id where roles0_.user_id=?
    Hibernate: select articlelis0_.user_id as user_id6_0_0_, articlelis0_.id as id1_0_0_, articlelis0_.id as id1_0_1_, articlelis0_.create_time as create_t2_0_1_, articlelis0_.update_time as update_t3_0_1_, articlelis0_.content as content4_0_1_, articlelis0_.title as title5_0_1_, articlelis0_.user_id as user_id6_0_1_ from article articlelis0_ where articlelis0_.user_id=?
    Hibernate: select roles0_.user_id as user_id2_3_0_, roles0_.role_id as role_id1_3_0_, role1_.id as id1_1_1_, role1_.create_time as create_t2_1_1_, role1_.update_time as update_t3_1_1_, role1_.role_name as role_nam4_1_1_, role1_.role_type as role_typ5_1_1_, role1_.state as state6_1_1_ from user_role roles0_ inner join role role1_ on roles0_.role_id=role1_.id where roles0_.user_id=?
    Hibernate: select articlelis0_.user_id as user_id6_0_0_, articlelis0_.id as id1_0_0_, articlelis0_.id as id1_0_1_, articlelis0_.create_time as create_t2_0_1_, articlelis0_.update_time as update_t3_0_1_, articlelis0_.content as content4_0_1_, articlelis0_.title as title5_0_1_, articlelis0_.user_id as user_id6_0_1_ from article articlelis0_ where articlelis0_.user_id=?
    Hibernate: select roles0_.user_id as user_id2_3_0_, roles0_.role_id as role_id1_3_0_, role1_.id as id1_1_1_, role1_.create_time as create_t2_1_1_, role1_.update_time as update_t3_1_1_, role1_.role_name as role_nam4_1_1_, role1_.role_type as role_typ5_1_1_, role1_.state as state6_1_1_ from user_role roles0_ inner join role role1_ on roles0_.role_id=role1_.id where roles0_.user_id=?
    

    虽然只用分页控制了结果集,但是sql的量还是非常大的。

    JPA2.1提供了新的特性来解决N+1查询的问题,就是实体图(EntityGraph),由于实体间的关系错综复杂,如果实体间存在关系那么就用线将实体进行连接,那么实体间将形成一个网状的图,而实体图技术就是对这个关系图的操作,它允许开发者指定某个实体及其发展出来的关系网中的某些节点,这些节点间形成一条路径,当调用查询方法时,查询方法会按照实体图中的路径将路径中的这些节点立即加载,而绕过延迟加载,从而更高效的实现数据的检索。

    下面是实体图的应用,我们在User表上使用@NamedEntityGraphs注解定义实体图,你可以定义多个实体图,如下

    /**
     * 用户实体类
     */
    @Entity
    @Table(name = "user") //对应数据库中的表名
    //EntityGraph(实体图)使用演示,解决查询N+1问题
    @NamedEntityGraphs({
            @NamedEntityGraph(name = "user.all",
                    attributeNodes = { // attributeNodes 用来定义需要立即加载的属性
                            @NamedAttributeNode("articleList"), // 无延伸
                            @NamedAttributeNode("roles"), // 无延伸
                    }
            ),
    })
    public class User extends BaseData {
        ...
    }
    

    @NamedEntityGraph代表着一个实体图,通过name属性设置实体图的名字,通过attributeNodes属性来定义需要立即加载的属性,如果role还关联了另外一张表,并且设置为了懒加载,那如果想要立即加载改表,就通过subgraphs属性来进行描述,下面是在Role实体中定义的实体图

    @Entity
    @Table(name = "role")
    // EntityGraph(实体图)使用演示,解决查询N+1问题
    @NamedEntityGraphs({
            @NamedEntityGraph(name = "role.all",
                    attributeNodes = { // attributeNodes 用来指定要立即加载的节点,节点用 @NamedAttributeNode 定义
                            @NamedAttributeNode(value = "users", subgraph = "userWithArticleList"), // 要立即加载users属性中的articleList元素
                    },
                    subgraphs = { // subgraphs 用来定义关联对象的属性,也就是对上面的 userWithArticleList 进行描述
                            @NamedSubgraph(name = "userWithArticleList", attributeNodes = @NamedAttributeNode("articleList")), // 一层延伸
                    }
            ),
    })
    public class Role extends BaseData {
        ...
    }
    

    光是定义了实体图还不够,我们需要在查询时指定使用哪一个实体图进行查询,如下,重写findAll()方法,使用@EntityGraph注解指定通过名为user.all的实体图查询

    public interface UserDao extends JpaRepository<User, String>, JpaSpecificationExecutor<User> {
    
        //重写findAll(Pageable pageable)方法,用实体图查询
        @EntityGraph(value = "user.all", type = EntityGraph.EntityGraphType.FETCH)
        @Override
        Page<User> findAll(Pageable pageable);
    }
    

    执行查询,打印出的sql日志如下,为了方便阅读将其格式化输出

    Hibernate: 
    select 
        user0_.id as id1_2_0_, 
        articlelis1_.id as id1_0_1_, 
        role3_.id as id1_1_2_, 
        user0_.create_time as create_t2_2_0_, 
        user0_.update_time as update_t3_2_0_, 
        user0_.birthday as birthday4_2_0_, 
        user0_.email as email5_2_0_, 
        user0_.name as name6_2_0_, 
        user0_.password as password7_2_0_, 
        user0_.sex as sex8_2_0_, 
        user0_.status as status9_2_0_, 
        articlelis1_.create_time as create_t2_0_1_, 
        articlelis1_.update_time as update_t3_0_1_, 
        articlelis1_.content as content4_0_1_, 
        articlelis1_.title as title5_0_1_, 
        articlelis1_.user_id as user_id6_0_1_, 
        articlelis1_.user_id as user_id6_0_0__, 
        articlelis1_.id as id1_0_0__, 
        role3_.create_time as create_t2_1_2_, 
        role3_.update_time as update_t3_1_2_, 
        role3_.role_name as role_nam4_1_2_, 
        role3_.role_type as role_typ5_1_2_, 
        role3_.state as state6_1_2_, 
        roles2_.user_id as user_id2_3_1__, 
        roles2_.role_id as role_id1_3_1__ 
        from user user0_ 
    left outer join 
        article articlelis1_ 
    on 
        user0_.id=articlelis1_.user_id 
    left outer join 
        user_role roles2_ 
    on 
        user0_.id=roles2_.user_id 
    left outer join 
        role role3_ 
    on 
        roles2_.role_id=role3_.id 
    order by 
        user0_.update_time desc, user0_.create_time desc
    

    可以看到只打印了一条sql语句。

    本文相关源码地址

    https://github.com/alonglong/spring-boot-all/tree/master/spring-boot-jpa

    相关文章

      网友评论

          本文标题:SpringBoot2(五):Spring Data JPA 使

          本文链接:https://www.haomeiwen.com/subject/cbqsjqtx.html