分类
Spring Data Spring Persistence

spring data jpa优雅的实现软删除

软删除,即通过在标识数据表中的某个字段来达到看似删除的目的,由于软删除对数据库的侵入比较小,所以在删除时可以完美的规避数据约束的问题。

我曾在4年前写过一篇spring boot实现软删除,在该文章中给出了实现软删除的一种方式。本文将使用一种更优雅的方式来实现它。

项目源码

本文项目源码https://github.com/codedemo-club/spring-data-jpa-soft-delete

接口

首先我们需要建立一个接口,来标识哪些实体在查询时是需要软删除的:

public interface SoftDelete {
  Boolean getDeleted();
}

然后实体实现这一接口:

@Entity
public class Foo implements SoftDelete {
  private Boolean deleted = false;

  @Override
  public Boolean getDeleted() {
    return deleted;
  }

  // 设置为私有
  private void setDeleted(Boolean deleted) {
    this.deleted = deleted;
  }

注意:在此省略了其它的属性,其它的属性请参考源代码

@Entity
public class Bar implements SoftDelete {
  private Boolean deleted = false;

  @Override
  public Boolean getDeleted() {
    return deleted;
  }

  // 设置为私有
  private void setDeleted(Boolean deleted) {
    this.deleted = deleted;
  }

设置标识

接下来,使用@SQLDelete注解来设置删除语句,虽然此操作我们也可以通过相应的方法统一设置,但使用@SQLDelete会使软删除更加的灵活,特别是当我们解决一些数据唯一性的时候。

@Entity
@SQLDelete(sql = "update `bar` set deleted = 1 where id = ?")
public class Bar implements SoftDelete {
@Entity
@SQLDelete(sql = "update `foo` set deleted = 1 where id = ?")
public class Foo implements SoftDelete {

自定义软删除实现类

通过自定义的软删除实现类来达到在查询时加入软删除的目的。

package club.codedemo.springdatajpasoftdelete;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.JpaEntityInformationSupport;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.lang.Nullable;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * 软删除实现类
 * https://www.codedemo.club/spring-data-jpa-soft-delete/
 * https://developer.aliyun.com/article/465404
 * https://stackoverflow.com/questions/36721601/spring-boot-how-to-declare-a-custom-repository-factory-bean
 */
@Transactional(
    readOnly = true
)
public class SoftDeleteRepositoryImpl<T, ID> extends SimpleJpaRepository<T, ID>  implements PagingAndSortingRepository<T, ID>,
    CrudRepository<T, ID>,
    JpaSpecificationExecutor<T> {
  private final Logger logger = LoggerFactory.getLogger(this.getClass());

  public SoftDeleteRepositoryImpl(Class<T> domainClass, EntityManager em) {
    this(JpaEntityInformationSupport.getEntityInformation(domainClass, em), em);
  }

  public SoftDeleteRepositoryImpl(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
    super(entityInformation, entityManager);
  }

  @Override
  public boolean existsById(ID id) {
    return this.findById(id).isPresent();
  }

  @Override
  public List<T> findAll() {
    return this.findAll(this.andDeleteFalseSpecification(null));
  }

  @Override
  public Page<T> findAll(Pageable pageable) {
    return this.findAll(this.andDeleteFalseSpecification(null), pageable);
  }

  @Override
  public List<T> findAll(@Nullable Specification<T> specification) {
    return super.findAll(this.andDeleteFalseSpecification(specification));
  }

  @Override
  public Page<T> findAll(@Nullable Specification<T> specification, Pageable pageable) {
    return super.findAll(this.andDeleteFalseSpecification(specification), pageable);
  }

  @Override
  public List<T> findAll(@Nullable Specification<T> specification, Sort sort) {
    return super.findAll(this.andDeleteFalseSpecification(specification), sort);
  }

  @Override
  public Optional<T> findById(ID id) {
    Optional<T> entityOptional = super.findById(id);
    if (entityOptional.isPresent()) {
      T entity = entityOptional.get();
      if (entity instanceof SoftDelete) {
        if (!((SoftDelete) entity).getDeleted())
          return entityOptional;
      } else {
        this.logger.warn("未实现SoftDeleteEntity的实体" + entity.getClass() + "使用了软删除功能。请检查JpaRepositoryFactoryBean配置");
      }
    }
    return Optional.empty();
  }

  @Override
  public List<T> findAllById(Iterable<ID> ids) {
    return super.findAllById(ids).stream().filter(entity -> {
      if (entity instanceof SoftDelete) {
        return !((SoftDelete) entity).getDeleted();
      } else {
        this.logger.warn("未实现SoftDeleteEntity的实体" + entity.getClass() + "使用了软删除功能。请检查JpaRepositoryFactoryBean配置");
      }
      return false;
    }).collect(Collectors.toList());
  }

  @Override
  public long count() {
    return this.count(this.andDeleteFalseSpecification(null));
  }

  @Override
  public long count(@Nullable Specification<T> specification) {
    return super.count(this.andDeleteFalseSpecification(specification));
  }

  /**
   * 添加软查询条件
   *
   * @param specification 综合查询条件
   */
  private Specification<T> andDeleteFalseSpecification(Specification<T> specification) {
    Specification<T> deleteIsFalse = (root, criteriaQuery, criteriaBuilder) -> criteriaBuilder.equal(root.get("deleted").as(Boolean.class), false);
    if (specification == null) {
      specification = deleteIsFalse;
    } else {
      specification = specification.and(deleteIsFalse);
    }
    return specification;
  }
}

自定义JpaRepositoryFactoryBean

通过自定义JpaRepositoryFactoryBean达到:实现了SoftDelete的实体在查询时使用SoftDeleteRepositoryImpl,而未实现SoftDelete的实体在查询时使用原SimpleJpaRepository


/**
 * 自定义软件删除工厂
 * @param <R> 仓库
 * @param <T> 实体
 */
public class SoftDeleteRepositoryFactoryBean <R extends JpaRepository<T, Serializable>, T> extends JpaRepositoryFactoryBean<R, T, Serializable> {
  public SoftDeleteRepositoryFactoryBean(Class<? extends R> repositoryInterface) {
    super(repositoryInterface);
  }

