分类
persistence

在JPA中的定义索引

译者注

原文地址:https://www.baeldung.com/jpa-indexes
实例代码:https://github.com/eugenp/tutorials/tree/master/persistence-modules/java-jpa-3

1. 概述

本文我们将讨论使用JPA的@Index注解来定义索引。在实例中,我们首先讲解如何使用JPA和Hiernage来定义第一个索引。接下来讲解更多关于自定义注解的知识。

2. @Index 注解

数据库索引是一种数据结构,它以增加的写入和存储空间为代价,提高了对表进行数据检索操作的速度。通常来说,它是从某个单表中选择的数据库的副本。我们应该合理的为数据表创建索引来提高数据库的查询性能。

JPA允许我们在代码中使用@Index注解来定义数据表索引。该注解能够实现在创建(更新)数据表时自动为数据表添加索引。

接下来我们看一下如何使定义索引:

2.1. javax.persistence.Index

自JPA2.1版本,JPA增加了java.prsistence.Index来允许我们为数据表创建注解:

@Target({})
@Retention(RUNTIME)
public @interface Index {
    String name() default "";
    String columnList();
    boolean unique() default false;
}

如上代码所示,@Index注解中仅有columnList属性是必填的。后面我们将具体使用该注解来具体的查看各个属性的用法。

2.2. JPA 和 Hibernate

我们常弄不清JPA与Hibernate的关系。我们知道JPA仅仅是一个规范,它除了做了一些规定提供了一些愿景(也可以说模板、蓝图)以外,其它什么也没做。而要其使愿景便成事实,则需要有人来做实际的工作将其实现。在Spring中使用了Hibernate来完成了这个愿景,你可以参考本文来进一步学习。当然了,除了 Hibernate以外还有其它的JPA实现,比如EclipseLink。所以JPA就好像面向对象思想中的接口,而HibernateEclipseLink则都是这个接口的具体实现

值的注意的是由于JPA添加索引支持比较晚,而在这之前多个ORM框架均有自己支持索引的方法。这使得在JPA统一规范以前,出现了各个ORM各自为政的情况。比如Hibernate就有自己的org.hibernate.annotations.Index注解。所以我们在使用索引注解时,一定上要确认使用的是JPA下的注解 ---- java.prsistence.Index(而不是其他框架中的同名注解)。

简单的了解一些技术背景了,让我们正式开始。

3. 定义@Index

在定义索引前,我们需要了解一些如何定义JPA实体的相关知识。

我们实现一个Student学生实体如下:

/**
 * 学生.
 *
 * @author panjie
 */
@Entity
@Table
public class Student implements Serializable {
  @Id
  @GeneratedValue
  private Long id;

  /**
   * 姓名.
   */
  private String name;

  /**
   * 学号.
   */
  private String sno;

  // setters getters
}

接下来,我们在@Table注解中加入相关的@Index注解。

@Table(indexes = @Index(columnList = "name"))

如上我们创建了第一个基于name的索引,此时数据表在初始化时,控制台的日志信息中将对应生成以下Sql代码:

// 中间的值是自动生成的,不会完全一致
[main] DEBUG org.hibernate.SQL -
  create index IDX2gdkcjo83j0c2svhvceabnnoh on Student (name)

接下来我们索引的其他属性和功能。

3.1. 索引名称

不难看出,索引必须有一个名称。默认情况下我们没有指定名称,那么将会使用自动生成的值。
如果我们需要自定义一个名称,只需要修改name属性:

@Index(name = "test_index", columnList = "name")

如代码所示,我们创建了一个名为test_index的索引。
在数据表初始化时,日志就会变成:

[main] DEBUG org.hibernate.SQL -
  create index test_index on Student (name)

此外,我们可以通过在名称中指定模式(schema)的名称,以便在不同的模式(schema)中创建索引:

@Index(name = "schema2.test_index", columnList = "name")

3.2. 在多个列上定义一个@Index

columnList语法如下:

column ::= index_column [,index_column]*
index_column ::= column_name [ASC | DESC]

刚才我们提到,索引是对于一个列的索引,也必须包括列名。然而,我们也可以在单个索引中指定多个列。
方法就是用逗号来分隔名称:

@Index(name = "mulitIndex1", columnList = "name, phoneNumber")

@Index(name = "mulitIndex2", columnList = "phoneNumber, name")

生成的日志:

[main] DEBUG org.hibernate.SQL -
  create index mulitIndex1 on Student (name, phoneNumber)

[main] DEBUG org.hibernate.SQL -
  create index mulitIndex2 on Student (phoneNumber, name)

需要注意的是,如果我们定义多列索引,索引的前后顺序必须相同。如果顺序变化,即使使用的同一组列,也是两个不同的索引,如代码所示。

