Spring Boot 使用Spring Data

一、Repository 接口及查询方法

Spring Data 是 Spring 家族中专门用于数据访问的开源框架,其核心理念是对所有存储媒介支持资源配置从而实现数据访问。我们知道,数据访问需要完成领域对象与存储数据之间的映射,并对外提供访问入口,Spring Data 基于 Repository 架构模式抽象出一套实现该模式的统一数据访问方式。

Spring Data 对数据访问过程的抽象主要体现在两个方面:① 提供了一套 Repository 接口定义及实现;② 实现了各种多样化的查询支持,接下来我们分别看一下。


Repository 接口及实现

Repository 接口是 Spring Data 中对数据访问的最高层抽象,接口定义如下所示:

public interface Repository<T, ID> {

}

 

在以上代码中,我们看到 Repository 接口只是一个空接口,通过泛型指定了领域实体对象的类型和 ID。在 Spring Data 中,存在一大批 Repository 接口的子接口和实现类,该接口的部分类层结构如下所示:

Spring Boot 使用Spring Data

可以看到 CrudRepository 接口是对 Repository 接口的最常见扩展,添加了对领域实体的 CRUD 操作功能,具体定义如下代码所示:

public interface CrudRepository<T, ID> extends Repository<T, ID> {

  <S extends T> S save(S entity);

  <S extends T> Iterable<S> saveAll(Iterable<S> entities);

  Optional<T> findById(ID id);

  boolean existsById(ID id);

  Iterable<T> findAll();

  Iterable<T> findAllById(Iterable<ID> ids);

  long count();

  void deleteById(ID id);

  void delete(T entity);

  void deleteAll(Iterable<? extends T> entities);

  void deleteAll();

}

 

这些方法都是自解释的,我们可以看到 CrudRepository 接口提供了保存单个实体、保存集合、根据 id 查找实体、根据 id 判断实体是否存在、查询所有实体、查询实体数量、根据 id 删除实体 、删除一个实体的集合以及删除所有实体等常见操作,我们具体来看下其中几个方法的实现过程。


多样化查询支持

在日常开发过程中,数据查询的操作次数要远高于数据新增、数据删除和数据修改,因此在 Spring Data 中,除了对领域对象提供默认的 CRUD 操作外,我们还需要对查询场景高度抽象。而在现实的业务场景中,最典型的查询操作是 @Query 注解和方法名衍生查询机制。

@Query 注解

我们可以通过 @Query 注解直接在代码中嵌入查询语句和条件,从而提供类似 ORM 框架所具有的强大功能。

下面就是使用 @Query 注解进行查询的典型例子:

public interface AccountRepository extends JpaRepository<Account,Long> {

      @Query("select a from Account a where a.userName = ?1")

      Account findByUserName(String userName);

}

 

注意到这里的 @Query 注解使用的是类似 SQL 语句的语法,它能自动完成领域对象 Account 与数据库数据之间的相互映射。因我们使用的是 JpaRepository,所以这种类似 SQL 语句的语法实际上是一种 JPA 查询语言,也就是所谓的 JPQL(Java Persistence Query Language)。

JPQL 的基本语法如下所示:

  1. SELECT 子句 FROM 子句

  2. [WHERE 子句]

  3. [GROUP BY 子句]

  4. [HAVING 子句]

  5. [ORDER BY 子句]

JPQL 语句是不是和原生的 SQL 语句非常类似?唯一的区别就是 JPQL FROM 语句后面跟的是对象,而原生 SQL 语句中对应的是数据表中的字段。

方法名衍生查询

方法名衍生查询也是 Spring Data 的查询特色之一,通过在方法命名上直接使用查询字段和参数,Spring Data 就能自动识别相应的查询条件并组装对应的查询语句。典型的示例如下所示:

public interface AccountRepository extends JpaRepository<Account,Long> {

     List<Account> findByFirstNameAndLastName(String firstName, String,lastName);

}

 