  @Override
  protected RepositoryFactorySupport createRepositoryFactory(final EntityManager entityManager) {
    return new JpaRepositoryFactory(entityManager) {
      protected SimpleJpaRepository<T, Serializable> getTargetRepository(
          RepositoryInformation information, EntityManager entityManager) {
        Class<T> domainClass = (Class<T>) information.getDomainType();
        if(SoftDelete.class.isAssignableFrom(domainClass)) {
          return new SoftDeleteRepositoryImpl(domainClass, entityManager);
        } else {
          return new SimpleJpaRepository(domainClass, entityManager);
        }
      }

      @Override
      protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
        return metadata.getDomainType().isAssignableFrom(SoftDelete.class) ? SoftDeleteRepositoryImpl.class : SimpleJpaRepository.class;
      }
    };
  }
}

注册Bean

最后我们注册刚刚创建的工厂Bean,以使其生效:

@EnableJpaRepositories(value = "club.codedemo.springdatajpasoftdelete",
        repositoryFactoryBeanClass = SoftDeleteRepositoryFactoryBean.class)
public class SpringDataJpaSoftDeleteApplication {

此时,将我们在进行查询操作时,如果实体实现了SoftDelete,则会使用我们自定义的SoftDeleteRepositoryImpl,而如果没有实现SoftDelete,则仍然使用原查询。

SoftDeleteRepositoryImpl方法中,我们重写了所有的查询方法,并在查询中加入了deletedfasle的查询条件。

测试

此时,重新启动系统,点击删除操作后。数据仍然存在于数据中,而且关联查询也不会发生任何错误。如果我们希望在@OneToMany注解中也使用软删除生效(即不返回已删除的关联实体),则仅仅需要在相应的字段加入@Where(clause = "deleted = false")注解即可。

分类
Spring Data Spring Persistence

Spring Data JPA设置字段默认值的两种方法

1. 简介

本文将介绍如何在JPA中定义数据列的默认值。
通常有两种方式来实现默认值的定义:在实体类中设置默认值以及使用JPA注解直接操作数据表。

2. 实体类

第一种方法是直接在实体中定义:

@Entity
public class User {
    @Id
    private Long id;
    private String firstName = "Code demo";
    private Integer age = 25;
    private Boolean locked = false;
}

此时,当我们使用new关键字来实例化实体时,实体的各个字段将以默认值填充:

@Test
void saveUser_shouldSaveWithDefaultFieldValues() {
    User user = new User();
    user = userRepository.save(user);

    assertEquals(user.getName(), "Code demo");
    assertEquals(user.getAge(), 25);
    assertFalse(user.getLocked());
}

使用方法设置的默认值并未在数据表中定义中生效,查看相应的SQL语句如下:

create table user
(
    id     bigint not null constraint user_pkey primary key,
    name   varchar(255),
    age    integer,
    locked boolean
);

由以上SQL可知,该方法实际上并没有为数据表中的字段设置默认值,所以如果我们在代码中手动的将某个字段设置为null,并不会引发任何异常:

@Test
void saveUser_shouldSaveWithNullName() {
    User user = new User();
    user.setName(null);
    user.setAge(null);
    user.setLocked(null);
    user = userRepository.save(user);

    assertNull(user.getName());
    assertNull(user.getAge());
    assertNull(user.getLocked());
}

3. 定义数据表

在JPA中,我们可以使用@Column注解的columnDefinition参数来定义数据表字段的默认值:

@Entity
public class User {
    @Id
    Long id;

    @Column(columnDefinition = "varchar(255) default 'Code demo'")
    private String name;

    @Column(columnDefinition = "integer default 25")
    private Integer age;

    @Column(columnDefinition = "boolean default false")
    private Boolean locked;
}

使用上述方法定义后,JPA将对应生成以下SQL语句:

create table user
(
    id     bigint not null constraint user_pkey primary key,
    name   varchar(255) default 'Code demo',
    age    integer      default 35,
    locked boolean      default false
);

此时将未设置某个字段的值时,该字段将使用默认值填充:

@Test
void saveUser_shouldSaveWithDefaultSqlValues() {
    User user = new User();
    user = userRepository.save(user);

    assertEquals(user.getName(), "John Snow");
    assertEquals(user.getAge(), 25);
    assertFalse(user.getLocked());
}

值得注意的是:使用该方案在新建数据时,我们无法将某个字段的值设置null。因为如果我们将某个字段的值设置为null,则在进行数据保存操作时,将会以默认值来覆盖null值。

4. 总结

本文着重介绍了两个设置数据字段默认值的方法,在实际的应用中,还需要结合实际的情景来选择具体合适哪种方案。

https://www.baeldung.com/jpa-default-column-values

分类
Spring Data Testing

为spring应用单独配置测试环境数据库

1. 概述

很多Spring应用都需要关联数据库。而每次跑测试都对接真实的数据库有时候会让我们很头疼,所以如何在测试的环境中摒弃生产环境下的数据库而取而代之地使用一种更快、更小、对环境依赖程度更小的数据库便成为了急待解决的问题。

本文中将介绍几种为测试环境配置单独的数据库的方案。

注意:其实这里用『数据库(database)』并不正确,正确的说法应该是数据源(data source)。鉴于习惯,本文中使用了数据库来替待了数据源

2. MAVEN依赖

在正式编码之间,我们首先创建一个基本的Spring Boot Data项目,pom.xml文件如下:

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

<dependency>
	<groupId>com.h2database</groupId>
	<artifactId>h2</artifactId>
	<scope>runtime</scope>
</dependency>
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<scope>runtime</scope>
</dependency>

如上我们添加了Data JPA依赖以及H2数据库、mysql数据库依赖。

3. 项目配置

在Spring Boot项目中,默认的配置文件为:src/main/resources/application.properties。我们打开此文件并将项目的数据库配置为mysql:

spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:3306/db_example
spring.datasource.username=root

此时若运行本应用,则需要有一个服务于3306端口的本机数据库支持,用户名为root,密码为空,且存在db_example数据库。如果不存在满足以上条件的数据库,系统启动时便会发生一个异常。

4. 测试配置

与运行环境下生效的src/main/resources/application.properties文件对应,在测试文件夹中还可以存在(不存在的话手动建立即可)一个src/test/resources/application.properties文件,该文件只 在测试时起作用。在运行单元测试时,Spring首先查找src/test/resources/application.properties文件,如果存在则使用该文件做配置文件,如果不存在再去main目录下查找application.properties文件。

比如声明在测试中使用h2数据库,则可以在src/test/resources/application.properties文件中添加如下配置项:

spring.datasource.url=jdbc:h2:mem:db_example

此时,在运行测试时Spring应用便自动启用h2数据源且同时创建db_example数据库。所以即使没有满足生产环境配置下的数据库,测试也同样可以启动成功。

5. 自定义测试配置文件

除了直接在src/test/resources/application.properties文件中进行测试配置以外,还可以在单元测试中指定自定义配置文件。

比如新建src/main/resources/h2datasource.properties文件:

jdbc.url=jdbc:h2:mem:db_example
jdbc.username=root
jdbc.password=

接下来便可以根据该文件来创建一个Bean返回数据库信息了:

@Configuration
@PropertySource("classpath:h2datasource.properties")
@Profile("default")
public class H2DataSourceConfig {

    @Autowired
    Environment environment;

    /**
     * 定义数据源
     *
     * @return
     */
    @Bean
    DataSource dataSource() {
        DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
        dataSourceBuilder.url(this.environment.getProperty("jdbc.url"));
        dataSourceBuilder.username(this.environment.getProperty("jdbc.user"));
        dataSourceBuilder.password(this.environment.getProperty("jdbc.password"));
        return dataSourceBuilder.build();
    }
}

单元测试时,可以在@SpringBootTest中指定该配置文件来启用该配置文件:

@SpringBootTest(classes = {SpringTestingSeparateDataSourceApplication.class,
        H2DataSourceConfig.class})
public class H2DataSourceConfigTest {
    @Autowired
    StudentRepository studentRepository;

