12.2.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.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;

@Data
@RequiredArgsConstructor
@NoArgsConstructor(access=AccessLevel.PRIVATE, force=true)
@Table("ingredients")
public class Ingredient {

    @PrimaryKey
    private final String id;
    private final String name;
    private final Type type;
    public static enum Type {
        WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
    }
}

Ingredient 类似乎否定了我所说的只需替换一些注解。在这里不用 JPA 持久化那样的 @Entity 注解,而是用 @Table 注解,以指示应该将 Ingredient 持久化到一张名为 ingredients 的表中。不是用 @id 注解在 id 属性上,而是用 @PrimaryKey 注解。到目前为止,你似乎只替换了很少的几个注解。

但别让 Ingredient 类欺骗了你。Ingredient 类是最简单的实体类型。当你处理 Taco 类时,事情会变得复杂。

package tacos;
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 org.springframework.data.rest.core.annotation.RestResource;
import com.datastax.driver.core.utils.UUIDs;
import lombok.Data;

@Data
@RestResource(rel="tacos", path="tacos")
@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;
}

正如您所看到的,映射 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 属性的副本。事实上,用户自定义的类型可能包含您需要的任何属性,它不需要与任何表定义进行一对一的映射。

我意识到您现在可能没有一个清晰的完整视图,来理解用户自定义类型中的数据是如何关联,并持久化到库中的。图 12.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 对象。

您可能注意到 图 12.1 中的其他用户自定义类型。您需要继续将其他实体映射到 Cassandra 表。还需要加一些注解,包括 Order 类。下一个清单展示了为 Cassandra 持久化进行注解的 Order 类。

@Data
@Table("tacoorders")
public class Order implements Serializable {

    private static final long serialVersionUID = 1L;

    @PrimaryKey
    private UUID id = UUIDs.timeBased();

    private Date placedAt = new Date();

    @Column("user")
    private UserUDT user;

    // delivery and credit card properties omitted for brevity's sake

    @Column("tacos")
    private List<TacoUDT> tacos = new ArrayList<>();

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

}

程序清单 12.2 故意省略了 Order 类的一些属性,这些属性本身并不适用对 Cassandra 数据建模的探讨。剩下的一些属性和映射,类似于 Taco 上的注解。@Table 用于将 Order 映射到 tacoorders 表。在里,由于您不关心排序,id 属性只需用 @PrimaryKey 注解,指定它既是一个分区键,又是一个具有默认顺序的聚类键。

tacos 属性很有趣,因为它是一个 List<TackUDT>, 而不是一个 Taco 列表。这里 Order 和 Taco/TacoUDT 之间的关系,类似于 Taco 和 Ingredient/IngredientUDT 的关系。也就是说,不是通过外键将表中的多行数据链接起来,而是在 Order 表中包含所有相关的 taco 数据,以优化表的读取速度。

类似地,user 属性引用 UserUDT 对象,并把数据持久化到 user 列中。同样,这与关系数据库的外键策略形成了鲜明对比。

至于 TacoUDT 类,它与 IngredientUDT 类非常相似,不过它包含引用其他用户定义类型的集合:

@Data
@UserDefinedType("taco")
public class TacoUDT {

    private final String name;
    private final List<IngredientUDT> ingredients;
}

UserUDT 类也差不多,只是它有三个属性而不是两个:

@UserDefinedType("user")
@Data
public class UserUDT {

    private final String username;
    private final String fullname;
    private final String phoneNumber;
}

尽管,重用在第3章中创建的实体类,或者把一些 JPA 注解换成 Cassandra 注解,应该更方便,但 Cassandra 持久化的本质特性决定了不能这样做。它要求您重新思考数据的建模方式。现在实体都已经映射了,可以编写 Repository 了。

results matching ""

    No results matching ""