3.2 使用Spring Data JPA持久化数据
3.2 使用Spring Data JPA持久化数据
Spring Data是一个非常大的伞形项目,由多个子项目组成,其中大多数子项目都关注对不同的数据库类型进行数据持久化。比较流行的几个Spring Data项目包括:
- Spring Data JPA:基于关系型数据库进行JPA持久化。
- Spring Data MongoDB:持久化到Mongo文档数据库。
- Spring Data Neo4j:持久化到Neo4j图数据库。
- Spring Data Redis:持久化到Redis key-value存储。
- Spring Data Cassandra:持久化到Cassandra数据库。
Spring Data为所有项目提供了一项最有趣且最有用的特性,就是基于repository规范接口自动生成repository的功能。
要了解Spring Data是如何运行的,我们需要重新开始,将本章前文基于JDBC的repository替换为使用Spring Data JPA的repository。首先,我们需要将SpringData JPA添加到项目的构建文件中。
3.2.1 添加Spring Data JPA到项目中
Spring Boot应用可以通过JPA starter来添加Spring Data JPA。这个starter依赖不仅会引入Spring Data JPA,还会传递性地将Hibernate作为JPA实现引入进来:
1 | <dependency> |
如果你想要使用不同的JPA实现,那么至少需要将Hibernate依赖排除出去并将你所选择的JPA库包含进来。举例来说,如果想要使用EclipseLink来替代Hibernate,就需要像这样修改构建文件:
1 | <dependency> |
需要注意,根据你所选择的JPA实现,这里可能还需要其他的变更。你可以参考所选择的JPA实现文档以了解更多细节。现在,我们重新看一下领域对象,并为它们添加注解,使其支持JPA持久化。
3.2.2 将领域对象标注为实体
你马上将会看到,在创建repository方面,Spring Data为我们做了很多非常棒的事情。但是,在使用JPA映射注解标注领域对象方面,它却没有提供太多的助益。我们需要打开Ingredient、Taco和Order类,并为其添加一些注解,首先是Ingredient类,如程序清单3.16所示。
程序清单3.16 为Ingredient添加注解使其支持JPA持久化
1 | package tacos; |
为了将Ingredient声明为JPA实体,它必须添加@Entity注解。它的id属性需要使用@Id注解,以便于将其指定为数据库中唯一标识该实体的属性。
除了JPA特定的注解,你可能会发现我们在类级别添加了@NoArgsConstructor注解。JPA需要实体有一个无参的构造器,Lombok的@NoArgsConstructor注解能够帮助我们实现这一点。但是,我们不想直接使用它,因此通过将access属性设置为AccessLevel.PRIVATE使其变成私有的。因为这里有必须要设置的final属性,所以我们将force设置为true,这样Lombok生成的构造器就会将它们设置为null。
我们还添加了一个@RequiredArgsConstructor注解。@Data注解会为我们添加一个有参构造器,但是使用@NoArgsConstructor注解之后,这个构造器就会被移除掉。现在,我们显式添加@RequiredArgsConstructor注解,以确保除了private的无参构造器之外,我们还会有一个有参构造器。
接下来,我们看一下程序清单3.17所示的Taco类,看看它是如何标注为JPA实体的。
程序清单3.17 将Taco标注为实体
1 | package tacos; |
与Ingredient类似,Taco类现在添加了@Entity注解,并为其id属性添加了@Id注解。因为我们要依赖数据库自动生成ID值,所以在这里还为id属性设置了@GeneratedValue,将它的strategy设置为AUTO。
为了声明Taco与其关联的Ingredient列表之间的关系,我们为ingredients添加了@ManyToMany注解。每个Taco可以有多个Ingredient,而每个Ingredient可以是多个Taco的组成部分。
你会看到,在这里有一个新的方法createdAt(),并使用了@PrePersist注解。在Taco持久化之前,我们会使用这个方法将createdAt设置为当前的日期和时间。最后,我们要将Order对象标注为实体。程序清单3.18展示了新的Order类。
程序清单3.18 将Order标注为JPA实体
1 | package tacos; |
我们可以看到,Order所需的变更就是Taco的翻版。但是,在类级别这里有了一个新的注解,即@Table。它表明Order实体应该持久化到数据库中名为Taco_Order的表中。
我们可以将这个注解用到所有的实体上,但是只有Order有必要这样做。如果没有它,JPA默认会将实体持久化到名为Order的表中,但是order是SQL的保留字,这样做的话会产生问题。实体都已经标注好了,现在我们该编写repository了。
3.2.3 声明JPA repository
在JDBC版本的repository中,我们显式声明想要repository提供的方法。但是,借助Spring Data,我们可以扩展CrudRepository接口。举例来说,如下是新的IngredientRepository接口。
1 | package tacos.data; |
CrudRepository定义了很多用于CRUD(创建、读取、更新、删除)操作的方法。注意,它是参数化的,第一个参数是repository要持久化的实体类型,第二个参数是实体ID属性的类型。对于IngredientRepository来说,参数应该是Ingredient和String。
我们可以非常简单地定义TacoRepository:
1 | package tacos.data; |
IngredientRepository和TacoRepository之间唯一比较明显的区别就是CrudRepository的参数。在这里,我们将其设置为Taco和Long,从而指定Taco实体(及其ID类型)是该repository接口的持久化单元。最后,相同的变更可以用到OrderRepository上:
1 | package tacos.data; |
现在,我们有了3个repository。你可能会想,我们应该需要编写它们的实现类,包括每个实现类所需的十多个方法。但是,Spring Data JPA带来的好消息是,我们根本就不用编写实现类!当应用启动的时候,Spring Data JPA会在运行期自动生成实现类。这意味着,我们现在就可以使用这些repository了。我们只需要像使用基于JDBC的实现那样将它们注入控制器中就可以了。
CrudRepository所提供的方法对于实体的通用持久化是非常有用的。但是,如果我们的需求并不局限于基本持久化,那又该怎么办呢?接下来,我们看一下如何自定义repository来执行特定领域的查询。
3.2.4 自定义JPA repository
假设除了CrudRepository提供的基本CRUD操作之外,我们还需要获取投递到指定邮编(Zip)的订单。实际上,我们只需要添加如下的方法声明到OrderRepository中,这个问题就解决了:
1 | List<Order> findByDeliveryZip(String deliveryZip); |
当创建repository实现的时候,Spring Data会检查repository接口的所有方法,解析方法的名称,并基于被持久化的对象来试图推测方法的目的。本质上,SpringData定义了一组小型的领域特定语言(Domain-Specific Language,DSL),在这里持久化的细节都是通过repository方法的签名来描述的。
Spring Data能够知道这个方法是要查找Order的,因为我们使用Order对CrudRepository进行了参数化。方法名findByDeliveryZip()确定该方法需要根据deliveryZip属性相匹配来查找Order,而deliveryZip的值是作为参数传递到方法中来的。
findByDeliveryZip ()方法非常简单,但是Spring Data也能处理更加有意思的方法名称。repository方法是由一个动词、一个可选的主题(Subject)、关键词By以及一个断言所组成的。在findByDeliveryZip()这个样例中,动词是find,断言是DeliveryZip,主题并没有指定,暗含的主题是Order。
我们考虑另外一个更复杂的样例。假设我们想要查找投递到指定邮编且在一定时间范围内的订单。在这种情况下,我们可以将如下的方法添加到OrderRepository中,它就能达到我们的目的。
1 | List<Order> readOrdersByDeliveryZipAndPlacedAtBetween( |
图3.2展现了Spring Data在生成repository实现的时候是如何解析和理解readOrdersByDeliveryZipAndPlacedAtBetween()方法的。我们可以看到,在readOrdersByDeliveryZipAndPlacedAtBetween()中,动词是read。SpringData会将get、read和find视为同义词,它们都是用来获取一个或多个实体的。另外,我们还可以使用count作为动词,它会返回一个int值,代表匹配实体的数量。
尽管方法的主题是可选的,但是这里要查找的就是Order。Spring Data会忽略主题中大部分的单词,所以你尽可以将方法命名为readPuppiesBy…,它依然会去查找Order实体,因为CrudRepository的类型是参数化的。
单词By后面的断言是方法签名中最为有意思的一部分。在本例中,断言指定了Order的两个属性:deliveryZip和placedAt。deliveryZip属性的值必须要等于方法第一个参数传入的值。关键字Between表明placedAt属性的值必须要位于方法最后两个参数的值之间。
除了Equals和Between操作之外,Spring Data方法签名还能包括如下的操作符:
- IsAfter、After、IsGreaterThan、GreaterThan
- IsGreaterThanEqual、GreaterThanEqual
- IsBefore、Before、IsLessThan、LessThan
- IsLessThanEqual、LessThanEqual
- IsBetween、Between
- IsNull、Null
- IsNotNull、NotNull
- IsIn、In
- IsNotIn、NotIn
- IsStartingWith、StartingWith、StartsWith
- IsEndingWith、EndingWith、EndsWith
- IsContaining、Containing、Contains
- IsLike、Like
- IsNotLike、NotLike
- IsTrue、True
- IsFalse、False
- Is、Equals
- IsNot、Not
- IgnoringCase、IgnoresCase
作为IgnoringCase/IgnoresCase的替代方案,我们还可以在方法上添加AllIgnoringCase或AllIgnoresCase,这样它就会忽略所有String对比的大小写。例如,请看如下方法:
1 | List<Order> findByDeliveryToAndDeliveryCityAllIgnoresCase( |
最后,我们还可以在方法名称的结尾处添加OrderBy,实现结果集根据某个列排序。例如,我们可以按照deliveryTo属性排序:
1 | List<Order> findByDeliveryCityOrderByDeliveryTo(String city); |
尽管方法名称约定对于相对简单的查询非常有用,但是,不难想象,对于更为复杂的查询,方法名可能会面临失控的风险。在这种情况下,可以将方法定义为任何你想要的名称,并为其添加@Query注解,从而明确指明方法调用时要执行的查询,如下面的样例所示:
1 |
|
在本例中,通过使用@Query,我们声明只查询所有投递到Seattle的订单。但是,我们可以使用@Query执行任何想要的查询,有些查询是通过方法命名约定很难甚至根本无法实现的。