3.3. @Index 索引排序(Order

我们还可以在列名column_name之后指定ASC(升序)或DESC(降序),来控制索引值的排列顺序:

@Index(name = "mulitSortIndex", columnList = "name, phoneNumber DESC")

日志:

[main] DEBUG org.hibernate.SQL -
  create index mulitSortIndex on Student (name, phoneNumber desc)

3.4. 索引唯一性(Unique

还有一个参数是unique属性,它用来定义索引是否唯一。“唯一”索引确保索引的字段不存储重复值(就像平时常见的ID或者学号等等)。
默认情况下它是false,如果想启用唯一性,可以这样声明:

@Index(name = "uniqueIndex", columnList = "name", unique = true)

日志:

[main] DEBUG org.hibernate.SQL -
  alter table Student add constraint uniqueIndex unique (firstName)

当我们在某个列上使用唯一索引的时候,就相当于再这个列上添加了一个唯一性约束。它的作用和“在@Column注解上使用约束”是相似的。
但由于@Index可以声明多个列的唯一约束(相当于联合主键,单个列的数据允许重复,只有约束中的所有列的数据都重复,才不被允许),使用@Index要优于使用@Column:

@Index(name = "uniqueMulitIndex", columnList = "firstName, lastName", unique = true)

3.5. 在单个实体上定义多个@Index索引

到目前为止,我们已经学习了索引的各个参数的功能。
而事实上,我们可以在一个实体上定义多个索引,多个索引的定义方法是——在大括号中重复使用@Index注解:

@Entity
@Table(indexes = {
  @Index(columnList = "firstName"),
  @Index(name = "test_index", columnList = "name"),
  @Index(name = "mulitIndex1", columnList = "name, phoneNumber"),
  @Index(name = "mulitIndex2", columnList = "phoneNumber, name"),
  @Index(name = "mulitSortIndex", columnList = "name, phoneNumber DESC"),
  @Index(name = "uniqueIndex", columnList = "name", unique = true),
  @Index(name = "uniqueMulitIndex", columnList = "name, phoneNumber", unique = true)
})
public class Student implements Serializable

此外,我们还可以为同一组列创建多个索引。

3.6. 主键

当我们谈论索引时,必须要考虑主键。因为被实体管理器EntityManager来管理的每个实体都必须指定一个主键。
一般来说,主键是一种特定类型的唯一索引,与其他索引不同的是,主键不需要用前面介绍的@Index来声明,因为@Id 注解自动实现了这个功能。

3.7. 对于非实体(Entity)对象使用@Index

我们需要知道的是,@Index注解不仅仅能用在表(@Table)上,也能用于某些其他的对象,例如@SecondaryTable(多表联合)、@CollectionTable(集合表)、@JoinTable(连接表)、@TableGenerator(表生成器),这些知识不再详细讲解,请查看javax.persistence JavaDoc

4. 结论

本文中我们讨论了如果使用JPA中的@Index索引,包括更改名称、包含的列、顺序、以及唯一性。后面我们讨论了主键,以及如何在其他对象中声明索引。

简单总结一下:

  1. 索引名称name,不使用名称时会自动生成
  2. 多个列columnList,列的顺序必须一致
  3. 排序ASCDESC
  4. 唯一性unique = true
  5. 主键不需要使用@Index,因为@Id完成了这个功能

本文中的示例可在 GitHub 上找到。

分类
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 Persistence Testing

Spring JPA使用内存数据库进行测试

1. 概述

本文将构建一个简单的Spirng应用,并在该应用中使用内存数据库进行单元测试。

往往在开发过程中我们习惯于使用与生产环境相同的数据库,比如MySQL。这就要求在进行开发时,必须在本地准备一个与生产环境相同的数据库环境。

在进行单元测试时,由于每个测试用例都是测试的某一小部分功能,这决定了大多数的单元测试可以不依赖于与生产环境相同的数据库。此时,为这部分单元测试提供快速、轻量化的内存数据库便有了必要。

下面,我们将共同学习如何在单元测试中使用h2内存型数据库来替代MySQL数据库。

2. Maven依赖

我们在Spring Boot(2.3.3.release)项目中的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>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
	<scope>test</scope>
	<exclusions>
		<exclusion>
			<groupId>org.junit.vintage</groupId>
			<artifactId>junit-vintage-engine</artifactId>
		</exclusion>
	</exclusions>
</dependency>

3. 数据模型及仓库

我们首先创建一个Student学生实体

@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    // 空构造函数及setter/getter请自行补充
}

然后创建该Student学生实体对应的数据仓库StudentRepository

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

4. 为测试指定H2数据库

为测试指定与运行不同的数据库,我们需要为测试指定一个特定的配置文件。然后设置该配置文件仅在测试环境下生效。

Spring Boot的配置文件位于src/main/resources文件夹中。如果要定义专门用于单元测试的配置文件,则应该将其放置到src/test/resources文件夹中。

Spring在运行单元测试时,首先在src/test/resources查找相应的配置文件。如果在文件夹中没有找到,则会使用src/main/resources文件夹中的文件;如果找到了,则会使文件下的文件而跳过src/main/resources文件夹的查找。

4.1 测试专用配置

如果我们所有的单元测试都想依赖于H2数据库(这虽不是一个好的选择,但有时候在快速切换数据环境时很有效),则可以直接在src/test/resources新建application.properties,并在该文件中将数据数连接信息设置为H2:

spring.datasource.url=jdbc:h2:mem:myDb;DB_CLOSE_DELAY=-1
spring.datasource.username=root
spring.datasource.password=

spring.jpa.show-sql=true

此时当运行单元测试时,Spring将默认加载此文件做为应用的配置文件,进行在单元测试中启用了h2数据库:

@SpringBootTest
class StudentRepositoryTest {
    @Autowired
    StudentRepository studentRepository;

    /**
     * 由于本机并没有安装mysql(即使安装了,也没有exampleDb数据库)
     * 所以如果本方法被成功执行,则说明当前单元测试连接的为H2数据库
     */
    @Test
    void findAll() {
        this.studentRepository.findAll();
    }
}

4.2 自定义测试配置文件

如果我们仅仅是需要在部分的单元测试中启用H2数据库而非全部测试,那么单独定义一个测试配置文件是个不错的选择。比如在src/test/resources建立student.properites,专门用于测试对student数据表的操作:

注意:src/main/resources建立也是相同的。本节在起初位置已经介绍了:Spring进行单元测试在查找某个配置位置时会优先查找src/test/resources文件夹,如果找不到则还会查找src/main/resources文件夹。

url=jdbc:h2:mem:myDb;DB_CLOSE_DELAY=-1
# 若使用注释的值设置url,则在StudentJpaConfigTest测试时将尝试连接mysql数据库,进而会发生异常。
# 这说明了此设置文件在单元测试时生效了
# url=jdbc:mysql://localhost:3306/exampleDb
username=root
password=

接下下让我们创建一个使用@Configuration注解的类,并将其设置为:搜索student.properites文件并自动装配至Environment变量。并使用@PropertySource来绑定配置文件:

@Configuration
@PropertySource("classpath:student.properties")
public class StudentJpaConfig {

    @Autowired
    Environment environment;

    @Bean
    DataSource dataSource() {
        DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
        dataSourceBuilder.url(this.environment.getProperty("url"));
        dataSourceBuilder.username(this.environment.getProperty("username"));
        dataSourceBuilder.password(this.environment.getProperty("password"));
        return dataSourceBuilder.build();
    }
}

DataSource bean表示使用当前方法的返回值来替找Spring中的默认DataSource。此时若StudentJpaConfig生效,则应用的数据源将切换为dataSource()方法的返回值,即H2内存数据库。

5. 验证

最后,在我们想启用内存数据库的单元测试上引入该配置类即实现指定某个单元测试连接的数据库为H2数据库的目的:

@SpringBootTest(classes★ = {
        SpringJpaTestInMemoryDatabaseApplication.class★,
        StudentJpaConfig.class★
})
class StudentJpaConfigTest {
    @Autowired
    StudentRepository studentRepository;

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

运行测试:

2020-09-02 11:02:23.047 INFO 799 --- [ task-1] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
Hibernate: drop sequence if exists hibernate_sequence
Hibernate: create sequence hibernate_sequence start with 1 increment by 1
Hibernate: create table student (id bigint not null, name varchar(255), primary key (id))
Hibernate: select student0_.id as id1_0_, student0_.name as name2_0_ from student student0_

日志输出了当前的方言为H2Dialect以表明当前单元测试使用了H2数据库;日志同时输出了创建数据表以及查询数据表的SQL语句表时数据操作正常。

6. 总结

在单元测试的很多时候,一个体积小、速度快的数据库完全能够满足测试需求(比如我们新建某个数据仓库后,往往需要测试一个findAll方法是否发生异常),所以应该为这些测试应用速度更快、体积更小的H2内存数据库。

但同时由于H2内存数据库在一些细节上仍然与生产环境下的数据库有所差距,所以涉及到一些细节时,仍然需要生产环境下的数据库。这就要求:部分单元测试用生产环境数据库,而另外部分测试则仅需要H2数据库即可。

本文对单元测试中如何启用H2内存数据库进行介绍。

分类
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 Persistence

在spring中使用EclipseLink

1. 概述

Spring Data使用了Hibernate做为默认的JPA实现。其实这只是Spring Data JPA的一种选择,但不是唯一选择。本文中,我们将介绍如何使用 EclipseLink 来替代Hibernate实现Spring Data JPA。

2. Maven依赖

在Spring应用中使用EclipseLink,需要在pom.xml中添加 org.eclipse.persistence.jpa依赖:

<dependency>
    <groupId>org.eclipse.persistence</groupId>
    <artifactId>org.eclipse.persistence.jpa</artifactId>
    <version>2.7.7</version>
</dependency>

前面我们刚刚讲过,Spring Data默认使用了Hibernate做为了JPA实现。所以如果我们想使用EclipseLink来替代Hibernate的话,则应该移除默认的Hibernate实现。该操作应该在pom.xml中进行:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

移除掉默认的Hibernate以后,接下来我们来展示如何将EclipseLink配置为Spring Data的实现。

3. 配置EclipseLink实现

Spring Boot提供了一个抽象类JpaBaseConfiguration来定义JPA实现。在配置EclipseLink实现时,仅需要继承该抽象类并重写createJpaVendorAdapter()与getVendorProperties()即可:

@Configuration
public class EclipseLinkJpaConfiguration extends JpaBaseConfiguration {

    protected EclipseLinkJpaConfiguration(DataSource dataSource, JpaProperties properties, ObjectProvider<JtaTransactionManager> jtaTransactionManager) {
        super(dataSource, properties, jtaTransactionManager);
    }

    @Override
    protected AbstractJpaVendorAdapter createJpaVendorAdapter() {
       
    }

    @Override
    protected Map<String, Object> getVendorProperties() {
       
    }
}

createJpaVendorAdapter()方法中,我们返回一个Eclipse适配器:

    @Override
    protected AbstractJpaVendorAdapter createJpaVendorAdapter() {
        return new EclipseLinkJpaVendorAdapter();
    }

getVendorProperties()返回适用于EclipseLink的配置信息:

    @Override
    protected Map<String, Object> getVendorProperties() {
        HashMap<String, Object> map = new HashMap<>();
        map.put(PersistenceUnitProperties.WEAVING,
                InstrumentationLoadTimeWeaver.isInstrumentationAvailable() ? "true" : "static");
        map.put(PersistenceUnitProperties.DDL_GENERATION, "drop-and-create-tables");
        return map;
    }

上述代码中分别对WEAVING(与惰性加载有关)以及DDL(与数据库初始化有关)进行了配置。配置过程中使用了org.eclipse.persistence.config.PersistenceUnitProperties类包中的静态变量,该类中包含了EclipseLink所有的可配置项。

然后,然后就没有然后了。此时我们仍然像以前一样来使用Spring Data JPA,不一样的是当前实现已经由Hibernate切换成了EclipseLink。

4. 测试

接下来,我们新建Student实体StudentRepository数据仓库、在H2数据库的支持下,进行单元测试。

@SpringBootTest
class StudentRepositoryTest {

    @Autowired
    StudentRepository studentRepository;

    @Test
    void save() {
        Student student = new Student();
        student.setName(RandomString.make(4));
        this.studentRepository.save(student);
    }
}

5. 结论

本文阐述了如何将Spring Data的默认实现由Hibernate切换为EclipseLink。相对于课堂上说过的很多遍的“面向接口编程、依赖于抽象而不依赖于具体”,相信本文能给我们带来更深的感受。

看一百次不如亲自做一次,听别人讲一百次也不如自己看一次。如果你想了解更多的细节还可以参考完整的示例代码。

分类
persistence

Querydsl 简介

1. 概述

Querydsl是一个功能强大的数据持久化API,本文将围绕其进行基本的介绍。本文力争达到如下效果:能够将Querydsl添加到项目中;了解如何自动生成查询类以及为何要生成它们;了解如何使用Querydsl编写类型安全的数据库查询语句。

2. 为什么要用Querydsl

对象关系映射框架是企业JAVA的核心。它的出现使得应用面向对象的思想来操作数据库成为了可能。得益于对象关系映射,开发者们能够使加简洁、更加面向对象的代码来操作数据了。

类型安全:能够在编译阶段发现查询语法错误;类型不安全:只有在程序运行时才能哆发现查询语法错误。

但是如果使用代码创建类型安全的查询,还仍然面临如何选择ORM框架的困难。

Hibernate是一款被广泛应用的ORM框架,其提供了一种与SQL语言非常相近的语言HQL(JPQL)。这种方法的明显缺点是无法保证类型安全以及在编译阶段对检查静态查询是否合法。在更复杂的情况下(例如,当需要根据某些条件在运行时构造查询时),构建HQL查询通常涉及字符串的拼接,这种拼接操作通常非常的不安全且容易出错。

由于在拼接过程中可能导致最终的HQL语法错误,而对于编译器而言最终错误的HQL就是一般的字符串,编译器无法确认最终拼接后的HQL字符串,当然也没有检查某个字符串是否为正确的HQL语法的能力。所以我们说HQL这种基于字符串的特性,是类型不安全的。

JPA 2.0对此进行了改进,引入了Criteria Query API。其在处理过程中引用了注解,并做为一种全新的构建查询方式出现。但基于JAVA无法读取某个类中的属性的属性名的特性(比如我们无法获取Student类中的name属性的属性名为'name'),使用 Criteria Query API构造的查询显得十分的臃肿。比如官方给出以下代码,其作用仅仅是为了实现SELECT p FROM Pet p

EntityManager em = ...;
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Pet> cq = cb.createQuery(Pet.class);
Root<Pet> pet = cq.from(Pet.class);
cq.select(pet);
TypedQuery<Pet> q = em.createQuery(cq);
List<Pet> allPets = q.getResultList();

基于此Querydsl应运而生,旨在提从一种更流畅、更易读、更易维护的查询方式。

3. Querydsl生成类

从根本上讲Criteria Query之所以无法达到类型安全,是由于我们无法在运行时获取一个类的元数据。比如某实体类中有以下属性:

class Student {
  private String name;

该name字段将对应数据表中的name字段,如若在Criteria Query实现根据name字段进行查询,那么代码如下:

root.get('name')

而上述代码的'name'是个普通的字符串,如果此时我们不小心输入为root.get(“nane”)也同样不会被编译器发现。而明显的由于数据表中并不存在nane字段,所以在运行时将报一个异常。

这并不是我们所期待并希望能够规避的。我们更希望当发生上述拼写错误时编译器能够快速的提示我们,所以我们更期望能够使用类似的Student.name来替换不可靠的"name"字符串。但是由于JAVA的机制,我们无法像Student.name一样来获取Student类中的name属性这个元数据。

接下来,让我们一步步学习Querydsl是如何解决上述问题的。

3.1 添加依赖

使用Querydsl需要添加querydsl-apt以及querydsl-jpa两个依赖,两个依赖在使用时要求版本相统一。为了避免由于输入问题造成的版本不统一的问题,我们在pom.xml中首先定义一个querydsl.version变量,并赋值为 4.3.1(更多版本请查看MAVEN 官方仓库):

<properties>
    <querydsl.version>4.1.3</querydsl.version>
</properties>

然后加入依赖如下:

    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-apt</artifactId>
        <version>${querydsl.version}</version>
        <scope>provided</scope>
    </dependency>
 
    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-jpa</artifactId>
        <version>${querydsl.version}</version>
    </dependency>

querydsl-apt是一个注释处理器annotation processing tool,简称APT,APT可以在编译之前对源码文件进行检测并处理特定的注解。querydsl-apt会根据扫描特定注解下的实体类,并生成与其对应生成Q类型(以Q打头)Q- type类。例如,如果您的应用程序类中有一个标有@Entity注解的User类,则querydsl-apt将在编译以前在User类的当前位置上为其对应生成QUser类。

<scope>provided</scope>的作用是声明querydsl-apt依赖仅仅在项目编译、测试的时候生效。

querydsl-jpa即为Querydsl,-jpa后缀意在表明该依赖应当与标准的JPA一起使用。

若要使querydsl-apt生效,则需要在pom.xml中添加以下代码:

	<build>
		<plugins>
			...
			<plugin>
				<groupId>com.mysema.maven</groupId>
				<artifactId>apt-maven-plugin</artifactId>
				<version>1.1.3</version>
				<executions>
					<execution>
						<goals>
							<goal>process</goal>
						</goals>
						<configuration>
							<outputDirectory>target/generated-sources/java</outputDirectory>
							<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
						</configuration>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>

该插件的作用是在执行maven build时自动生成Q类型。outputDirectory属性指定了Q类型Q-type源文件的生成位置 。我们可以此文件夹中查看某个Q类型Q-type。

大多流行的IDE均能够自动实识outputDirectory设置路径中的Q类型文件。如果你使用的编辑器幸运的无法自动识别outputDirectory下生成的Q类型源文件的话,那么首先尝试在IDE的官文文档的找到答案,或者借助一些搜索工具来解决这个问题。

本文中以演示为目的,创建了课程Course班级Klass学生Student三个实体,学生Student实体如下:

/**
 * 学生
 */
@Entity
public class Student {
    @Id
    @GeneratedValue
    private Long id;

    /**
     * 姓名
     */
    private String name;

    /**
     * 学号
     */
    private String no;

    /**
     * 体重
     */
    private Integer weight;

    /**
     * 所在班级
     */
    @ManyToOne
    private Klass klass;

    /**
     * 拥有的课程
     */
    @ManyToMany
    private List<Course> courses = new ArrayList<>();

    // 省略构造函数及setter/getter

生成实体类对应的Q类型Q-type类,仅需要执行:

mvn compile

3.2 查看生成的类

执行完mvn compile便可以在apt-maven-plugin插件的outputDirectory属性设置的路径中找到对应生的Q类型Q-type类了。找开我们设置的target/generated-sources/java路径,将查看到QCourse.java、QKlass.java、QStudent.java三个类文件。

QStudent.java类将做为所有以Student实体为根查询的入口。打开此文件将会看到类拥有@Generated注解,这代表着:该文件是自动生成而非手动编辑的;如果生成该类依赖的文件发生变化,则必须重新运行mvn compile来更新此类。

观察该文件会发现此类除了存在几个QStudent构造函数以外,还存在与Student实体对应的被声明为final类型的属性。属性中还存在一个QStudent类型:

    public static final QStudent student = new QStudent("student");

除一些join的关联查询外,我们可以使用此属性来完成大多数有关Student实体的查询。

继续观察该类我们还发现实体类的每个字段在Q类型中都有一个对应的* Path字段,例如QStudent类中的NumberPath idStringPath nameListPath courses,分别对应Student类中的 Long id, String name, List courses。稍后我们将在查询中使用到它们。

4. 小试牛刀

4.1 一般查询

我们需要一个JPAQueryFactory的实例用于构造查询。该实例可以通过EntityManager获取到。在JPA应用中,可以通过调用 EntityManagerFactory.createEntityManager() 或使用@PersistenceContext注解来获取到EntityManager

    @PersistenceContext
    EntityManager entityManager;

下面,我们开始创建第一个查询:

    /**
     * 查询姓名为X的学生
     * @param name 学生姓名
     * @return
     */
    Student findByName(String name) {
        JPAQueryFactory query = new JPAQueryFactory(entityManager); // ❶
        QStudent qStudent = QStudent.student; // ❷

        return query.selectFrom(qStudent)  // ❸
             .where( // ❹
                 qStudent.name.eq(name))  // ❺
             .fetchOne(); // ❻
    }
  • ❶ 通过EntityManager获取了一个JPAQueryFactory实例。
  • ❷ 直接引用静态变量QStudent.student.
  • ❸ selectFrom开始构造查询,并指定查询的实体为qStudent对应的Student。
  • ❹ where方法用于指定查询条件。
  • ❺ qStudent的name属性的类型为StringPath,该类型上的eq()方法用于构建“等于”条件。本行代码表示:Student实体中的 name字段与传入的name参数相同的查询条件。
  • ❻ 根据查询条件来获取某个数据。如果未找到相应的数据,则返回null,找到唯一符合要求的数据则返回该数据,如果找到了多条符合要求的数据,则抛出NonUniqueResultException

4.2 排序orderBy与分组grouping

可以使用orderBy方法来轻构的完成排序:

query.selectFrom(qStudent)
                .orderBy(qStudent.weight.asc())
                .fetch();

如下所示*Path除拥有eq()方法以外,还拥有asc()、desc()两个用于排序的方法。上述代码将按学生的体重weigth进行升序排列。

此外,还可以很轻松的实现较为复杂的查询:

    /**
     * 按体重group、按人数排序
     *
     * @return
     */
    public List<Tuple> groupByWeightAndOrderById() {
        JPAQueryFactory query = new JPAQueryFactory(entityManager);
        QStudent qStudent = QStudent.student;
        
        NumberPath<Long> count = Expressions.numberPath(Long.class, "c");

        return query.select(qStudent.weight, qStudent.id.count().as(count))
                    .from(qStudent)
                    .groupBy(qStudent.weight)
                    .orderBy(count.desc())
                    .fetch();
    }

上述代码按学生的体重进行分组的同时,还使用了自定义NumberPath来计算了每种体重的人数。

4.3 关联查询Joins与子查询Subqueries

可以使用innerJoin()以及on()方法来快带的完成关联join查询。比询出所有排有某门课程的学生:

    public List<Student> findAllByCourseName(String courseName) {
        JPAQueryFactory query = new JPAQueryFactory(entityManager);
        QStudent qStudent = QStudent.student;
        QCourse qCourse = QCourse.course;

        return query.selectFrom(qStudent)
                    .innerJoin(qStudent.courses, qCourse)
                    .on(qCourse.name.endsWith(courseName))
                    .fetch();
    }

当然,也支持子查询:

    /**
     * 子查询
     */
    public List<Student> findAllByKlassName(String klassName) {
        JPAQueryFactory query = new JPAQueryFactory(entityManager);
        QStudent qStudent = QStudent.student;
        QKlass qKlass = QKlass.klass;

        return query.selectFrom(qStudent)
                    .where(qStudent.klass.id.in(
                            JPAExpressions.select(qKlass.id)
                                          .from(qKlass)
                                          .where(qKlass.name.eq(klassName))
                    )).fetch();
    }

观察如上代码不难得到:使用Querydsl编写的子查询非常的清爽、易读。在使用子查询时,应该调用in()方法,并在该方法中传入JPAExpressions中的相关方法。

4.4 更新、删除数据

JPAQueryFactory不仅仅能够用于数据查询,还可以使用其进行数据的更新或删除:

query.update(qStudent)
     .where(qStudent.name.eq(name))
     .set(qStudent.no, no)
     .execute();

以下代码实现了按学生姓名更新学生学号的功能。其中set()实现了对某个字符赋新值的功能。上述方法中的where()方法是个选填项,如果在上述代码中去除where()方法,也不会发生语法错误,但是功能却变更为:更新数据表中的所以学生的学号。

如果我们想根据学生姓名删除某个学生,则可以这么写:

query.delete(qStudent)
     .where(qStudent.name.eq(name))
     .execute();

同更新数据中的where()方法一样,删除数据时where()方法同样可以移除。但在实际的使用过程中,应该避免这样移除where(),除非你想清空某个数据表中的所有数据。

此时你心里可能在想:Querydsl既然有了查询、更新、删除方法,那么应该还存在插入方法。很遗憾的说,受限于JPA查询规范的限制,底层的javax.persistence.Query.executeUpdate() 并不拥有插入的功能,所以若想要插入数据则仍然需要调用EntityManager。

如果说你对Querydsl情有独钟,数据的插入操作也想使用Querydsl来完成,那么你可以选择使用SQLQueryFactory。该类位于querydsl-sql库中。

5. 总结

本文中我们围绕功能强大、类型安全的Querydsl展开了讨论。Querydsl在执行查询的过程中,首先根据实体生成了Q类型的类,然后在接下来的查询中充分的调用Q类型,从而达到了类型安全type-safe的目的。

在本文的最后给出应用Querydsl进行数据更新、删除,以致于查询数据的方法。其实Querydsl功能远不止如此,希望在以后的文章中能够有机会与大家继续深入讨论。

此外,我们还一如既往的为大家准备了完整的示例代码,希望能对你有所帮助。

分类
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的自定义仓库模式使得我们可以自由地对数据仓库进行扩展。

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

分类
persistence

JPA Querydsl 入门

1. 概述

Querydsl是一款开源的JAVA框架,用于构造安全的SQL查询语句,在使用的风格上更贴近于SQL。本文将讨论其集成在JPA中的使用方法。Querydsl起初仅提供了对Hibernate中的HQL的支持,但如今其已经全面支持JPA, JDO, JDBC, Lucene, Hibernate Search, MongoDB, Collections 以及 RDFBean 了。

2. 准备

 在Spring Boot项目中编辑pom.xml文件,添加以下信息:

<properties>
	...
	<querydsl.version>4.1.3</querydsl.version>
</properties>
<dependencies>
        ...

	<dependency>
		<groupId>com.querydsl</groupId>
		<artifactId>querydsl-apt</artifactId>
		<version>${querydsl.version}</version>
		<scope>provided</scope>
	</dependency>

	<dependency>
		<groupId>com.querydsl</groupId>
		<artifactId>querydsl-jpa</artifactId>
		<version>${querydsl.version}</version>
	</dependency>
</dependencies>
<build>
	<plugins>
               ...

		<plugin>
			<groupId>com.mysema.maven</groupId>
			<artifactId>apt-maven-plugin</artifactId>
			<version>1.1.3</version>
			<executions>
				<execution>
					<goals>
						<goal>process</goal>
					</goals>
					<configuration>
						<outputDirectory>target/generated-sources/java</outputDirectory>
						<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
					</configuration>
				</execution>
			</executions>
		</plugin>
	</plugins>
</build>

如上所示,我们使用的Querdsl版本为4.1.3。添加了两个依赖以及一个MAVEN APT插件。该插件中声明的com.querydsl.apt.jpa.JPAAnnotationProcessor的作用是:查找以 javax.persistence.Entity注解的类,并自动生成其对应的用于Querdsl查询的类。

3. 小试牛刀

Querdsl根据实体来构造查询类型,然后在查询的过程中直接应用查询类型的属性,从而保证了在编译的阶段便能够发现其拼写错误,被认为是安全的。

相较于JPA Criteria Queries中在查询时指定字符串而造成不安全的特性,Querdsl在安全性上更加具有优势。

同时,Querdsl在查询时也保持了较高的一致性。下面,让我们分步查看Querds是如何工作的。

3.1 建立实体

新建学生Student实体如下:

/**
 * 学生
 */
@Entity
public class Student {
    @Id
    @GeneratedValue
    private Long id;

    /**
     * 姓名
     */
    private String name;

    /**
     * 学号
     */
    private String no;

    /**
     * 体重
     */
    private Integer weight;

  
    // setter and getter
}

此时执行mvn compile,将在target文件夹对应的包中对应生成供查询使用的QStudent类。

我们可以通过获取QStudent中的静态变量student来获取一个QStudent的实例:

QStudent qStudent = QStudent.student;

如果你使用的是IDEA编辑器,可能会接收到找不到类的错误,此时可以尝试找到pom.xml,点击右键 -> maven -> Generate Sources and Update Folders。

3.2 使用JPAQueryFactory构建查询

我们可以如下代码来获取一个JPAQueryFactory实例:

JPAQueryFactory query = new JPAQueryFactory(entityManager);

注意:上述代码中的entityManager是由JPA中的EntityManager

假设需要查找姓名为X的学生,则代码为:

/**
 * 查询姓名为X的学生
 * @param name 学生姓名
 * @return
 */
Student findByName(String name) {
    JPAQueryFactory query = new JPAQueryFactory(entityManager);

    QStudent qStudent = QStudent.student;

    return query.selectFrom(qStudent)
         .where(qStudent.name.eq(name))
         .fetchOne();
}

上述代码中:selectFrom指定了查询的数据表为student,where()方法规定的查询的条件,fetchOne()返回相关的某条学生记录。

多条件组件查询示例:

query.selectFrom(qStudent)
                .where(qStudent.name.eq(name), qStudent.no.eq(no))
                .fetchOne();

where()方法可能接收多个表达式(查询条件),在查询过程中对多个表达式(查询条件)做组合查询。此外,也可以调用表达式的and方法来完成组合查询:

query.selectFrom(qStudent)
                    .where(qStudent.name.eq(name).and(qStudent.no.eq(no)))
                    .fetchOne();

如果将上述代码转换为JPQL语言,则相当于:

select student form Student as student where student.name = ?1 and student.no = ?2

此外,还支持or查询:

/**
 * 按姓名或学号查询学生
 * 满足任一条件即可
 *
 * @param name 姓名
 * @param no   学号
 * @return
 */
List<Student> findByNameOrNo(String name, String no) {
    JPAQueryFactory query = new JPAQueryFactory(entityManager);
    QStudent qStudent = QStudent.student;

    return query.selectFrom(qStudent)
                .where(qStudent.name.eq(name).or(qStudent.no.eq(no)))
                .fetch();
}

4. 排序及聚合查询

4.1 排序

使用orderBy方法来实现按指定字段排序:

/**
 * 按体重排序
 *
 * @return
 */
List<Student> findAllOrderByWeight() {
    JPAQueryFactory query = new JPAQueryFactory(entityManager);
    QStudent qStudent = QStudent.student;

    return query.selectFrom(qStudent)
                .orderBy(qStudent.weight.asc())
                .fetch();
}

4.2 聚合

querydsl支持Sum, Avg, Max, Min等聚合函数,比如查询出所有学生中最大的体重:

int maxWeight = query.selectFrom(qStudent)
            .select(qStudent.weight.max())
            .fetchFirst();

4.3 GroupBy

当然也支持groupBy,比如接学生的体重进行分类:

query.select(qStudent.weight)
                    .from(qStudent)
                    .groupBy(qStudent.weight)
                    .fetch();

5. 单元测试

单元测试是保证代码正确的高效有力的保障手段,为此我们使用data.sql创建了4个学生,并新建如下单元测试进行验证:

package club.codedemo.querydslwithjpatutorial;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@SpringBootTest
class StudentDaoTest {

    @Autowired
    StudentDao studentDao;

    @Test
    void findByName() {
        Assertions.assertEquals(1L,
                this.studentDao.findByName("zhangsan").getId());
    }

    @Test
    void findByNameAndNo() {
        Assertions.assertEquals(1L,
                this.studentDao.findByNameAndNo("zhangsan", "200001").getId());
    }

    @Test
    void findByNameAndNo1() {
        Assertions.assertEquals(1L,
                this.studentDao.findByNameAndNo1("zhangsan", "200001").getId());
    }

    @Test
    void findByNameOrNo() {
        Assertions.assertEquals(2,
                this.studentDao.findByNameOrNo("zhangsan", "200002").size());
    }

    @Test
    void findAllOrderByWeight() {
        List<Student> students = this.studentDao.findAllOrderByWeight();

        Assertions.assertEquals(56, students.get(0).getWeight());
        Assertions.assertEquals(65, students.get(students.size() - 1).getWeight());
    }

    @Test
    void getMaxWeight() {
        Assertions.assertEquals(65, this.studentDao.getMaxWeight());
    }

    @Test
    void groupByWeight() {
        Assertions.assertEquals(3,
                this.studentDao.groupByWeight().size());
    }
}

6. 总结

本文对Spring Boot JPA中使用Querydsl进行了初步介绍。Querydsl类型安全、语法简短、贴近于SQL的特点,使其很好做为JPA查询的一种补充。更多用户请参考Querydsl简介

分类
persistence

JPA 条件查询

1. 概述

本文将围绕Spring Data JPA以及Hibernate中的Criteria Queries(条件查询)进行讨论,该特性是JPA的一个非常重要的特性,广泛的应用于各种逻辑查询中。在使用Criteria Queries的过程中,可以完全使用面向对象的思想,整个过程中不需要写任何的sql语句,这也正是Hibernate的主要特性。使用Criteria API可以使用编码的方式创建动态的查询语句,这使得我们可以根据实现的情况与需求,结合具体的业务来创建条件查询。

自Hibernate 5.2以后,Hibernate Criteria API被官方声明为弃用方法,转而推荐使用JPA Criteria API。本文将围绕如何使用Hibernate以及 JPA创建条件查询来展开讨论。

2. Maven依赖

在单独使用Hibernate的情况下,需要加入Hibernate依赖:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>   
    <version>5.3.2.Final</version>
</dependency>

最新的Hibernate版本可以在Maven官方仓库中获取。

如果你使用是Spring Boot,则应该加入Sprinb Data JPA依赖:

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

注意:我们提供的代码示例基于Spring Boot,采用了Hibernate原生方法。

3. 综合查询示例

在开始展示如何使用条件查询Criteria queries之前,让我们做一些准备工作。首先,我们创建一个学生实体:

/**
 * 学生
 */
@Entity
public class Student {
    @Id
    @GeneratedValue
    private Long id;

    /**
     * 姓名
     */
    private String name;

    /**
     * 学号
     */
    private String no;

    /**
     * 体重
     */
    private Integer weight;

    // 以下省略构造函数及setter/getter,详情下载参阅我们提供的代码示例。

}

接下来,让我们看下如果使用条件查询来获取数据中的所有学生student信息:

    /**
     * 查找所有
     *
     * @return
     */
    @Transactional
    public List<Student> findAll() {
        Session session = this.getSession(); // ❶
        CriteriaBuilder cb = session.getCriteriaBuilder(); // ❷
        CriteriaQuery<Student> cq = cb.createQuery(Student.class); // ❸

        Root<Student> root = cq.from(Student.class); // ❹
        cq.select(root); // ❹

        return session.createQuery(cq).getResultList(); // ❺
    }

上述代码中我们主要完成了如下功能:

  1. 获取当前的Session实例。
  2. 调用Session中的getCriteriaBuilder()来获取一个CriteriaBuilder
  3. 调用CriteriaBuilder中的createQuery来获取一个CriteriaQuery
  4. 指定在查询结果中返回的数据项
  5. 根据CriteriaQuery创建查询并获取查询结果。

3.1 表达式Expressions

CriteriaBuilder可以创建特定的查询条件,我们把这些查询条件称为表达式ExpressionsCriteriaQuerywhere()方法可以接收表达式Expressions,并做为最终的查询的条件。

下面,我们给出几种表达式Expressions的示例:

查找体重大于X的学生:

cq.select(root).where(cb.gt(root.get("weight"), weight))

查找体重小于X的学生:

                cq.select(root).where(cb.lt(root.get("weight"), weight))

姓名包含X的学生:

                cq.select(root).where(cb.like(root.get("name"), "%" + name + "%"))

体重介于X(不包含)与Y(不包含)之间的学生:

                cq.select(root).where(cb.between(root.get("weight"), minWeight, maxWeight))

学号为null的学生:

                cq.select(root).where(cb.isNull(root.get("no")))

学号不为null的学生:

                cq.select(root).where(cb.isNotNull(root.get("no")))

除此以外,我们还只可以使用isEmpty()以及isNotEmpty()查询出某些字为空或不为空的数据。

刚刚我们展示几种查询的方式,但有个共同的特点:仅限于1个查询条件,那么当需要有多个查询条件时,比如我们希望查询出学生姓名以X打头并且学号不为null的所有学生,那么应该怎么做呢?

条件数组查询:

Predicate[] predicates = new Predicate[2];
predicates[0] = cb.isNotNull(root.get("no"));
predicates[1] = cb.like(root.get("name"), name + "%");
cq.select(root).where(predicates);

将Predicate数组做为表达式传入将查询出符合数组中所有条修的的数据,如果我们想进行OR查询,则可以这么做:

Predicate weightGt = cb.gt(root.get("weight"), weight);
Predicate nameLike = cb.like(root.get("name"), name + "%");
cq.select(root).where(cb.or(weightGt, nameLike));

有了OR查询,当然也可以使用AND查询:

Predicate weightGt = cb.gt(root.get("weight"), weight);
Predicate nameLike = cb.like(root.get("name"), name + "%");
cq.select(root).where(cb.and(weightGt, nameLike));

3.2 排序

Criteria除支持条件查询外,还支持按特定的字段排序:

按体重逆序排列+按姓名升序排列:

cq.select(root).orderBy(
        cb.desc(root.get("weight")),
        cb.asc(root.get("name"))

3.3 方法

Criteria还对方法提供了良好的支持:

获取全部条数的count查询:

cq.select(cb.count(root))

获取全部学生的平均身高的average:

cq.select(cb.avg(root.get("weight")))

Criteria除支持如上count、avg函数以外,还sum()max()min() 等其它函数提供了良好的支持。

3.4 更新数据CriteriaUpdate

自JPA 2.1开始,Criteria API提供了更新功能,该功能主要是通过CriteriaUpdate中的set()方法实现的:

public void updateNoByName(String no, String name) {
    Session session = this.getSession();
    CriteriaBuilder cb = session.getCriteriaBuilder();
    CriteriaUpdate<Student> criteriaUpdate = cb.createCriteriaUpdate(Student.class);
    Root<Student> root = criteriaUpdate.from(Student.class);

    criteriaUpdate.set("no", no);
    criteriaUpdate.where(cb.equal(root.get("name"), name));

    session.createQuery(criteriaUpdate).executeUpdate();
}

上述代码中,我们通过CriteriaBuilder创建了一个CriteriaUpdate<Student>实例,然后调用其set()方法更新了学号no字段,并最终调用了executeUpdate()完成了数据更新工作。

3.5 删除数据CriteriaDelete

CriteriaDelete提供了数据删除的功能。通过CriteriaDelete的where()方法来指定删除的条件,并调用executeUpdate()来完成删除工作:

public void deleteById(Long id) {
    Session session = this.getSession();
    CriteriaBuilder cb = session.getCriteriaBuilder();
    CriteriaDelete<Student> criteriaDelete = cb.createCriteriaDelete(Student.class);
    Root<Student> root = criteriaDelete.from(Student.class);

    criteriaDelete.where(cb.equal(root.get("id"), id));

    session.createQuery(criteriaDelete).executeUpdate();
}

4. 与HQL比较

在前面几个小节中我们阐述了如何使用条件查询Criteria Queries。

很明显,相对于HQL我们使用Criteria Queries创建了更加优雅、漂亮、干净的代码。

由于在使用Criteria Queries的过程中可以加入任何逻辑判断,所以我们可以依托其满足更加灵活的查询条件。相较于HQL,Criteria Queries更加的面向对象、较少的涉及了数据表中的字段名称,当然的也更不容易出现拼写错误。

当然了,凡事都有两面性。Criteria Queries也并不完美,特别是写一些关联查询的时候,使用Criteria Queries将会显得更加臃肿。

所以编程这事没有最好,只有最合适。无论什么技术,最终都是为业务实现的,最合适的就是最好的。我们在深入的学习各种知识的目的并不是要将最前沿、最优秀的技术应用到所有的项目中,而是:将最合适的技术应用到项目中。

5. 总结

本文重点介绍了Hibernate及JPA中的条件查询。由条件入手讲到多条件,接下来介绍了更新及删除操作,最后与HQL进行简短的对比。同时,如果你想了解更多的关于本文所提到的技术细节,请下载我们为本文准备的可运行、测试的代码示例。