# 12.2.3 Cassandra 持久化实体映射

在第 3 章中，您在实体类型（Taco、Ingredient、Order 等等）上使用 JPA 规范提供的注解。这些注解将实体类型映射到要持久化的关系型数据库表上。但这些注解在使用 Cassandra 进行持久化时不起作用，Spring Data Cassandra 提供了一组自己的注解，用于完成类似的映射功能。

让我们从最简单的 Ingredient 类开始，这个新的 Ingredient 类如下所示：

```java
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 类时，事情会变得复杂。

{% code title="程序清单 12.1 为 Taco 类添加 Cassandra 持久化注解" %}

```java
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;
}
```

{% endcode %}

正如您所看到的，映射 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` 的缩写，表示用户自定义类型）：

```java
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 是反范式的, 用户自定义类型包含其他表中的数据复本](https://867170852-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LrmLE3NwQoVJk02Q_BX%2Fsync%2Ff9d7fe2ad8aeb1aebafddef8a3ad5e7b20e89b84.png?generation=1616675849279830\&alt=media)

具体到您刚刚创建的用户自定义类型，请注意 Taco 有一个 IngredientUDT，它保存从 Ingredient 对象复制的数据。当一个 Taco 被持久化的时候，是 Taco 对象和其中的 IngredientUDT 列表被保存到 tacos 表中。IngredientUDT 的列表数据全部保存到 ingredients 列中。

另一种方法可以帮助您理解用户自定义类型的使用，就是查询 tacos 表在数据库中的数据。使用 CQL 和 Cassandra 附带的 cqlsh 工具可以看到以下结果：

```sql
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 类。

{% code title="程序清单 12.2 映射 Order 类到 Cassandra 数据库的 tacoorders 表" %}

```java
@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);
    }

}
```

{% endcode %}

程序清单 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 类非常相似，不过它包含引用其他用户定义类型的集合：

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

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

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

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

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

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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://potoyang.gitbook.io/spring-in-action-v5/di-12-zhang-xiang-ying-shi-chi-jiu-hua-shu-ju/12.2-shi-yong-xiang-ying-shi-cassandra-ku/12.2.3-cassandra-chi-jiu-hua-shi-ti-ying-she.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