在上面的例子中,通过 findByFirstNameAndLastname 这样符合普通语义的方法名,并在参数列表中按照方法名中参数的顺序和名称(即第一个参数是 fistName,第二个参数 lastName)传入相应的参数,Spring Data 就能自动组装 SQL 语句从而实现衍生查询。是不是很神奇?

而想要使用方法名实现衍生查询,我们需要对 Repository 中定义的方法名进行一定约束。

首先我们需要指定一些查询关键字,常见的关键字如下表所示:

Spring Boot 使用Spring Data

Spring Boot 使用Spring Data

请注意,Spring Data 默认使用的是 CREATE_IF_NOT_FOUND 策略,也就是说系统会先查找 @Query 注解,如果查到没有,会再去找与方法名相匹配的查询。


二、 ORM 集成:使用 Spring Data JPA 访问关系型数据库步骤

(1)引入 Spring Data JPA

如果你想在应用程序中使用 Spring Data JPA,首先需要在 pom 文件中引入 spring-boot-starter-data-jpa 依赖,如下代码所示:

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

在介绍这一组件的使用方法之前,我们有必要对 JPA 规范进行一定的了解。

JPA 全称是 JPA Persistence API,即 Java 持久化 API,它是一个 Java 应用程序接口规范,用于充当面向对象的领域模型和关系数据库系统之间的桥梁,属于一种 ORM(Object Relational Mapping,对象关系映射)技术。

JPA 规范中定义了一些既定的概念和约定,集中包含在 javax.persistence 包中,常见的如对实体(Entity)定义、实体标识定义、实体与实体之间的关联关系定义,以及 09 讲中介绍的 JPQL 定义等,关于这些定义及其使用方法,一会儿我们会详细展开说明。

与 JDBC 规范一样,JPA 规范也有一大批实现工具和框架,极具代表性的如老牌的 Hibernate 及今天我们将介绍的 Spring Data JPA。

为了演示基于 Spring Data JPA 的整个开发过程,我们将在 SpringCSS 案例中专门设计和实现一套独立的领域对象和 Repository,接下来我们一起来看下。

(2)实体类注解

实体类是指和数据表对应的映射表。

我们先来看下相对简单的 JpaGoods,这里我们把 JPA 规范的相关类的引用罗列在了一起,JpaGoods 定义如下代码所示:

@Entity

@Table(name="goods")

public class JpaGoods {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)

    private Long id;   

    private String goodsCode;

    private String goodsName;

    private Float price;   

    //省略 getter/setter

}

 

JpaGoods 中使用了 JPA 规范中用于定义实体的几个注解:最重要的 @Entity 注解、用于指定表名的 @Table 注解、用于标识主键的 @Id 注解,以及用于标识自增数据的 @GeneratedValue 注解,这些注解都比较直白,在实体类上直接使用即可。

接下来,我们看下比较复杂的 JpaOrder,定义如下代码所示:

@Entity

@Table(name="`order`")

