4.1.3 Cassandra 持久化实体映射
在第 3 章中,您在实体类型(Taco、Ingredient、Order 等等)上使用 JPA 规范提供的注解。这些注解将实体类型映射到要持久化的关系型数据库表上。但这些注解在使用 Cassandra 进行持久化时不起作用,Spring Data Cassandra 提供了一组自己的注解,用于完成类似的映射功能。
让我们从最简单的 Ingredient 类开始,这个新的 Ingredient 类如下所示:
package tacos;
import org.springframework.data.cassandra.core.mapping.PrimaryKey;
import org.springframework.data.cassandra.core.mapping.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor(access=AccessLevel.PRIVATE, force=true)
@Table("ingredients")
public class Ingredient {
@PrimaryKey
private String id;
private String name;
private Type type;
public static enum Type {
WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
}
}
Ingredient 类似乎否定了我所说的只需替换一些注解。在这里不用 JPA 持久化那样的 @Entity
注解,而是用 @Table
注解,以指示应该将 Ingredient 持久化到一张名为 ingredients
的表中。不是用 @id
注解在 id 属性上,而是用 @PrimaryKey
注解。到目前为止,您似乎只替换了很少的几个注解。
但别让 Ingredient 类欺骗了您。Ingredient 类是最简单的实体类型。当您处理 Taco 类时,事情会变得复杂。
程序清单 4.1 为 Taco 类添加 Cassandra 持久化注解
package tacos;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.springframework.data.cassandra.core.cql.Ordering;
import org.springframework.data.cassandra.core.cql.PrimaryKeyType;
import org.springframework.data.cassandra.core.mapping.Column;
import org.springframework.data.cassandra.core.mapping.PrimaryKeyColumn;
import org.springframework.data.cassandra.core.mapping.Table;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import lombok.Data;
@Data
@Table("tacos")
public class Taco {
@PrimaryKeyColumn(type=PrimaryKeyType.PARTITIONED)
private UUID id = Uuids.timeBased();
@NotNull
@Size(min = 5, message = "Name must be at least 5 characters long")
private String name;
@PrimaryKeyColumn(type=PrimaryKeyType.CLUSTERED,
ordering=Ordering.DESCENDING)
private Date createdAt = new Date();
@Size(min=1, message="You must choose at least 1 ingredient")
@Column("ingredients")
private List<IngredientUDT> ingredients = new ArrayList<>();
public void addIngredient(Ingredient ingredient) {
this.ingredients.add(TacoUDRUtils.toIngredientUDT(ingredient));
}
}
正如您所看到的,映射 Taco 类的内容更为复杂。与 Ingredient 一样, @Table
注解用于将 TACO 类标识为使用 tacos
表进行保存。但这是唯一与 Ingredient 类相似的地方。
id 属性仍然是主键,但它只是两个主键列中的一个。更具体地说,id 属性使用注解 @PrimaryKeyColumn
,且设置类型为 PrimaryKeyType.PARTITIONED
。 这样设置指定了 id 属性作为分区键,用于确定每行 taco 应该将数据写入哪个 Cassandra 分区。
您还注意到 id 属性现在是 UUID,而不是 Long 类型。尽管不是强制的,但 ID 值属性通常为 UUID 类型。此外,新 Taco 对象的 UUID 是基于时间的 UUID 。(但从数据库读取已有的 Taco 时,可能会覆盖该值)。
再往下一点,您会看到 createdAt 属性被映射为主键列的另一个属性。本例中,设置了 @PrimaryKeyColumn
的 type 属性为 PrimaryKeyType.CLUSTERED
,它将 createdAt 属性指定为聚类键。如前所述,聚类键用于确定分区中的行数据的顺序。更具体地说,排序设置为降序。因此,在给定的分区中,较新行首先出现在 tacos 表中。
最后,ingredients
属性现在是一个 IngredientUDT
对象的列表。正如您所记得的,Cassandra 表是非规范化的,可能包含从其他表复制的数据。虽然 ingredients
表将作为所有可用 Ingredient 的记录表,但每个 taco
的 Ingredient 会在 ingredients
中重复出现。这不仅仅是简单地引用 ingredients
表中的一行或多行,而是在 ingredients
属性中包含完整数据。
但为什么要引入一个新的 IngredientUDT 类呢?为什么不重用 Ingredient 类呢?简单地说,包含数据集合的列,例如 ingredients
列,必须是基本类型(整数、字符串等)或用户自定义类型的集合。
在 Cassandra 中,用户自定义的类型和基本类型相比,允许您声明更丰富的表和列属性。通常,它们类似关系型数据库的外键。但与外键不同,外键只保存在另一个表的行数据中。但用户自定义类型的列,实际上可能携带从另一个表的行中复制的数据。对于 tacos 表中的 ingredients
列,它将包含所有 ingredients
的数据。
不能将 Ingredient 类用作自定义的类型,因为 @Table
注解已经将其映射为 Cassandra 中持久化的一个实体。因此,您必须创建一个新类,来定义如何在 taco
表上的 ingredients
列。IngredientUDT 类用于达到此目的(其中 “UDT” 是 user defined type
的缩写,表示用户自定义类型):
package tacos;
import org.springframework.data.cassandra.core.mapping.UserDefinedType;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
@Data
@RequiredArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
@UserDefinedType("ingredient")
public class IngredientUDT {
private final String name;
private final Ingredient.Type type;
}
尽管 IngredientUDT 看起来很像 Ingredient,但它的映射简单的多。它用 @UserDefinedType
注解,以将其标识为用户自定义的类型。除此之外,它就是一个具有一些属性的简单类。
您还将注意到,IngredientUDT 类并不包含 id 属性。尽管它可能包含从 Ingredient 复制来的 id 属性的副本。事实上,用户自定义的类型可能包含您需要的任何属性,它不需要与任何表定义进行一对一的映射。
我意识到您现在可能没有一个清晰的完整视图,来理解用户自定义类型中的数据是如何关联,并持久化到库中的。图 4.1 显示了整个 Taco Cloud 的数据模型,包括用户自定义的类型。
图 12.1 不用外链和关联, Cassandra 是反范式的, 用户自定义类型包含其他表中的数据复本。
具体到您刚刚创建的用户自定义类型,请注意 Taco 有一个 IngredientUDT,它保存从 Ingredient 对象复制的数据。当一个 Taco 被持久化的时候,是 Taco 对象和其中的 IngredientUDT 列表被保存到 tacos 表中。IngredientUDT 的列表数据全部保存到 ingredients 列中。
另一种方法可以帮助您理解用户自定义类型的使用,就是查询 tacos 表在数据库中的数据。使用 CQL 和 Cassandra 附带的 cqlsh 工具可以看到以下结果:
cqlsh:tacocloud> select id, name, createdAt, ingredients from tacos;
id | name | createdat | ingredients
---------+-----------+-----------+----------------------------------------
827390...| Carnivore | 2018-04...| [{name: 'Flour Tortilla', type: 'WRAP'},
{name: 'Carnitas', type: 'PROTEIN'},
{name: 'Sour Cream', type: 'SAUCE'},
{name: 'Salsa', type: 'SAUCE'},
{name: 'Cheddar', type: 'CHEESE'}]
(1 rows)
如您所见,id、name 和 createdAt 列包含简单值。它们与您熟悉的关系型数据的查询没有太大的不同。但是 ingredients 有点不同。因为这个列定义为包含用户自定义类型的集合(由 IngredientUDT 定义),它的值显示为一个 JSON 数组,其中包含 JSON 对象。
您可能注意到 图 4.1 中的其他用户自定义类型。您需要继续将其他实体映射到 Cassandra 表。还需要加一些注解,包括 TacoOrder 类。下一个清单展示了为 Cassandra 持久化进行注解的 TacoOrder 类。
程序清单 4.2 映射 TacoOrder 类到 Cassandra 数据库的 tacoorders 表
package tacos;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
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.cassandra.core.mapping.Column;
import org.springframework.data.cassandra.core.mapping.PrimaryKey;
import org.springframework.data.cassandra.core.mapping.Table;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import lombok.Data;
@Data
@Table("orders")
public class TacoOrder implements Serializable {
private static final long serialVersionUID = 1L;
@PrimaryKey
private UUID id = Uuids.timeBased();
private Date placedAt = new Date();
// delivery and credit card properties omitted for brevity's sake
@Column("tacos")
private List<TacoUDT> tacos = new ArrayList<>();
public void addTaco(TacoUDT taco) {
this.tacos.add(taco);
}
}
程序清单 4.2 故意省略了 TacoOrder 类的一些属性,这些属性本身并不适用对 Cassandra 数据建模的探讨。剩下的一些属性和映射,类似于 Taco 上的注解。@Table
用于将 TacoOrder 映射到 tacoorders 表。在里,由于您不关心排序,id 属性只需用 @PrimaryKey
注解,指定它既是一个分区键,又是一个具有默认顺序的聚类键。
tacos 属性很有趣,因为它是一个 List<TackUDT>
, 而不是一个 Taco 列表。这里 TacoOrder 和 Taco/TacoUDT 之间的关系,类似于 Taco 和 Ingredient/IngredientUDT 的关系。也就是说,不是通过外键将表中的多行数据链接起来,而是在 TacoOrder 表中包含所有相关的 taco 数据,以优化表的读取速度。
至于 TacoUDT 类,它与 IngredientUDT 类非常相似,不过它包含引用其他用户定义类型的集合:
package tacos;
import java.util.List;
import org.springframework.data.cassandra.core.mapping.UserDefinedType;
import lombok.Data;
@Data
@UserDefinedType("taco")
public class TacoUDT {
private final String name;
private final List<IngredientUDT> ingredients;
}
尽管,重用在第 3 章中创建的实体类,或者把一些 JPA 注解换成 Cassandra 注解,应该更方便,但 Cassandra 持久化的本质特性决定了不能这样做。它要求您重新思考数据的建模方式。现在实体都已经映射了,可以编写 Repository 了。