13.1.1 为 R2DBC 定义实体
为了了解 Spring Data R2DBC,我们将重新创建 Taco Cloud 应用程序的持久化层。我们只关保存订单数据,包括为 TacoOrder、Taco 和 Ingredient 创建领域实体,以及每个实体对应的 Repository 。
我们要创建的第一个实体类是 Ingredient 类。看起来有点像清单 13.1 所示:
清单 13.1 使用 R2DBC 持久化的 Ingredient 实体类。
package tacos;
import org.springframework.data.annotation.Id;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
@Data
@NoArgsConstructor
@RequiredArgsConstructor
@EqualsAndHashCode(exclude = "id")
public class Ingredient {
@Id
private Long id;
private @NonNull String slug;
private @NonNull String name;
private @NonNull Type type;
public static enum Type {
WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
}
}
正如您所看到的,这和我们以前创建过的 Ingredient 类没有太大的不同。有两个值得注意的区别:
- Spring Data R2DBC 要求属性具有 setter 方法。因此,不能定义大多数属性为 Final 属性,它们必须是非 Final 属性。但要帮助 Lombok 创造一个带参构造函数,我们给大多数属性加了 @NonNull 注解。还有 @RequiredArgsConstructor 注解,这将使 Lombok 生成的构造函数中包含这些属性。
- 通过 Spring Data R2DBC Repository 保存对象时,如果对象的 ID 属性为非 null,将其视为更新。对于 Ingredient 对象,id 属性以前是字符串类型,并在创建时指定。但这样会导致 Spring Data R2DBC 报错。因此,这里我们将该字符串 ID 转为用属性 slug 存储,这是一个 Ingredient 的伪 id。我们使用 Long 类型的 id 属性,该属性的值将由数据库自动生成。
对应的数据库表在 schema.sql 中定义:
create table Ingredient (
id identity,
slug varchar(4) not null,
name varchar(25) not null,
type varchar(10) not null
);
Taco 实体类也非常类似于它的 Spring Data JDBC 版本,如清单 13.2 所示。
清单 13.2 使用 R2DBC 持久化的 Taco 实体类。
package tacos;
import java.util.HashSet;
import java.util.Set;
import org.springframework.data.annotation.Id;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
@Data
@NoArgsConstructor
@RequiredArgsConstructor
public class Taco {
@Id
private Long id;
private @NonNull String name;
private Set<Long> ingredientIds = new HashSet<>();
public void addIngredient(Ingredient ingredient) {
ingredientIds.add(ingredient.getId());
}
}
与 Ingredient 类一样,我们必须在实体字段上添加 setter 方法,因此字段上添加 @NonNull 注解,而不是 final 类型。
但这里特别有趣的是,我们没有定义集合类型的 Ingredient 属性。Taco 实体中用一个 Set<Long>
属性,引用了所有 Ingredient 的ID。使用 Set 而不是 List,是要保证唯一性。但是为什么我们必须使用 Set<Long>
而不是 Set<Ingredient>
呢?
与其他 Spring Data 项目不同,Spring Data R2DBC 目前不支持直接的实体间关联(至少目前不行)。作为一个相对较新的项目,Spring Data R2DBC 仍在努力解决,以非阻塞方式处理关系型数据的一些挑战。这可能会在 Spring Data R2DBC 的未来版本中做出改进。
在那之前,我们不能让 Taco 直接引用一组 Ingredient 并进行持久性。在处理实体关联时,我们有几个选择:
- 定义实体中只引用相关对象的 ID。在这种情况下,则必须在数据库表中的相应列中使用数组类型。H2 和 PostgreSQL 是两个支持数组列的数据库,但很多其他数据库没有支持。此外,即使数据库支持数组列,也可能无法将属性定义为引用其他表的外键,来保证关联参照完整性。
- 定义实体及其对应的表,使它们彼此完全匹配。对于集合,这意味着引用的对象将有一个列映射回引用表。例如,Taco 对象的表需要有一个指向 TacoOrder 的列。
- 将引用的实体序列化为 JSON,并将 JSON 存储在一个大的 VARCHAR 列中。如果不需要查询到被引用的对象,这种方法尤其有效。但是,由于 VARCHAR 列的长度有限制,它对 JSON 序列化对象的大小有潜在的限制。此外,没办法保证引用的完整性,因为引用的对象被存储为简单的字符串值了(这可以包含任何内容)。
尽管这些选择都不理想,但在权衡了各种选项后,我们将为 Taco 选择第一个选项。Taco 类有一个 Set<Long>
,该集合引用一个或多个Ingredient 的 ID。这意味着相应的表必须有一个数组列来存储这些 ID。对于 H2 数据库,Taco 表的定义如下:
create table Taco (
id identity,
name varchar(50) not null,
ingredient_ids array
);
ingredient_ids 列上使用的数组类型特定于 H2。对于 PostgreSQL,这列可能定义为 integer[]。若您选择的是其他数据库类型,请参阅您相关的数据库文档,查找如何定义数组列。请注意,并非所有数据库实现都支持数组列,因此您可能需要为关系建模选择其他选项之一。
最后,如清单 13.3 所示,TacoOrder 类使用的许多东西我们已经介绍过了。
清单13.3 使用 R2DBC 持久化的 TacoOrder 实体类。
package tacos;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.data.annotation.Id;
import lombok.Data;
@Data
public class TacoOrder {
@Id
private Long id;
private String deliveryName;
private String deliveryStreet;
private String deliveryCity;
private String deliveryState;
private String deliveryZip;
private String ccNumber;
private String ccExpiration;
private String ccCVV;
private Set<Long> tacoIds = new LinkedHashSet<>();
private List<Taco> tacos = new ArrayList<>();
public void addTaco(Taco taco) {
this.tacos.add(taco);
}
}
正如您所看到的,除了拥有更多的属性之外,类 TacoOrder 也遵循和 Taco 类同样的规则。它通过一个 Set<Long>
引用其 Taco 对象。尽管如此,稍后,我们将看到如何将完整的 Taco 对象放入 TacoOrder,即使 Spring Data R2DBC 不支持这种方式。
Taco_Order 表的数据库结构如下所示:
create table Taco_Order (
id identity,
delivery_name varchar(50) not null,
delivery_street varchar(50) not null,
delivery_city varchar(50) not null,
delivery_state varchar(2) not null,
delivery_zip varchar(10) not null,
cc_number varchar(16) not null,
cc_expiration varchar(5) not null,
cc_cvv varchar(3) not null,
taco_ids array
);
就像 Taco 表一样,TacoOrder 表使用数组列 taco_ids 引用 taco 对象。再说一次,这个模式只用于 H2 数据库;请查阅相关数据库文档,以了解有关创建数组列的支持的详细信息。
通常,生产应用程序已经通过其他方式和方法定义了数据库 schema,除了测试之外,这样的脚本方式是不可取的。因此,这个 bean 是在配置中定义的,它仅在运行自动测试时加载,在应用程序运行时不可用。我们将在定义服务之后,看一个测试 R2DBC Repository 的示例。
此外,请注意,这个 bean 只使用类路径根目录中的“schema.sql”文件(在项目 src/main/resources
路径下)。如果您希望其他 SQL 脚本也作为数据库初始化的一部分,可在调用 populator.addPopulators()
时添加更多 ResourceDatabasePopulator 对象。
现在我们已经定义了实体及其对应的数据库 schema,让我们创建 Repository ,通过它我们将保存和获取 Taco 数据。