public class JpaOrder implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)

    private Long id;

    private String orderNumber;

    private String deliveryAddress;

 

    @ManyToMany(targetEntity=JpaGoods.class)
    @JoinTable(name = "order_goods", joinColumns = @JoinColumn(name = "order_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "goods_id", referencedColumnName = "id"))

    private List<JpaGoods> goods = new ArrayList<>();

 

    //省略 getter/setter

}

这里除了引入了常见的一些注解,还引入了 @ManyToMany 注解,它表示 order 表与 goods 表中数据的关联关系。

在JPA 规范中,共提供了 one-to-one、one-to-many、many-to-one、many-to-many 这 4 种映射关系,它们分别用来处理一对一、一对多、多对一,以及多对多的关联场景。

针对 order-service 这个业务场景,我们设计了一张 order_goods 中间表存储 order 与 goods 表中的主键关系,且使用了 @ManyToMany 注解定义 many-to-many 这种关联关系,也使用了 @JoinTable 注解指定 order_goods 中间表,并通过 joinColumns 和 inverseJoinColumns 注解分别指定中间表中的字段名称以及引用两张主表中的外键名称。

(3)自定义类实现  Repository接口的子接口

@Repository("orderJpaRepository")
public interface OrderJpaRepository extends JpaRepository<JpaOrder, Long>{}

实现接口以后,自定义类便会自带很多从Repository接口继承来的crud方法,也可以自己新写方法。

使用 @Query 注解

使用 @Query 注解实现查询的示例如下代码所示:

@Repository("orderJpaRepository")

public interface OrderJpaRepository extends JpaRepository<JpaOrder, Long>

{

    @Query("select o from JpaOrder o where o.orderNumber = ?1")
    JpaOrder getOrderByOrderNumberWithQuery(String orderNumber);

}

这里,我们使用了 JPQL 根据 OrderNumber 查询订单信息。JPQL 的语法与 SQL 语句非常类似。

说到 @Query 注解,JPA 中还提供了一个 @NamedQuery 注解对 @Query 注解中的语句进行命名。@NamedQuery 注解的使用方式如下代码所示:

@Entity

@Table(name = "`order`")

@NamedQueries({ @NamedQuery(name = "getOrderByOrderNumberWithQuery", query = "select o from JpaOrder o where o.orderNumber = ?1") })

public class JpaOrder implements Serializable {}

在上述示例中,我们在实体类 JpaOrder 上添加了一个 @NamedQueries 注解,该注解可以将一批 @NamedQuery 注解整合在一起使用。同时,我们还使用了 @NamedQuery 注解定义了一个“getOrderByOrderNumberWithQuery”查询,且指定了对应的 JPQL 语句。

如果你想使用这个命名查询,在 OrderJpaRepository 中定义与该命名一致的方法即可。

使用方法名衍生查询

使用方法名衍生查询是最方便的一种自定义查询方式,在这过程中开发人员唯一需要做的就是在 JpaRepository 接口中定义一个符合查询语义的方法。

比如我们希望通过 OrderNumber 查询订单信息,那么可以提供如下代码所示的接口定义:

@Repository("orderJpaRepository")

public interface OrderJpaRepository extends JpaRepository<JpaOrder, Long>

{

    JpaOrder getOrderByOrderNumber(String orderNumber);

}

 

 

通过 getOrderByOrderNumber 方法后,我们就可以自动根据 OrderNumber 获取订单详细信息了。

 

使用 QueryByExample 机制

接下来我们将介绍另一种强大的查询机制,即 QueryByExample(QBE)机制。

针对 JpaOrder 对象,假如我们希望根据 OrderNumber 及 DeliveryAddress 中的一个或多个条件进行查询,按照方法名衍生查询的方式构建查询方法后,得到如下代码所示的方法定义:

List<JpaOrder> findByOrderNumberAndDeliveryAddress (String orderNumber, String deliveryAddress);

 

如果查询条件中使用的字段非常多,上面这个方法名可能非常长,且还需要设置一批参数,这种查询方法定义显然存在缺陷。

因为不管查询条件有多少个,我们都需要把所有参数进行填充,哪怕部分参数并没有被用到。而且,如果将来我们需要再添加一个新的查询条件,该方法必须做调整,从扩展性上讲也存在设计缺陷。为了解决这些问题,我们便可以引入 QueryByExample 机制。

QueryByExample 可以翻译为按示例查询,是一种用户友好的查询技术。它允许我们动态创建查询,且不需要编写包含字段名称的查询方法,也就是说按示例查询不需要使用特定的数据库查询语言来编写查询语句。

从组成结构上讲,QueryByExample 包括 Probe、ExampleMatcher 和 Example 这三个基本组件。其中, Probe 包含对应字段的实例对象,ExampleMatcher 携带有关如何匹配特定字段的详细信息,相当于匹配条件,Example 则由 Probe 和 ExampleMatcher 组成,用于构建具体的查询操作。

现在,我们基于 QueryByExample 机制重构根据 OrderNumber 查询订单的实现过程。

首先,我们需要在 OrderJpaRepository 接口的定义中继承 QueryByExampleExecutor 接口,如下代码所示:

@Repository("orderJpaRepository")

public interface OrderJpaRepository extends JpaRepository<JpaOrder, Long>, QueryByExampleExecutor<JpaOrder> {}

 

然后,我们在 JpaOrderService 中实现如下代码所示的 getOrderByOrderNumberByExample 方法:

public JpaOrder getOrderByOrderNumberByExample(String orderNumber) {

        JpaOrder order = new JpaOrder();

        order.setOrderNumber(orderNumber);

 

        ExampleMatcher matcher = ExampleMatcher.matching().withIgnoreCase()

                .withMatcher("orderNumber", GenericPropertyMatchers.exact()).withIncludeNullValues();

 

        Example<JpaOrder> example = Example.of(order, matcher);

 

        return orderJpaRepository.findOne(example).orElse(new JpaOrder());

}

 

上述代码中,我们首先构建了一个 ExampleMatcher 对象用于初始化匹配规则,然后通过传入一个 JpaOrder 对象实例和 ExampleMatcher 实例构建了一个 Example 对象,最后通过 QueryByExampleExecutor 接口中的 findOne() 方法实现了 QueryByExample 机制。

使用 Specification 机制

本节课中,最后我们想介绍的查询机制是 Specification 机制。

先考虑这样一种场景,比如我们需要查询某个实体,但是给定的查询条件不固定,此时该怎么办?这时我们通过动态构建相应的查询语句即可,而在 Spring Data JPA 中可以通过 JpaSpecificationExecutor 接口实现这类查询。相比使用 JPQL 而言,使用 Specification 机制的优势是类型安全。

继承了 JpaSpecificationExecutor 的 OrderJpaRepository 定义如下代码所示:

@Repository("orderJpaRepository")

public interface OrderJpaRepository extends JpaRepository<JpaOrder, Long>,     JpaSpecificationExecutor<JpaOrder>{}

 

对于 JpaSpecificationExecutor 接口而言,它背后使用的就是 Specification 接口,且 Specification 接口核心方法就一个,我们可以简单地理解该接口的作用就是构建查询条件,如下代码所示:

Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);

