使用Spring Data JDBC
Spring Data 是一个非常大的伞形项目,由多个子项目组成,其中大多数子项目都关注对不同的数据库类型进行数据持久化。最流行的几个 Spring Data 项目如下。
-
Spring Data JDBC:对关系型数据库进行 JDBC 持久化。
-
Spring Data JPA:对关系型数据库进行 JPA 持久化。
-
Spring Data MongoDB:持久化到 Mongo 文档数据库。
-
Spring Data Neo4j:持久化到 Neo4j 图数据库。
-
Spring Data Redis:持久化到 Redis 键-值存储。
-
Spring Data Cassandra:持久化到 Cassandra 列存储数据库。
Spring Data 为各种项目提供了一项有趣且有用的特性:基于存储库规范接口自动创建存储库。因此,使用 Spring Data 项目进行持久化只有很少(甚至没有)持久化相关的逻辑,只需要编写一个或多个存储库接口。
我们看一下如何将 Spring Data JDBC 用到我们的项目中,以简化JDBC的数据持久化。首先,需要将 Spring Data JDBC 添加到项目构建中。
添加Spring Data JDBC到构建文件中
对于 Spring Boot 应用,Spring Data JDBC 能够以 starter 依赖的形式添加进来。在添加到项目的 pom.xml 文件时,starter 依赖如程序清单3.12所示。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
现在,不再需要为我们提供 JdbcTemplate 依赖了,所以我们可以移除如下所示的这个 starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
我们依然还需要数据库,所以不会移除 H2 依赖。
定义存储库接口
好消息是,我们已经创建了 IngredientRepository 和 OrderRepository,所以定义存储库的很多工作其实已经完成了。但是,我们需要对它们做一些细微的改变,从而以 Spring Data JDBC 的方式使用它们。
Spring Data 会在运行时自动生成存储库接口的实现。但是,只有当接口扩展自 Spring Data 提供的存储库接口时,它才会帮我们实现这一点。至少,我们的存储库接口需要扩展 Repository,这样 Spring Data 才会自动为我们创建实现。例如,我们可以按照如下的方式编写 IngredientRepository,让它扩展 Repository 接口:
package tacos.data;
import java.util.Optional;
import org.springframework.data.repository.Repository;
import tacos.Ingredient;
public interface IngredientRepository
extends Repository<Ingredient, String> {
Iterable<Ingredient> findAll();
Optional<Ingredient> findById(String id);
Ingredient save(Ingredient ingredient);
}
可以看到,Repository 接口是参数化的,其中第一个参数是该存储库要持久化的对象类型,在本例中也就是 Ingredient。第二个参数是要持久化对象的 ID 字段的类型,对于 Ingredient 来说,也就是 String。
尽管 IngredientRepository 可以像展示的这样通过扩展 Repository 实现我们的功能,但是 Spring Data 也为一些常见的操作,提供了 CrudRepository 基础接口,其中就包含了我们在 IngredientRepository 中所定义的 3 个方法。所以,与其扩展 Repository,还不如扩展 CrudRepository,如程序清单3.13所示。
package tacos.data;
import org.springframework.data.repository.CrudRepository;
import tacos.Ingredient;
public interface IngredientRepository
extends CrudRepository<Ingredient, String> {
}
类似地,我们的 OrderRepository 也可以扩展 CrudRepository,如程序清单3.14所示。
package tacos.data;
import org.springframework.data.repository.CrudRepository;
import tacos.TacoOrder;
public interface OrderRepository
extends CrudRepository<TacoOrder, Long> {
}
在这两种情况下,因为 CrudRepository 已经定义了我们需要的方法,所以没有必要在 IngredientRepository 和 OrderRepository 中显式定义它们了。
现在,我们有了两个存储库。你可能会想,我们接下来需要为这两个存储库(甚至包括 CrudRepository 中定义的十多个方法)编写实现了。但是,关于 Spring Data 的好消息是:我们根本不需要编写任何实现!当应用启动的时候,Spring Data 会在运行时自动生成一个实现。这意味着存储库已经准备就绪,我们将其注入控制器就可以了。
更重要的是,因为 Spring Data 会在运行时自动创建这些接口的实现,所以我们不需要 JdbcIngredientRepository 和 JdbcOrderRepository 这两个显式实现。我们可以干脆利落地删除这两个类。
为领域类添加持久化的注解
我们唯一需要做的就是为领域类添加注解,这样 Spring Data JDBC 才能知道如何持久化它们。一般来讲,这涉及为标识属性添加 @Id,以让 Spring Data 知道哪个字段代表了对象的唯一标识,并且我们可能需要在类上添加 @Table 注解,不过后面的这个步骤是可选的。
举例来说,TacoOrder 类可能需要添加 @Table 和 @Id 注解,如程序清单3.15所示。
package tacos;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.validation.constraints.Digits;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import org.hibernate.validator.constraints.CreditCardNumber;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
import lombok.Data;
@Data
@Table
public class TacoOrder implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private Long id;
// ...
}
@Table 注解完全是可选的。默认情况下,对象会基于领域类的名称映射到数据库的表上。在本例中,TacoOrder 会映射至名为 “Taco_Order” 的表。如果这种行为对你来说没有问题,那么就可以完全删除 @Table 注解,或者在使用它的时候不添加任何的参数。但是,如果想要映射至一个不同的表名,可以在 @Table上 指定表名,如下所示:
@Table("Taco_Cloud_Order")
public class TacoOrder {
...
}
如代码所示,TacoOrder 会被映射到名为 “Taco_Cloud_Order” 的表上。至于 @Id 注解,它指定 id 属性作为 TacoOrder 的唯一标识。TacoOrder 中的其他属性会根据其属性名自动映射到数据库的列上。例如,deliveryName 会自动映射至名为 delivery_name 的列。但是,如果想要显式定义列名映射,可以按照如下的方式为属性添加 @Column 注解:
@Column("customer_name")
@NotBlank(message = "Delivery name is required")
private String deliveryName;
在本例中,@Column 声明 deliveryName 属性会映射到名为 customer_name 的列上。
我们还需要将 @Table 和 @Id 用到其他的领域类上,包括 Ingredient,如程序清单3.16所示。
package tacos;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Persistable;
import org.springframework.data.relational.core.mapping.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Table
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
public class Ingredient implements Persistable<String> {
@Id
private String id;
// ...
}
另外,还包括 Taco 类,如程序清单3.17所示。
package tacos;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
import lombok.Data;
@Data
@Table
public class Taco {
@Id
private Long id;
// ...
}
至于 IngredientRef,它会自动映射到名为 “Ingredient_Ref” 的表,这恰好满足我们应用的需要。你如果愿意,可以为其添加 @Table 注解,但这并不是必需的。而且,Ingredient_Ref 表中没有唯一标识字段,因此没有必要为 IngredientRef 中的任何字段添加 @Id 注解。
完成了这些小的变更,尤其是移除了 JdbcIngredientRepository 和 JdbcOrderRepository 之后,我们的持久化代码少了很多。即便如此,Spring Data 在运行时生成的存储库实现依然能够完成使用 JdbcTemplate 的存储库所做的所有功能。实际上,它们完全可以做更多的事情,因为这两个存储库接口都扩展了 CrudRepository 接口,这个接口提供了十多种操作来创建、读取、更新和删除对象。
使用CommandLineRunner预加载数据
使用 JdbcTemplate 时,我们在应用启动阶段会借助 data.sql 预加载 Ingredient 数据,这个过程会在数据源bean初始化的时候针对数据库来执行。同样的方式也适用于 Spring Data JDBC。实际上,它适用于任何以关系型数据库作为支撑数据库的持久化机制。但是,不妨看一下另一种在启动时填充数据库的方式,这种方式会提供更多的灵活性。
Spring Boot 提供了两个非常有用的接口,用于在应用启动的时候执行一定的逻辑,即 CommandLineRunner 和 ApplicationRunner。这两个接口非常类似,它们都是函数式接口,都需要实现一个 run() 方法。当应用启动的时候,应用上下文中所有实现了 CommandLineRunner 或 ApplicationRunner 的 bean 都会执行其 run() 方法,执行时机是应用上下文和所有 bean 装配完毕之后、所有其他功能执行之前。这为将数据加载到数据库中提供了便利。
因为 CommandLineRunner 和 ApplicationRunner 都是函数式接口,所以在配置类中可以很容易地将其声明为 bean,只需在一个返回 lambda 表达式的方法上使用 @Bean 注解。例如,下面展现了如何创建一个数据加载的 CommandLineRunner bean:
@Bean
public CommandLineRunner dataLoader(IngredientRepository repo) {
return args -> {
repo.save(new Ingredient("FLTO", "Flour Tortilla", Type.WRAP));
repo.save(new Ingredient("COTO", "Corn Tortilla", Type.WRAP));
repo.save(new Ingredient("GRBF", "Ground Beef", Type.PROTEIN));
repo.save(new Ingredient("CARN", "Carnitas", Type.PROTEIN));
repo.save(new Ingredient("TMTO", "Diced Tomatoes", Type.VEGGIES));
repo.save(new Ingredient("LETC", "Lettuce", Type.VEGGIES));
repo.save(new Ingredient("CHED", "Cheddar", Type.CHEESE));
repo.save(new Ingredient("JACK", "Monterrey Jack", Type.CHEESE));
repo.save(new Ingredient("SLSA", "Salsa", Type.SAUCE));
repo.save(new Ingredient("SRCR", "Sour Cream", Type.SAUCE));
};
}
在这里,IngredientRepository 被注入 bean 方法,并在 lambda 表达式中被用以创建 Ingredient 对象。CommandLineRunner 的 run() 方法接受一个参数,这是 String 类型的可变长度参数(vararg),其中包含了运行应用时所有的命令行参数。在将配料加载到数据库时,我们不需要这些参数,所以 args 参数可以忽略。
作为替代方案,我们还可以将数据加载的 bean 定义为 ApplicationRunner 的 lambda 表达式实现,如下所示:
@Bean
public ApplicationRunner dataLoader(IngredientRepository repo) {
return args -> {
repo.save(new Ingredient("FLTO", "Flour Tortilla", Type.WRAP));
repo.save(new Ingredient("COTO", "Corn Tortilla", Type.WRAP));
repo.save(new Ingredient("GRBF", "Ground Beef", Type.PROTEIN));
repo.save(new Ingredient("CARN", "Carnitas", Type.PROTEIN));
repo.save(new Ingredient("TMTO", "Diced Tomatoes", Type.VEGGIES));
repo.save(new Ingredient("LETC", "Lettuce", Type.VEGGIES));
repo.save(new Ingredient("CHED", "Cheddar", Type.CHEESE));
repo.save(new Ingredient("JACK", "Monterrey Jack", Type.CHEESE));
repo.save(new Ingredient("SLSA", "Salsa", Type.SAUCE));
repo.save(new Ingredient("SRCR", "Sour Cream", Type.SAUCE));
};
}
CommandLineRunner 和 ApplicationRunner 的关键区别在于传递给各自 run() 方法的参数。CommandLineRunner 会接受一个 String 类型的可变长度参数,代表传递给命令行的原始参数。但是,ApplicationRunner 会接受一个 ApplicationArguments 参数,其提供了访问已解析命令行组件参数的方法。
例如,假设我们的应用接受命令行参数 “--version 1.2.3”,而且我们的数据加载 bean 需要考虑这个参数。如果使用 CommandLineRunner,那么我们需要在数组中搜索 “--version”,然后精确地去找数组中的下一个值。但是,如果使用 ApplicationRunner,那么我们可以查询给定的 ApplicationArguments 对象以获取 “--version” 参数,如下所示:
public ApplicationRunner dataLoader(IngredientRepository repo) {
return args -> {
List<String> version = args.getOptionValues("version");
...
};
}
getOptionValues() 方法会返回一个 List<String>,允许可变参数多次设置它的值。不过,不管是使用 CommandLineRunner 还是 ApplicationRunner,我们加载数据都不需要命令行参数。所以,在数据加载 bean 中,我们可以忽略 args 参数。
使用 CommandLineRunner 或 ApplicationRunner 初始化数据加载的好处在于,它们使用存储库来创建要持久化的对象,而不是使用 SQL 脚本。这意味着这种方式对关系型数据库和非关系型数据库同样有效。在第 4 章使用 Spring Data 持久化到非关系型数据库时,这一点会发挥其优势。
但在此之前,我们先看一下用于持久化关系型数据库数据的另一个 Spring Data 项目:Spring Data JPA。