3.2使用Spring Data JPA持久化数据

3.2使用Spring Data JPA持久化数据

Spring Data项目是一个相当大的伞形项目,几个子项目组成,其中大多数子项目关注于具有各种不同数据库类型的数据持久化。一些最流行的Spring数据项目包括:

  • Spring Data JPA-针对关系数据库的持久化
  • Spring Data Mongo-针对Mongo文档数据库的持久化
  • Spring Data Neo4j-针对Neo4j图形数据库的持久化
  • Spring Data Redis-针对Redis键值存储的持久化
  • Spring Data Cassandra-针对Cassandra数据库的持久化

Spring Data为所有这些项目提供的最有意思和最有用的特性之一是能够基于存储库规范接口自动创建存储库。

为了了解Spring Data是如何工作的,需要将本章前面介绍的基于jdbc的存储库替换为Spring Data JPA创建的存储库。但是首先,需要将Spring Data JPA添加到项目构建中。

3.2.1添加Spring Data JPA到数据库中

Spring Data JPA可用于具有JPA starterSpring Boot应用程序。这个starter依赖不仅带来了Spring Data JPA,还包括Hibernate作为JPA的实现:

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

如果想使用不同的JPA实现,那么至少需要排除Hibernate依赖,并包含所选择的JPA库。例如,要使用EclipseLink而不是Hibernate,需要按如下方式更改构建:

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

<dependency>
    <groupId>org.eclipse.persistence</groupId>
    <artifactId>eclipselink</artifactId>
    <version>2.5.2</version>
</dependency>

请注意,根据对JPA实现的选择,可能需要进行其他更改。详细信息请参阅选择的JPA实现的文档。现在,让我们重新查看域对象并对它们进行注解以实现JPA持久化。

3.2.2注解域作为实体

很快就会看到,在创建存储库方面,Spring Data做了一些惊人的事情。但不幸的是,在使用JPA映射注解注解域对象时,它并没有太大的帮助。需要打开IngredientTacoOrder类,并添加一些注解。首先是Ingredient类。程序清单3.16JPA持久化注解Ingredient

package tacos;

import javax.persistence.Entity;
import javax.persistence.Id;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;

@Data
@RequiredArgsConstructor
@NoArgsConstructor(access=AccessLevel.PRIVATE, force=true)
@Entity
public class Ingredient {
    @Id
    private final String id;
    private final String name;
    private final Type type;

    public static enum Type {
        WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
    }
}

为了将其声明为JPA实体,必须使用@Entity注解。它的id属性必须使用@Id进行注解,以便将其指定为惟一标识数据库中实体的属性。

除了特定于JPA的注解之外,还在类级别上添加了@NoArgsConstructor注解。JPA要求实体有一个无参构造函数,所以Lombok@NoArgsConstructor实现了这一点。但是要是不希望使用它,可以通过将access属性设置为AccessLevel.PRIVATE来将其设置为私有。因为必须设置final属性,所以还要将force属性设置为true,这将导致Lombok生成的构造函数将它们设置为null

还添加了一个@RequiredArgsConstructor@Data隐式地添加了一个必需的有参构造函数,但是当使用@NoArgsConstructor时,该构造函数将被删除。显式的@RequiredArgsConstructor确保除了私有无参数构造函数外,仍然有一个必需有参构造函数。

现在让我们转到Taco类,看看如何将其注解为JPA实体。程序清单3.17Taco注解为实体

package tacos;

import java.util.Date;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.PrePersist;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import lombok.Data;

@Data
@Entity
public class Taco {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;

    @NotNull
    @Size(min=5, message="Name must be at least 5 characters long")
    private String name;

    private Date createdAt;

    @ManyToMany(targetEntity=Ingredient.class)
    @Size(min=1, message="You must choose at least 1 ingredient")
    private List<Ingredient> ingredients;

    @PrePersist
    void createdAt() {
        this.createdAt = new Date();
    }
}

Ingredient一样,Taco类现在使用@Entity注解,其id属性使用@Id注解。因为依赖于数据库自动生成id值,所以还使用@GeneratedValue注解id属性,指定自动策略。

要声明Taco及其相关Ingredient列表之间的关系,可以使用@ManyToMany注解ingredient属性。一个Taco可以有很多Ingredient,一个Ingredient可以是很多Taco的一部分。

还有一个新方法createdAt(),它用@PrePersist注解。将使用它将createdAt属性设置为保存Taco之前的当前日期和时间。最后,让我们将Order对象注解为一个实体。下一个程序清单展示了新的Order类。程序清单3.18Order注解为JPA实体

package tacos;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.PrePersist;
import javax.persistence.Table;
import javax.validation.constraints.Digits;
import javax.validation.constraints.Pattern;
import org.hibernate.validator.constraints.CreditCardNumber;
import org.hibernate.validator.constraints.NotBlank;
import lombok.Data;

@Data
@Entity
@Table(name="Taco_Order")
public class Order implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;

    private Date placedAt;

    ...

    @ManyToMany(targetEntity=Taco.class)
    private List<Taco> tacos = new ArrayList<>();

    public void addDesign(Taco design) {
        this.tacos.add(design);
    }

    @PrePersist
    void placedAt() {
        this.placedAt = new Date();
    }
}

Order的更改与对Taco的更改非常相似。但是在类级别有一个新的注解:@Table。这指定订单实体应该持久化到数据库中名为Taco_Order的表中。

尽管可以在任何实体上使用这个注解,但它对于Order是必需的。没有它,JPA将默认将实体持久化到一个名为Order的表中,但是OrderSQL中是一个保留字,会导致问题。现在实体已经得到了正确的注解,该编写repository了。

