一、Spring Boot项目引入Spring Data步骤
第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步:实体类注解
我们知道 order-service 中存在两个主要领域对象,即 Order 和 Goods。为了与前面课时介绍的领域对象有所区分,本节课我们重新创建两个领域对象,分别命名为 JpaOrder 和 JpaGoods,它们就是 JPA 规范中的实体类。
我们先来看下相对简单的 JpaGoods,这里我们把 JPA 规范的相关类的引用罗列在了一起,JpaGoods 定义如下代码所示:
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@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 的定义如下代码所示:
@Repository("orderJpaRepository")
public interface OrderJpaRepository extends JpaRepository<JpaOrder, Long>
{
}
从上面代码中我们发现,OrderJpaRepository 是一个继承了 JpaRepository 接口的空接口,基于 09 讲的介绍,我们知道 OrderJpaRepository 实际上已经具备了访问数据库的基本 CRUD 功能。
第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);
}
}
然后,我们再通过构建一个 Controller 类嵌入上述方法,并通过 HTTP 请求查询 Id 为 1 的 JpaOrder 对象,获得的结果如下代码所示:
{
"id": 1,
"orderNumber": "Order10001",
"deliveryAddress": "test_address1",
"goods": [
{
"id": 1,
"goodsCode": "GoodsCode1",
"goodsName": "GoodsName1",
"price": 100.0
},
{
"id": 2,
"goodsCode": "GoodsCode2",
"goodsName": "GoodsName2",
"price": 200.0
}
]
}
请注意,这里我们不仅获取了 order 表中的订单基础数据,还同时获取了 goods 表中的商品数据,这种效果是如何实现的呢?是因为在 JpaOrder 对象中,我们添加了 @ManyToMany 注解,该注解会自动从 order_goods 表中获取商品主键信息,并从 goods 表中获取商品详细信息。
了解了使用 Spring Data JPA 实现关系型数据库访问的过程,并对比《数据访问:如何使用 JdbcTemplate 访问关系型数据库?》中通过 JdbcTemplate 获取这部分数据的实现过程,我们发现使用 Spring Data JPA 更简单。
在多样化查询实现过程中,我们不仅可以使用 JpaRepository 中默认集成的各种 CRUD 方法,还可以使用 09 讲中介绍的 @Query 注解、方法名衍生查询等。今天,我们还将同时引入 QueryByExample 和 Specification 这两种机制来丰富多样化查询方式。
第5步:通过 @Query 注解、方法名衍生查询、 QueryByExample 和 Specification机制完善Repository接口
(1)使用 @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 语句非常类似,09 讲中我们对 JPQL 进行了讨论,这里我们不再赘述,你可以前往回顾。
说到 @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 中定义与该命名一致的方法即可。
(2)使用方法名衍生查询
使用方法名衍生查询是最方便的一种自定义查询方式,在这过程中开发人员唯一需要做的就是在 JpaRepository 接口中定义一个符合查询语义的方法。
比如我们希望通过 OrderNumber 查询订单信息,那么可以提供如下代码所示的接口定义:
@Repository("orderJpaRepository")
public interface OrderJpaRepository extends JpaRepository<JpaOrder, Long>
{
JpaOrder getOrderByOrderNumber(String orderNumber);
}
通过 getOrderByOrderNumber 方法后,我们就可以自动根据 OrderNumber 获取订单详细信息了。
(3)使用 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 机制。
(4)使用 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 参数进行了比对,从而实现了查询条件的构建过程。
二、Spring Data 如何对数据访问过程进行统一抽象
事实上,JdbcTemplate 是相对偏底层的一个工具类,作为系统开发最重要的基础功能之一,数据访问层组件的开发方式在 Spring Boot 中也得到了进一步简化,并充分发挥了 Spring 家族中另一个重要成员 Spring Data 的作用。
前面我们通过两个课时介绍了 Spring 框架用于访问关系型数据库的 JdbcTemplate 模板类,今天我们将对 Spring Data 框架中所提供的数据访问方式展开讨论。
Spring Data 是 Spring 家族中专门用于数据访问的开源框架,其核心理念是对所有存储媒介支持资源配置从而实现数据访问。我们知道,数据访问需要完成领域对象与存储数据之间的映射,并对外提供访问入口,Spring Data 基于 Repository 架构模式抽象出一套实现该模式的统一数据访问方式。
Spring Data 对数据访问过程的抽象主要体现在两个方面:① 提供了一套 Repository 接口定义及实现;② 实现了各种多样化的查询支持,接下来我们分别看一下。
Repository 接口及实现
Repository 接口是 Spring Data 中对数据访问的最高层抽象,接口定义如下所示:
public interface Repository<T, ID> {
}
在以上代码中,我们看到 Repository 接口只是一个空接口,通过泛型指定了领域实体对象的类型和 ID。在 Spring Data 中,存在一大批 Repository 接口的子接口和实现类,该接口的部分类层结构如下所示:
可以看到 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 删除实体 、删除一个实体的集合以及删除所有实体等常见操作,我们具体来看下其中几个方法的实现过程。
在实现过程中,我们首先需要关注最基础的 save 方法。通过查看 CrudRepository 的类层结构,我们找到它的一个实现类 SimpleJpaRepository,这个类显然是基于 JPA 规范所实现的针对关系型数据库的数据访问类。
save 方法如下代码所示:
private final JpaEntityInformation<T, ?> entityInformation;
private final EntityManager em;
@Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
显然,上述 save 方法依赖于 JPA 规范中的 EntityManager,当它发现所传入的实体为一个新对象时,就会调用 EntityManager 的 persist 方法,反之使用该对象进行 merge。关于 JPA 规范以及 EntityManager 我们在下一课时中会详细展开。
我们接着看一下用于根据 id 查询实体的 findOne 方法,如下代码所示:
public T findOne(ID id) {
Assert.notNull(id, ID_MUST_NOT_BE_NULL);
Class<T> domainType = getDomainClass();
if (metadata == null) {
return em.find(domainType, id);
}
LockModeType type = metadata.getLockModeType();
Map<String, Object> hints = getQueryHints();
return type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints);
}
在执行查询过程中,findOne 方法会根据领域实体的类型调用 EntityManager 的 find 方法来查找目标对象。需要注意的是,这里也会用到一些元数据 Metadata,以及涉及改变正常 SQL 执行效果的 Hint 机制的使用。
多样化查询支持
在日常开发过程中,数据查询的操作次数要远高于数据新增、数据删除和数据修改,因此在 Spring Data 中,除了对领域对象提供默认的 CRUD 操作外,我们还需要对查询场景高度抽象。而在现实的业务场景中,最典型的查询操作是 @Query 注解和方法名衍生查询机制。
(1)@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 的基本语法如下所示:
SELECT 子句 FROM 子句
[WHERE 子句]
[GROUP BY 子句]
[HAVING 子句]
[ORDER BY 子句]
JPQL 语句是不是和原生的 SQL 语句非常类似?唯一的区别就是 JPQL FROM 语句后面跟的是对象,而原生 SQL 语句中对应的是数据表中的字段。
介绍完 JPQL 之后,我们再回到 @Query 注解定义,这个注解位于 org.springframework.data.jpa.repository 包中,如下所示:
package org.springframework.data.jpa.repository;
public @interface Query {
String value() default "";
String countQuery() default "";
String countProjection() default "";
boolean nativeQuery() default false;
String name() default "";
String countName() default "";
}
@Query 注解中最常用的就是 value 属性,在前面示例中 JPQL 语句有使用到 。当然,如果我们将 nativeQuery 设置为 true,那么 value 属性则需要指定具体的原生 SQL 语句。
请注意,在 Spring Data 中存在一批 @Query 注解,分别针对不同的持久化媒介。例如 MongoDB 中存在一个 @Query 注解,但该注解位于 org.springframework.data.mongodb.repository 包中,定义如下:
package org.springframework.data.mongodb.repository;
public @interface Query {
String value() default "";
String fields() default "";
boolean count() default false;
boolean exists() default false;
boolean delete() default false;
}
与面向 JPA 的 @Query 注解不同的是,MongoDB 中 @Query 注解的 value 值是一串 JSON 字符串,用于指定需要查询的对象条件,这里我们就不具体展开了。
(3)方法名衍生查询
方法名衍生查询也是 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 中定义的方法名进行一定约束。
首先我们需要指定一些查询关键字,常见的关键字如下表所示:
有了这些查询关键字后,在方法命名上我们还需要指定查询字段和一些限制性条件。例如,在前面的示例中,我们只是基于“fistName”和“lastName”这两个字段做查询。
事实上,我们可以查询的内容非常多,下表列出了更多的方法名衍生查询示例,你可以参考下。
在 Spring Data 中,方法名衍生查询的功能非常强大,上表中罗列的这些也只是全部功能中的一小部分而已。
讲到这里,你可能会问一个问题:如果我们在一个 Repository 中同时指定了 @Query 注解和方法名衍生查询,那么 Spring Data 会具体执行哪一个呢?要想回答这个问题,就需要我们对查询策略有一定的了解。
在 Spring Data 中,查询策略定义在 QueryLookupStrategy 中,如下代码所示:
public interface QueryLookupStrategy {
public static enum Key {
CREATE, USE_DECLARED_QUERY, CREATE_IF_NOT_FOUND;
public static Key create(String xml) {
if (!StringUtils.hasText(xml)) {
return null;
}
return valueOf(xml.toUpperCase(Locale.US).replace("-", "_"));
}
}
RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries);
}
从以上代码中,我们看到 QueryLookupStrategy 分为三种,即 CREATE、USE_DECLARED_QUERY 和 CREATE_IF_NOT_FOUND。
这里的 CREATE 策略指的是直接根据方法名创建的查询策略,也就是使用前面介绍的方法名衍生查询。
而 USE_DECLARED_QUERY 指的是声明方式,主要使用 @Query 注解,如果没有 @Query 注解系统就会抛出异常。
而最后一种 CREATE_IF_NOT_FOUND 可以理解为是 @Query 注解和方法名衍生查询两者的兼容版。请注意,Spring Data 默认使用的是 CREATE_IF_NOT_FOUND 策略,也就是说系统会先查找 @Query 注解,如果查到没有,会再去找与方法名相匹配的查询。
(4)Spring Data 中的组件
Spring Data 支持对多种数据存储媒介进行数据访问,表现为提供了一系列默认的 Repository,包括针对关系型数据库的 JPA/JDBC Repository,针对 MongoDB、Neo4j、Redis 等 NoSQL 对应的 Repository,支持 Hadoop 的大数据访问的 Repository,甚至包括 Spring Batch 和 Spring Integration 在内的系统集成的 Repository。
在 Spring Data 的官方网站https://spring.io/projects/spring-data 中,列出了其提供的所有组件,如下图所示:
Spring Data 所提供的组件列表(来自 Spring Data 官网)
根据官网介绍,Spring Data 中的组件可以分成四大类:核心模块(Main modules)、社区模块(Community modules)、关联模块(Related modules)和正在孵化的模块(Modules in Incubation)。例如,前面介绍的 Respository 和多样化查询功能就在核心模块 Spring Data Commons 组件中。
这里,我特别想强调下的是正在孵化的模块,它目前只包含一个组件,即 Spring Data R2DBC。 R2DBC 是Reactive Relational Database Connectivity 的简写,代表响应式关系型数据库连接,相当于是响应式数据访问领域的 JDBC 规范。