    @Test
    void contextLoads() {
        this.studentRepository.findAll();
    }
}

当然了,我们同样可以在src/test/resources/建立与src/main/resources/中的同名文件。然后就像上一小节一样,一个用于测试环境,另一个用于生产环境。这本节中在@SpringBootTest中指定测试文件并不冲突。

注意:自定义配置文件并不属于本文的重点,你可以点击使用内存数据库进行独立测试来了解更多的内容。

6. 使用Spring Profiles

还可以在单元测试中指定特定的Profile情景,以达到在某个单元测试中启用测试数据库的目的:

@SpringBootTest(classes = {
        SpringTestingSeparateDataSourceApplication.class,
        H2ProfileJPAConfig.class
})
@ActiveProfiles("test")
class H2ProfileJPAConfigTest {
    @Autowired
    StudentRepository studentRepository;

    @Test
    void dataSource() {
        this.studentRepository.findAll();
    }
}

@ActiveProfiles("test")表明:运行该单元测试时,强行使用test情景。

除了可以在项目配置文件(application.properties)中声明profile情景以外,还可以在使用@Profile注解

@Configuration
@Profile("test")
public class H2ProfileJPAConfig {

    /**
     * 定义数据源
     * @return
     */
    @Bean
    @Profile("test")
    public DataSource dataSource() {
        DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
        dataSourceBuilder.url("jdbc:h2:mem:db_example");
        dataSourceBuilder.username("root");
        dataSourceBuilder.password("");
        return dataSourceBuilder.build();
    }
}

上述两段代码结合在一起便实现了:当运行H2ProfileJPAConfigTest时强行使用test情景,近而启用了test情景下的h2内存数据库进行单元测试。

7. 总结

在测试中使用内存数据库将减小项目的配置难度,可以更快的运行测试。内存数据库与项目的依赖环境相同,从而不必为了测试项目而单独建立一个数据库。当我们同时负责多个项目,而每个项目生产环境的数据库版本都不相同时,使用内存数据库跑测试将事半功倍。同时由于内存数据库又小又轻,相较于搭建生产环境下使用的数据库而言,它还可以节省一些计算机资源。

分类
Spring Data

Spring Data JPA 创建、组合自定义仓库

1. 简介

在对真实世界或操作流程进行建模时,建立领域驱动设计domain-drven design(DDD)风格的数据仓库是个不错的选择。正是基于此,我们在数据访问层选择了Spring Data JPA。

如果您对Spring Data JPA还不太了解,推荐您首先阅读Spring Data JPA简介一文。

在本文中,我们将重点介绍创建自定义数据仓库以及如果组合使用自定义的数据仓库。

2. MAVEN依赖

在Spring自版本5开始支持创建及组合自定义数据仓库,添加Spring Data JPA依赖如下:

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>2.2.2.RELEASE</version>
</dependency>

如果你使用了Spring Boot,应该移除上述依赖中的version版本号(本文代码示例基于Spring Boot)。

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

无疑还要为Spring Data JPA准备一个数据库,比如开源数据库mysql。但在开发测试环境中我们会经常使用内存数据库H2来代替生产环境中的mysql,该数据不需要任何环境的特性极大程度上提长了开发测试效率,当然也特别适用于学习交流中的代码演示。添加依赖如下:

<dependency>
	<groupId>com.h2database</groupId>
	<artifactId>h2</artifactId>
	<scope>runtime</scope>
</dependency>

3. 准备工作

3.1 Hibernate实现

Spring Data JPA默认使用了Hibernate。以致于有些时候,我们误认为为Spring Data JPA就是Hibernate,但本质上来讲Spring Data JPA其实是一种规范,Hibernate则是该规范的一种实现。

在实际的应用中,除Hibernate以外还有其它Spring Data JPA实现可供选择。比如:在Spring中使用EclipseLink

3.2 默认仓库

在多数情况下,我们并不需要写过多的查询语句便可以轻松的实现一般的数据操作功能。在这里,我们仅需要创建一个类似如下的接口:

public interface StudentRepository extends JpaRepository<Student❶, Long❷> {
}
  • ❶实体类
  • ❷主键类型

该接口继承了JpaRepository接口,从而自动实现了:CRUD、分页、排序等基本的数据操作功能。

此外Spring Data JPA还提供了一种根据方法名称自动实现查询功能的机制。比如查询姓名为X的学生列表可以定义如下方法:

public interface StudentRepository extends JpaRepository<Student, Long> {
    List<Student> findByName(String name);
}

Spring Data JPA还提供了更多的查询方法,详情请参阅官方网站

3.3 自定义数据仓库

当Spring Data JPA提供的内置方法无法满足复杂的业务需求时,可以通过自定义数据仓库的方法来扩展原数据仓库。

比如使用自定义的CustomStudentRepository来扩展StudentRepository:

public interface StudentRepository extends JpaRepository<Student, Long>, CustomStudentRepository {
}

CustomStudentRepository中定义如下:

/**
 * 自定义学生仓库,该仓库做为为基础仓库的补充
 */
public interface CustomStudentRepository {
    /**
     * 删除学生
     *
     * @param student 学生
     */
    void deleteStudent(Student student);
}

最后,新建CustomStudentRepositoryImpl类并实现CustomStudentRepository。

public class CustomStudentRepositoryImpl implements CustomStudentRepository {
    @PersistenceContext
    EntityManager entityManager;