3.2.3声明JPA repository

在存储库的JDBC版本中,显式地声明了希望repository提供的方法。但是使用Spring Data,扩展CrudRepository接口。例如,这是一个新的IngredientRepository接口:

package tacos.data;

import org.springframework.data.repository.CrudRepository;
import tacos.Ingredient;

public interface IngredientRepository extends CrudRepository<Ingredient, String> {
}

CrudRepositoryCRUD(创建、读取、更新、删除)操作声明了十几个方法。注意,它是参数化的,第一个参数是存储库要持久化的实体类型,第二个参数是实体id属性的类型。对于IngredientRepository,参数应该是IngredientString类型。

也可以这样定义TacoRepository

package tacos.data;

import org.springframework.data.repository.CrudRepository;
import tacos.Taco;

public interface TacoRepository extends CrudRepository<Taco, Long> {
}

IngredientRepositoryTacoRepository之间唯一显著的区别是对于CrudRepository的参数不同。在这里,它们被设置为TacoLong去指定Taco实体(及其id类型)作为这个respository接口的持久化单元。最后,同样的更改可以应用到OrderRepository

package tacos.data;

import org.springframework.data.repository.CrudRepository;
import tacos.Order;

public interface OrderRepository extends CrudRepository<Order, Long> {
}

现在有了这三个repository,可能认为需要为这三个repository编写实现,还包括每种实现的十几个方法。但这就是Spring Data JPA优秀的地方 —— 不需要编写实现!当应用程序启动时,Spring Data JPA会动态地自动生成一个实现。这意味着repository可以从一开始就使用。只需将它们注入到控制器中,就像在基于JDBC的实现中所做的那样。

CrudRepository提供的方法非常适合用于实体的通用持久化。但是如果有一些基本持久化之外的需求呢?让我们看看如何自定义repository来执行域特有的查询。

3.2.4自定义JPA repository

想象一下,除了CrudRepository提供的基本CRUD操作之外,还需要获取投递给指定邮政编码的所有订单。事实证明,通过在OrderRepository中添加以下方法声明可以很容易地解决这个问题:

List<Order> findByDeliveryZip(String deliveryZip);

在生成repository实现时,Spring Data检查存储库接口中的任何方法,解析方法名称,并尝试在持久化对象的上下文中理解方法的用途(在本例中是Order。本质上,Spring Data定义了一种小型的领域特定语言(DSL,其中持久化细节用repository中的方法签名表示。

Spring Data知道这个方法是用来查找订单的,因为已经用Order参数化了CrudRepository。方法名findByDeliveryZip()表明,该方法应该通过将其deliveryZip属性与作为参数,传递给匹配的方法来查找所有订单实体。

findByDeliveryZip()方法非常简单,但是Spring Data也可以处理更有趣的方法名。repository的方法由一个动词、一个可选的主语、单词by和一个谓词组成。在findByDeliveryZip()中,动词是find,谓词是DeliveryZip,主语没有指定,暗示是一个Order

让我们考虑另一个更复杂的例子。假设需要查询在给定日期范围内投递给指定邮政编码的所有订单。在这种情况下,当添加到OrderRepository时,下面的方法可能会被证明是有用的:

List<Order> readOrdersByDeliveryZipAndPlacedAtBetween(
    String deliveryZip, Date startDate, Date endDate);

3.2说明了在生成respository实现时,Spring Data如何解析和理解readOrdersByDeliveryZipAndPlacedAtBetween()方法。可以看到,readOrdersByDeliveryZipAndPlacedAtBetween()中的动词是readSpring Data还将findreadget理解为获取一个或多个实体的同义词。另外,如果只希望方法返回一个带有匹配实体计数的int,也可以使用count作为动词。

![3.2](E:\Document\spring-in-action-v5-translate\第一部分Spring基础\第三章 处理数据\3.2.jpg)

3.2 Spring Data解析repository方法特征来确定如何运行查询语句

尽管该方法的主语是可选的,但在这里它表示OrderSpring Data会忽略主题中的大多数单词,因此可以将方法命名为readPuppiesBy…它仍然可以找到Order实体,因为这是CrudRepository参数化的类型。

谓词跟在方法名中的By后面,是方法签名中最有趣的部分。在本例中,谓词引用两个Order属性:deliveryZipplacedAtdeliveryZip属性必须与传递给方法的第一个参数的值一致。Between关键字表示deliveryZip的值必须位于传入方法最后两个参数的值之间。

除了一个隐式的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

作为IgnoringCaseIgnoresCase的替代方法,可以在方法上放置AllIgnoringCaseAllIgnoresCase来忽略所有String比较的大小写。例如,考虑以下方法:

List<Order> findByDeliveryToAndDeliveryCityAllIgnoresCase(
    String deliveryTo, String deliveryCity);

最后,还可以将OrderBy放在方法名的末尾,以便根据指定的列对结果进行排序。例如,通过deliveryTo属性来订购:

List<Order> findByDeliveryCityOrderByDeliveryTo(String city);

虽然命名约定对于相对简单的查询很有用,但是对于更复杂的查询,不需要太多的想象就可以看出方法名称可能会失控。在这种情况下,可以随意将方法命名为任何想要的名称,并使用@Query对其进行注解,以显式地指定调用方法时要执行的查询,如下例所示:

@Query("Order o where o.deliveryCity='Seattle'")
List<Order> readOrdersDeliveredInSeattle();

在这个@Query的简单用法中,请求在西雅图交付的所有订单。但是也可以使用@Query来执行几乎任何想要的查询,即使通过遵循命名约定来实现查询很困难或不可能。

上一页
下一页