其中 Root 对象代表所查询的根对象,我们可以通过 Root 获取实体的属性,CriteriaQuery 代表一个顶层查询对象,用来实现自定义查询,而 CriteriaBuilder 用来构建查询条件。

基于 Specification 机制,我们同样对根据 OrderNumber 查询订单的实现过程进行重构,重构后的 getOrderByOrderNumberBySpecification 方法如下代码所示:

public JpaOrder getOrderByOrderNumberBySpecification(String orderNumber) {

        JpaOrder order = new JpaOrder();
        order.setOrderNumber(orderNumber);
        @SuppressWarnings("serial")
        Specification<JpaOrder> spec = new Specification<JpaOrder>() {
            @Override
            public Predicate toPredicate(Root<JpaOrder> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                Path<Object> orderNumberPath = root.get("orderNumber"); 
                Predicate predicate = cb.equal(orderNumberPath, orderNumber);
                return predicate;
            }
        };
        return orderJpaRepository.findOne(spec).orElse(new JpaOrder());    
}

从上面示例中可以看到,在 toPredicate 方法中,首先我们从 root 对象中获取了“orderNumber”属性,然后通过 cb.equal 方法将该属性与传入的 orderNumber 参数进行了比对,从而实现了查询条件的构建过程。

(4)使用 Spring Data JPA 访问数据库

有了上面定义的 JpaOrder 和 JpaGoods 实体类,以及 OrderJpaRepository 接口,我们已经可以实现很多操作了。

比如我们想通过 Id 获取 Order 对象,首先可以通过构建一个 JpaOrderService 直接注入 OrderJpaRepository 接口,如下代码所示:

@Service

public class JpaOrderService {

 

    @Autowired

    private OrderJpaRepository orderJpaRepository;

 

    public JpaOrder getOrderById(Long orderId) {

 

        return orderJpaRepository.getOne(orderId);

    }

}

 

 

上一篇:Kafka的生产者和消费者代码解析


下一篇:python-Moviepy缩放效果需要调整