    @Override
    public void deleteStudent(Student student) {
        this.entityManager.remove(student);
    }
}

需要注意的是,此实现类的命名必须为: 接口名称 + Impl后缀。因为只有这样Spring Data JPA才能够正确地扫描并使用该实现类。如果你想修改后缀关键字Impl(比如修改为CustomImpl),则可以进行如下配置:

使用XML配置示例:

<repositories base-package="club.codedemo.springdatacomposablerepositories.repository" repository-impl-postfix="CustomImpl" />

JAVA配置示例:

@EnableJpaRepositories(
        basePackages = "club.codedemo.springdatacomposablerepositories.repository",
        repositoryImplementationPostfix = "CustomImpl")

4. 组合多个自定义仓库

在Spring 5版本以前,我们是没有办法将多个自定义仓库组合到一起来使用的。

Spring 5开始支持将多个自定义仓库组合在一起来使用,比如我们再增加一个自定义仓库:

public interface CustomTeacherRepository {
    void deleteTeacherByStudent(Student student);
}

然后便可以采用多实现的方法将CustomTeacherRepository与原CustomStudentRepository组合在一起来使用了:

public interface StudentRepository extends
        JpaRepository<Student, Long>,
        CustomStudentRepository,
        CustomTeacherRepository {

此时StudentRepository的实例将同时拥有CustomStudentRepository以及CustomTeacherRepository定义的方法,并在某个方法方法被调用时,聪明地调用不同的实现类中的具体方法而实现相应的功能。

5. 处理岐义

当我们继承多个接口时,便不可避免的遇到同一个方法被不同的接口重复声明的情况。当这种情况发生时,便在“应该调用哪个具体实现类”上产生了岐义。

比如我们同时在StudentRepository、CustomStudentRepository以及CustomTeacherRepository上分别定义findByIdOrName()方法:

List<Student> findByIdOrName(Long id, String name);

我们以StudentRepository为例,来查看岐义的处理原则。:

public interface StudentRepository❸ extends
        JpaRepository<Student, Long>❹,
        CustomStudentRepository❶,
        CustomTeacherRepository❷ {

当发生岐义时,调用的优先级为:❶ > ❷ > ❸ > ❹

总结:自定义的仓库❶❷中的方法执行优先级最高,所以❶❷ > ❸❹;❶CustomStudentRepository位于❷CustomTeacherRepository之前,所以❶优先级高于❷。在❸StudentRepository中定义的方法优先级高于Spring Data JPA默认实现❹JpaRepository中定义的方法。

6. 总结

本文对Spring Data JPA创建并使用自定义数据仓库进行介绍。我们看到,Spring的自定义仓库模式使得我们可以自由地对数据仓库进行扩展。

创建自定义可组合仓库的模式无疑将对数据仓库的灵活性有大幅的提升。

分类
Spring Data

Spring Data JPA @Query

1. 概述

Spring Data提供了多种数据查询方法,本文我们将讨论使用Spring Data JPA中的@Query注解的方式。该注解同时支持JPQL以及SQL语句。不过在处理一些动态查询时,@Query注解有时候便力不从心了,为弥补该缺憾,在文章的后半部分将给出如何使用JPA Criteria API进行动态查询。

JPQL(Java Persistence Query Language) 是Java持久层查询语言。 JPQL和SQL的语法接近,但不同于SQL直接查询数据表,JPQL是对JPA实体来进行查询。最终JPQL会根据不同的底层数据库转换为对应的SQL语句。因此,使用JPQL可以减小因数据库版本差异造成的影响。

2. select语句

可以在Spring Data中的仓库层定义查询方法并使用@Query注解标识以实现特定的数据查询,@Query注解中的value属性可以接收JPQL或SQL语句,当被注解的方法被调用时,相应的JPQL或SQL语句则会对应并执行,从而达到自定义数据查询的目的。

在优先级方面,@Query注解的优先级大于@NameQuery注解或是在orm.xml文件定义的查询映射。

在Spring Data JPA中,往往会将对数据的所有操作都习惯性的集中到某个仓库接口(类)中,@Query注解可以应用到数据仓库的方法中,所以在选型时只要@Query注解能实现的,我们应该优先使用。

2.1 JPQL查询

默认情况下@Query注解使用JPQL做为查询语言。让我们看看如何使用JPQL来查询出状态为1的所有学生:

@Query("SELECT u FROM User u WHERE u.status = 1")
Collection<User> findAllActiveUsersUsingJPQL();

2.2 Native原生查询

除使用JPQL语句外,还可以定义原生的SQL语句。使用原生语句时,仅需要将nativeQuery属性设置为true:

@Query(value = "SELECT * FROM USER u WHERE u.status = 1", nativeQuery = true)
Collection<User> findAllActiveUsersUsingNative();

3. 在查询中定义排序方式

我们在可以在@Query注解下的方法中传入类型为Sort的额外参数以实现排序的目的。

3.1 使用JPA提供的方法进行排序

JPA内置了像findAll(Sort)这样的开箱即用的方法,我们可以直接使用它在参数中传递Sort进行实现排序功能:

userRepository.findAll(Sort.by(Sort.Direction.ASC, "name")

但需要注意的是JPA中接收的Sort.by方法不能够使用诸如LENGTH之类的函数,如果我们传入如下的Sort,则会得到一个异常:

userRepository.findAll(Sort.by("LENGTH(name)"))

当我们执行上面的代码时,收到异常为:

org.springframework.data.mapping.PropertyReferenceException: No property LENGTH(name) found for type User!

未找到类型为User的属性lENGTH(name)

如你如见,将字符串LENGTH(name)做为参数传给Sort.by()方法时,LENGTH(name)并没有被当成一个特殊的方法来对待,而被当前了普通的属性。由于User类中并无lENGTH(name)属性,所以发生了上述异常。

3.2 JPQL

当我们使用JPQL作为查询语言时,JPA同样可以很轻松的处理排序问题。方法与上一小节相同,同样是直接传入一个Sort类型即可:

@Query(value = "SELECT u FROM User u")
List<User> findAllUsersSortUsingJPQL(Sort sort);

比如我们想按User的name属性对结果进行排序:

userRepository.findAllUsersSortUsingJPQL(Sort.by("name"))

与JPA不的是,@Query注解支持Sort中的诸如LENGTH之类的函数名,比如要实现按用户名称的长度来获取用户的排序列表

userRepository.findAllUsersSortUsingJPQL(JpaSort.unsafe("LENGTH(name)"));

创建Sort时,使用JpaSort.unsafe()至关重要。如果直接使用Sort.by,则将收到调用原生findAll(Sort.by("LENGTH(name)"))一模一样的异常 :

Sort.by("LENGTH(name)")

在使用@Query注解时,Spring Data发现unsafe不安全的排序方法时将跳过检查传入的排序字符串是否属于当前实体的属性,而是仅仅将sort子句添加到查询中。从而达到了LENGTH()方法生效的目的。

3.3 Native

@Query注解使用原生查询(即native=true)时,无法使用在参考中传入Sort的方式来达到排序的目的:

如果这样使用Sort:

    // @Query(value = "SELECT * FROM USERS u WHERE u.status = 1", nativeQuery = true)
    // List<User> findAllUsersSortUsingNative(Sort sort);

则将触发InvalidJpaQueryMethodException异常。

org.springframework.data.jpa.repository.query.InvalidJpaQueryMethodException: Cannot use native queries with dynamic sorting and/or pagination

无法将Native原生查询与动态排序和/或分页一起使用

控制台中的异常消息提示我们:JPA无法在原生的查询中动态地处理排序或是分页。

4.分页

分页功能允许我们仅在Page中返回所有数据的一部分。例如,在浏览网页上的几页数据时,当用户浏览到第一页时,服务器只需要返回第一页的数据即可。

分页的另一个优点是:相较于返回所有数据,返回某一页的数据减小了服务器发送到客户端的数据量。在同等情况下,较少的数据量意味着较高的性能。

4.1 JPQL

在JPQL查询定义中使用分页很简单:

@Query(value = "SELECT u FROM User u ORDER BY id")
Page<User> findAllUsersWithPaginationUsingJPQL(Pageable pageable);

Pageable是一个接口,而PageRequest是它的一个实现类。我们可以传递PageRequest参数来获取数据页面。原生查询也支持分页,但是需要一些额外的调整。

4.2 原生Native查询

我们可以通过声明附加属性countQuery启用原生查询的分页功能,该属性的主要作用是获取符合当前查询条件的总数,也更生成Page分页的具体信息(总共多少页,共多少条,是否为首页等):

@Query(
  value = "SELECT * FROM Users ORDER BY id", 
  countQuery = "SELECT count(*) FROM Users", 
  nativeQuery = true)
Page<User> findAllUsersWithPaginationUsingNative(Pageable pageable);

4.3 2.0.4之前的Spring Data JPA版本

针对原生查询的方法在Spring Data JPA 2.0.4及更高版本中均能正常工作。

在该版本之前,当执行此类查询时,我们将收到一个与上一节有关排序的内容相同的异常。

我们可以通过在查询中添加一个用于分页的附加参数,来克服这一问题:

@Query(
  value = "SELECT * FROM Users ORDER BY id \n-- #pageable\n",
  countQuery = "SELECT count(*) FROM Users",
  nativeQuery = true)
Page<User> findAllUsersWithPaginationUsingNativeBeforeJPA2_0_4(Pageable pageable);

在上面的示例中,我们添加“ \ n– #pageable \ n”作为分页参数的占位符。这告诉Spring Data JPA如何解析查询并注入pageable参数。

我们已经介绍了如何通过JPQL和原生SQL创建简单的选择查询。接下来,我们将展示如何定义其他参数。

5.索引参数查询

我们可以通过两种方法将方法参数传递给查询。在本节中,在本节中我们将介绍索引参数,下一节将介绍命名参数

索引参数命名参数本质是一样的。它们的不同点在于是否需要定义查询参数的名称以及是否需要注意参数的顺序:

  • 索引参数的模式下: 不需要定义查询条件的名称,但要保证查询条件的顺序和参数的顺序相同
  • 在命名参数的模式下:需要手动设置参数的名称,但不用考虑参数出现的顺序。

5.1 JPQL

对于JPQL中的索引参数,Spring Data会将方法参数按照在方法声明中出现的顺序传递给查询:

@Query("SELECT u FROM User u WHERE u.status = ?1")
User findUserByStatusUsingJPQL(Integer status);
 
@Query("SELECT u FROM User u WHERE u.status = ?1 and u.name = ?2")
User findUserByStatusAndNameUsingJPQL(Integer status, String name);

对于上述查询,status参数将被分配给查询参数?1,name参数将被分配给查询参数?2。方法中参数出现的顺序,和SQL中查询条件的顺序相对应。

5.2 Native

原生查询的索引参数与JPQL的工作方式完全相同:

@Query(
  value = "SELECT * FROM Users u WHERE u.status = ?1", 
  nativeQuery = true)
User findUserByStatusUsingNative(Integer status);

6.命名参数查询

我们还可以使用命名参数将方法来传递查询条件。使用@Param注解定义这些参数。

@Param注解中的参数字符串,必须与相应的JPQL或SQL查询参数名称匹配。与索引参数相比,使用命名参数易于阅读,并且在需要重构查询的情况下更不易出错

6.1 JPQL

如上所述,我们在方法声明中使用@Param注解,将JPQL中的参数与方法中的参数进行匹配:

@Query("SELECT u FROM User u WHERE u.status = :status and u.name = :name")
User findUserByStatusAndNameNamedParamsUsingJPQL(
  @Param("status") Integer status, 
  @Param("name") String name);

请注意,在上面的示例中,只要 Param中的字符串 和 JPQL中的查询条件 对应即可,不需要 传入的变量 和 查询条件 对应。所以以下在@Query中调换了name与status的写法也是完全正确的:

@Query("SELECT u FROM User u WHERE u.name = :name and u.status = :status")
User findUserByStatusAndNameNamedParamsUsingJPQL(
  @Param("status") Integer status, 
  @Param("name") String name);

6.2 Native

对于原生查询定义,与JPQL相比没有什么不同。同样是使用@Param注解,只不过语句换成了SQL:

@Query(value = "SELECT * FROM Users u WHERE u.status = :status and u.name = :name",
nativeQuery = true)
User findUserByStatusAndNameNamedParamsUsingNative(
@Param("status") Integer status, @Param("name") String name);

7.集合参数查询

让我们看下如何处理JPQL或SQL查询中若包含  IN(或NOT IN)关键字的情况:

SELECT u FROM User u WHERE u.name IN :names

上面JPQL的含义是:传入一个姓名集合,只要一个用户的姓名被包含在这个集合中,这条记录就会被查出来。在这种情况下,我们可以定义一个以Collection 为参数的查询方法  :

@Query(value = "SELECT u FROM User u WHERE u.name IN :names")
List<User> findUserByNameListUsingJPQL(@Param("names") Collection<String> names);

由于参数是Collection是集合接口,而所有的列表都继承自这个接口,因此可以与List,HashSet等一起使用。接下来,我们将展示如何使用@Modifying注解修改数据。

8.使用@Modifying执行更新操作

我们还可以通过将@Modifying注解添加到仓库层的方法上,并结合@Query注解中的语句,来完成对数据库进行更新操作。

8.1 JPQL

select查询相比,update方法有两个区别:一是它具有@Modifying注解,二是查询使用update关键字,而不是select

@Modifying
@Query("update User u set u.status = :status where u.name = :name")
int updateUserSetStatusForNameUsingJPQL(@Param("status") Integer status, 
  @Param("name") String name);

索引参数和命名参数都可以在更新语句中使用。此外如果声明了返回值,则返回值中会给出“执行相关的方法后具体更新了多少行记录”。

8.2 Native

我们还可以在原生查询上使用@Modifying注解以达到修改数据的目的:

@Modifying
@Query(value = "update User u set u.status = ? where u.name = ?", nativeQuery = true)
int updateUserSetStatusForNameUsingNative(Integer status, String name);

8.3 Inserts 执行插入操作

由于INSERT并不是JPA接口的一部分,所以执行插入操作我们必须同时应用@Modifying并使用原生查询

@Modifying
@Query(value = "insert into User (name, status, email) values (:name, :status, :email)", nativeQuery = true)
void insertUserUsingNative(@Param("name") String name, @Param("status") Integer status, @Param("email") String email);

9.动态查询

通常我们会遇到这种情况:软件运行前无法确定具体的SQL语句,而只有运行时才知道某些参数的值,近而再根据这些值来构建不同的SQL语句。在这种情况下静态查询便不能胜任了,此时便需要借助于Criteria来实现动态查询。

9.1 动态查询的例子

假设我们需要查询电子邮件包含在一组列表中的用户,而此电子邮件列表是动态生成(不确定)的:

SELECT u FROM User u WHERE u.email LIKE '%email1%' 
    or  u.email LIKE '%email2%'
    ... 
    or  u.email LIKE '%emailn%'

由于电子邮件列表是动态构造的,因此在编译时我们无法知道要添加多少个LIKE子句。

那么这种情况下,我们使用@Query注解便无法完成功能需求了,因为我们无法提供静态SQL语句

此时我们可以通过实现定制一个动态的查询方法,并在该方法中根据当前的实际逻辑来动态的生成查询语句。最后,为了让其能够与Spring Data JPA完结合,再按一定的规范来组织接口、类与方法,从而达到自定义动态查询方法的目的。

9.2 自定义仓库与JPA Criteria API

Spring Data 创建自定义可组合仓库一文详细的表达了如果自定义仓库来优雅地完成动态查询。基本步骤如下:

创建自定义的接口,并在接口中定义查询方法、参数:

public interface UserRepositoryCustom {
    List<User> findUserByEmailsUsingCriteria(Set<String> emails);
}

接下来,新建一个上述接口的实现类,该类的名称必须为 接口名+Impl:

public class UserRepositoryCustomImpl implements UserRepositoryCustom {
    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List<User> findUserByEmailsUsingCriteria(Set<String> emails) {

        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<User> query = cb.createQuery(User.class);
        Root<User> user = query.from(User.class);

        Path<String> emailPath = user.get("email");

        List<Predicate> predicates = new ArrayList<>();
        for (String email : emails) {
            predicates.add(cb.like(emailPath, email));
        }
        query.select(user)
                .where(cb.or(predicates.toArray(new Predicate[predicates.size()])));

        return entityManager.createQuery(query)
                .getResultList();
    }
}

如上所示,我们利用了JPA Criteria API在findUserByEmailsUsingCriteria中构建了动态查询。

另外,我们需要确保在类名为:接口名+Impl因为只有这样Spring才会认为UserRepositoryCustomImpl是UserRepositoryCustom在仓库层面的实现类。Spring依靠此接口名+Impl机制来查找仓库的实现类。如果名称不对应,Spring会把findUserByEmailsUsingCriteria接口当成仓库的内部方法,并尝试对其进行解析,在这种情况下Sring会根据方法来尝试User实体emailsUsingCriteria字段,由于User实体并不存在该字段,所以最终会触发“EmailsUsingCriteria属性不存在”的异常。

9.3 扩展现有仓库层

我们通过继承JpaRepository仓库的同时继承自定义UserRepositoryCustom来达到扩展UserRepository的目的。

public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {
    // 第2-7节中的代码
}

此时,当调用Spring自动生成的UserRepository对象的findUserByEmailsUsingCriteria方法时,将是终调用UserRepositoryCustomImpl.findUserByEmailsUsingCriteria()方法。

9.4 使用仓库层

最后,我们可以如下调用动态查询方法:

Set<String> emails = new HashSet<>();
// 在集合中添加一些邮箱...
emails.add("test@test.com");
List<User> users = userRepository.findUserByEmailsUsingCriteria(emails);

10.总结

在本文中,我们介绍了使用@Query注解在Spring Data JPA仓库方法中定义查询的几种方法。包括使用JPQL、使用原生SQL、以及如何自定义仓库层,并使用Criteria实现动态查询。

分类
Spring Data

Spring Data JPA 简介

1. 概述

本文将围绕Spring项目中的Spring Data JPA展开介绍。如果你尚需要了解如何使用Spring来构造一个基本的应用程序,那么可以先阅读此篇文章。Spring JPA是一种新颖、优雅的数据访问方式,它允许我们在只定义接口及方法名称的前提下,快速的实现对数据库的访问。

2. Spring Data中的DAO

DAO模式:Data Access Object数据访问(存储)对象模式,简单来讲就是使用JAVA语言来快速、简单的操作数据库。

该模式一般由以下部分组成:

DAO接口:将对数据库的操作定义为抽象方法,比如定义save方来新增数据。

DAO实现类:负责DAO接口中抽象方法的具体实现。往往会根据数据库类型而给出不同的实现。

实体类:用于与数据库中的数据表进行映射。DAO可以将实体对象操作到数据表中,也可以将查询出的数据绑定到实体上。

基础类:为一些样板代码提供一些基础的支持,避免写过多的样板化代码。

https://www.runoob.com/note/27029

使用Spring简化DAO层的样板代码一文中,我们阐述了DAO层通常包含了过多重复的样板化的代码。对样板代码简单可以减少代码体量,降低维护与升级的成本,统一数据访问方式、配置信息等。

Spring Data对样板代码的简化做到了极致,在Spring Data中我们只需要定义相关接口即可,完全不需要对该接口进行实现。

若使定义的DAO接口生效,则需要使其继承JPA中指定的仓库接口 -- JpaRepository。Spring Data将会自动扫描继承了JpaRepository的接口并自动为其创建一个实现。

通过继承JpaRepository的方法,我们可以轻松的获取到一个包含了CRUD操作的标准DAO。

3. 自定义方法以及查询

虽然能过继承JpaRepository已经实现了基本的CRUD操作,但大多数的项目中,仅仅有CRUD操作是远远不够的。

为此,Spring JPA提供了多种自定义数据操作的方法:

  • 直接在接口中定义一个新的方法,并使用支持JPQL的@Query注解来进行标识
  • 使用高级用法:SpecificationQuerydsl
  • 通过JPA命名查询(Named Queries)来自定义查询方法

第二种方法与JPA的标准查询比较相似,不同的是使用这种方法将更加灵活、方便。这将使我们的代码具有更高的可读性,可复用性也会更强。特别是当我们处理一些复杂的查询逻辑时,这种方法的优势将更加突出。

3.1 仅需要定义方法名

当Spring Data扫描到继承了JpaRepository的接口并生成实现时,Spring Data将扫描接口定义的方法并尝试依据方法名将其自动转换为特定的查询语句。尽管说这种依据方法名来自动生成查询语句的方法有一定的局限性,但这种方法使用起来真的是太强大、太方便、太优雅了。

假设当前有学生(student)实体,我们此时想以学生姓名做为关键字进行查询,则仅仅需要在定义的DAO接口中加入以下方法:

/**
 * 操作学生表的DAO
 *
 * @author panjie
 */
public interface StudentDAO extends JpaRepository<Student, Long> {
    /**
     * 通过姓名查找某个学生
     *
     * @param name 学生姓名
     * @return
     */
    Student findByName(String name);
}

没错,仅仅需要定义一个方法,Spring Data将自动按此方法名称转换为:根据关键字来查询某个学生。这种查询方法支持很多种关键字,如果在不需要对逻辑进行处理时,不失为最佳的一种方法。更多的关键字请参考官方文档

当然了,在使用上述方法时,需要保证实体中的字段信息与方法中给出的信息一致,以避免发生异常。比如findByUsername方法的成功执行依赖于学生实体(student)中存在username字段,而此时学生实体并不存在userName字段,则解析器将抛出如下异常:

club.codedemo.thepersistencelayerwithspringdatajpa.StudentDAO.findByUsername(java.lang.String)! No property username found for type Student!

3.2 自定义查询语句

可以在方法上添加@Query注解以自定义查询语句:

    /**
     * 不区分大小写的根据name查询某个学生
     * 比如通过Zhangsan可以查询出学生名为zhangsan、zhangSan等的学生
     *
     * @param name 学生姓名
     * @return
     */
    @Query("SELECT s FROM Student s WHERE LOWER(s.name) = LOWER(:name)")
    Student retrieveByName(@Param("name") String name);

欲了解更多关于自定义查询语句的知识,请参考官方文档

4. 事务配置

Spring自动实现接口的特性决定了我们无法直接获取到整个实现过程,当然也无法得知Spring Data JPA是如何对事务进行配置的了。值得庆幸的事,我们可以通过观察Spring Data JPA中的org.springframework.data.jpa.repository.support.SimpleJpaRepository类了解其事务配置。

Spring使用了@Transaction(readOnly = true)对该类进行注解,表示该类中的方法默认采用的都是只读(read-only)模式。接着又在个别的非查询模式的方法上加入了@Transaction,从而覆盖了在类上标注的只读模式,近而达到了:如果该类中的某个方法上没有使用@Transaction注解,则标识在该类上的@Transaction(readOnly = true)起作用;如果某个方法使用了@Transaction注解,则忽略类上的@Transaction(readOnly = true)注解。

4.1 异常转换依然有效

现在可能你有一个疑问:既然Srping Data JPA并不依赖于已经在Spring5中移除的历史的模板引擎JpaTemplate与HibernateTemplate,那么我们是否仍要将JPA异常转换为Spring的DataAccessException呢?

答案是肯定的,我们仍然可以在DAO层使用@Repository注解来开启异常转换功能。@Repository注解将自动获取bean中的PersistenceExceptionTranslator并将其转换为我们熟知的DataAccessException

比如执行以下代码最终将获取DataIntegrityViolationException(该异常是DataAccessException的子类):

    /**
     * 由于Student实体中要求name字段不能为null
     * 所以保存一个name字段为null的默认学生实体时,将会发生DataIntegrityViolationException异常
     */
    @Test
    void givenStudentHaveNoName() {
        Student student = new Student();
        Assertions.assertThrows(DataIntegrityViolationException.class,
                () -> this.studentDAO.save(student));
    }

需要时刻提醒自己的是:和很多注解一样,异常转换是通过代码模式完成的。所以相关的方法绝对不能够声明为final

5. Spring Data JPA仓库配置

使用@EnableJpaRepositories来启用Spring JPA的仓库支持(自动根据接口创建DAO实现),同时在该注解中需要同时指定扫描的基础包:

/**
 * 测试EnableJpaRepositories注解时,请 注释/启用 11,12,13行后
 * 分别查看单元测试EnableJpaRepositoriesTest中获取bean的效果
 */
@SpringBootApplication
@EnableJpaRepositories(
		basePackages = "club.codedemo.repository"
)
public class ThePersistenceLayerWithSpringDataJpaApplication {

使用XML的话,配置如下:

<jpa:repositories base-package="com.baeldung.spring.data.persistence.repository" />

6. JAVA或XML配置

在认识Spring JPA一文中已经对如何在Spring中配置JPA进行了深入了讨论。除了在前文中讨论的内容以外,如果说我们使用XML进行配置,则需要在@ImportResource中指定XML的位置:

@Configuration
@EnableTransactionManagement
@ImportResource("classpath*:*springDataConfig.xml")
public class PersistenceJPAConfig {
    ...
}

7. MAVEN依赖

就像前文中提及的一样,使用JPA还需要加入spring-data-jpa依赖:

<dependency>
   <groupId>org.springframework.data</groupId>
   <artifactId>spring-data-jpa</artifactId>
   <version>2.2.7.RELEASE</version>
</dependency>

8. 使用Spring Boot

我们还可以使用Spring Boot Starter Data JPA依赖,其将会自动的为项目配置数据源。

当然了,自动配置数据源的前提是需要让其能够检测到当前项目使用的数据源,如果在项目中并没有提供任何数据源,则将发生一个异常。比如我们在项目中添加H2内容数据库:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-jpa</artifactId>
   <version>2.2.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.200</version>
</dependency>

则项目在启动时,将自动加载H2数据库做为数据源。

在未进行任何配置的情况下,Spring Boot在启动时,将会按标准的默认配置进行加载。这些默认的配置项可以轻松的通过application.properties文件进行更改:

spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=sa

比如我们通过以代码来修改数据源的地址以及数据库的认证信息。

9. 总结

本文对Spring 5、JPA2、Sptring Data JPA中数据持久化层的配置及实现进行介绍。在介绍的过程中给出了基于xml以及基于java的配置示例。

本文还讨论了自定义高级查询的几种方法、事务机制、异常转换以及如何扫描一个项目外的jpa命名空间。

总之Spring JPA是一种新颖、优雅的数据访问方式,它允许我们在只定义接口及方法名称的前提下,快速的实现对数据库的访问。

分类
Spring Data

Spring Data JPA 条件查询

1. 简介

Spring Data JPA中提供了多种数据查询的方式,比如说定义查询方法或使用自定义的JPQL。但有些时候,上述查询方式并不能够很好的满足我们对复杂综合查询的需要,这时候就需要Criteria APIQueryDSL登场了。

Criteria API提供了一种使用代码创建动态查询语句的功能,相较于编写传统的SQL语句,该方法能够有效的避免一些语法错误。当结合Metamodel API使用时,甚至可以在编译的阶段发现一些字段或类型的错误。

当然,凡是都有两面性,它的缺点是:即使实现一个看似不起眼的查询方法,也需要编写长篇的样板(只所以称为样板是由于每次写的都差不多)代码,这使得阅读代码的时候不是很爽;再者由于需要接受一些新的知识,所以对于初次接触Criteria API的小伙伴们并不友好。

本文将讨论如何使用criteria queries(条件查询)来实现自定义的数据访问逻辑,以及Spring是如果帮助我们减少样板代码的。

2. 需求假设

假设如下图书(Book)实体,我们需要实现的功能是:查询某个作者(author)并且图书名称包含特定的关键字的所有图书。

/**
 * 图书
 */
@Entity
public class Book {
    @Id
    @GeneratedValue
    private Long id;

    /**
     * 名称
     */
    String title;

    /**
     * 作者
     */
    String author;
    // setter and getter
}

本着一事一议、尽量减少干扰项的原则,本文引入Metamodel API。

3. 数据仓库类

按Spring规范,我们使用@Repository 来标识自定义数据仓库。要实现Criteria API还需要在数据仓库中注入EntityManager实例:

/**
 * 图书数据访问对象
 */
@Repository
public class BookDao {

    /**
     * 实体管理器
     */
    @Autowired
    EntityManager em;

    /**
     * 查询图书
     * @param authorName 作者名称(精确查询)
     * @param title 图书名称 模糊查询
     * @return
     */
    List<Book> findBooksByAuthorNameAndTitle(String authorName, String title) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Book> cq = cb.createQuery(Book.class);

        Root<Book> book = cq.from(Book.class);
        Predicate authorNamePredicate = cb.equal(book.get("author"), authorName);
        Predicate titlePredicate = cb.like(book.get("title"), "%" + title + "%");
        cq.where(authorNamePredicate, titlePredicate);

        TypedQuery<Book> query = em.createQuery(cq);
        return query.getResultList();
    }
}

上述代码遵循了标准的Criteria API 工作流:

  • 首先获取了一个CriteriaBuilderCriteriaBuilder是整个查询功能的基础。
  • 使用CriteriaBuilder创建了一个CriteriaQuery<Book>,泛型中的Book指明了我们想获取的数据是Book实体。
  • 使用CriteriaQuery<Book>的from方法来获取了一个Root<Book> book,指定了此次查询的起点为Book实体(对应book) 数据表。
  • 接下来使用CriteriaBuilder创建了两个基于Book实体的查询条件(仅仅是创建了一个查询条件,只有将其放入正式的查询中,该查询条件才会生效)。查询条件一用于查找book表中的author字段与传入authorname完全相同的记录;条询条件二用于查找book表的title字段左右模糊匹配title字段。
  • cp.where将两个查询条件组合到一起,表示:查询满足查询条件一并且同时满足查询条件二的数据。
  • 然后使用CriteriaQuery创建了一个TypedQuery<Book>实例,该实例具备了数据查询的能力。
  • 最后调用了TypedQuery的getResultList()方法,此时将综合查询条件进行数据查询,并且将获取的数据依次填充到CriteriaQuery<Book>上规定的<Book>实体上。

值得提出的一点时,由于我们在当前数据访问对象BookDao使用@Repository注解,所以spring将默认托管该类所产生的异常。

4. 使用自定义的查询的方法扩展数据仓库层

虽然我们可以使用Spring Data中根据方法名称自动创建动态查询的功能来完成一些个性化的查询工作。但当有一些些逻辑处理参杂在查询过程中时(比如对null值进行过滤),这种自动创建查询方法的功能便力不从心了。

当然了,我们完全可以像上一节展示的一样来创建自定义的数据仓库来实现一些复要的查询功能。

但我想说的是,参考Spring Data组合数据仓库一文将一些自定义的方法添加到JPA的@Repository接口机制,使用继承接口的方法来完成数据创建的初始化,会有一种更好开发体验。

比如我们创建如下BookRepositoryCustom接口:

/**
 * 自定义的Book数据仓库
 */
public interface BookRepositoryCustom {
    /**
     * 查询图书
     * @param authorName 作者名(精确)
     * @param title 图书名称(模糊)
     * @return
     */
    List<Book> findBooksByAuthorNameAndTitle(String authorName, String title);
}

然后在创建的数据仓库接口中继承BookRepositoryCustom:

interface BookRepository extends JpaRepository<Book, Long>, BookRepositoryCustom {}

值得注意的是,如果想使用BookDao中的findBooksByAuthorNameAndTitle方法做为BookRepositoryCustom中方法的实现,则必须将BookDao重命名为BookRepositoryImpl,同时实现BookRepositoryCustom接口。

在此,我们保持原BookDao名称不变。并新建BookRepositoryImpl:

/**
 * 图书数据访问对象
 */
@Repository
public class BookRepositoryImpl implements BookRepositoryCustom {

    /**
     * 实体管理器
     */
    @Autowired
    EntityManager em;

    /**
     * 查询图书
     * @param authorName 作者名称(精确查询)
     * @param title 图书名称 模糊查询
     * @return
     */
    @Override
    public List<Book> findBooksByAuthorNameAndTitle(String authorName, String title) {
    }
}

然后使用另外一种方法来实现查询的功能,该功能将对排除掉值为null的传入参数:

@Override
List<Book> findBooksByAuthorNameAndTitle(String authorName, String title) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Book> cq = cb.createQuery(Book.class);
 
    Root<Book> book = cq.from(Book.class);
    List<Predicate> predicates = new ArrayList<>();
    
    if (authorName != null) {
        predicates.add(cb.equal(book.get("author"), authorName));
    }
    if (title != null) {
        predicates.add(cb.like(book.get("title"), "%" + title + "%"));
    }
    cq.where(predicates.toArray(new Predicate[0]));
 
    return em.createQuery(cq).getResultList();
}

其实无论是哪种方法,都可以发现同一个特点:代码像懒婆娘的裹脚布一样,又臭又长。

接下来,我们继续展示使用JPA Specifications来掀开这又臭又长的裹脚布。

5. JPA Specifications

Spring Data 提供了org.springframework.data.jpa.domain.Specification来简化查询操作时的样板代码:

interface Specification<T> {
    Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb);
}

该接口提供了toPredicate方法,在方法中注入了样板代码中需要的root、CriteriaQuery以及CriteriaBuilder。我们可以利用该接口来快速的创建查询条件:

/**
 * 查询作者
 * @param author 作者
 * @return
 */
static Specification<Book> hasAuthor(String author) {
    return (book, cq, cb) -> cb.equal(book.get("author"), author);
}
 
/**
 * 查询图书名
 * @param title 名称
 * @return
 */
static Specification<Book> titleContains(String title) {
    return (book, cq, cb) -> cb.like(book.get("title"), "%" + title + "%");
}

在使用上述查询条件以前,我们还需要在数据仓库上继承org.springframework.data.jpa.repository.JpaSpecificationExecutor<T>

interface BookRepository extends JpaRepository<Book, Long>, JpaSpecificationExecutor<Book> {}

此时,当前数据仓库层便能够处理Spring Data的Specification查询条件了。此时,如若获取某个作者的所有图书,则可以简短的这样写:

bookRepository.findAll(hasAuthor(author));

美中不足的是,JpaSpecificationExecutor提供的findAll仅仅支持传入一个Specification查询条件。幸运的是我们可以使用org.springframework.data.jpa.domain.Specification接口提供的方法来完成查询条件的组合。

比如将作者与图书名两个查询条件组合在一起进行综合查询:

bookRepository.findAll(where(hasAuthor(author)).and(titleContains(title)));

上述代码中,我们使用了Specification接口上的where方法来完成了两个查询条件的组合。

这样以来,得益于伟大的Spring,我们在实现了查询模块化(可以轻易的按某一个或多个条件进行组合查询)的同时,还让代码看起来没有那么臃肿。

当然了,这并不意味着我们构造综合查询时可以完全的脱离样板化的代码。在有些时候,合适样板化代码来完成一些特定的查询功能也是非常有必要的。比如我们在查询中若想使用group进行分组,或是查询A数据表后,将数据填充到B实体上,再或者进行一些子查询。

条条大路通北京。综合查询的实现也是一样,没有最好的方法,只有最合适的方法。在实际的使用过程中,应该根据当前的业务需求,尝试选择出最合适的那个查询方式。

6. 总结

本文中先后介绍了3种不同的综合查询方法:

  • 最简单、直接的创建自定义的DAO
  • @Repository进行扩展,就像访问标准的查询方法一样调用综合查询方法
  • 使用Specification减少样板化的代码

同样,你可以打开文章顶部的github地址来获取一份与本文相同的且可运行的code demo。不止如此,在code demo中我们还提供了用于验证的单元测试代码。希望能对你有所帮助。