

# 使用 DynamoDB 作为在线商店的数据存储
<a name="data-modeling-online-shop"></a>

此使用案例讨论了使用 DynamoDB 作为在线商店（或电子商店）的数据存储。

## 使用案例
<a name="data-modeling-schema-online-shop"></a>

在线商店可让用户浏览不同的商品并最终购买它们。根据生成的发票，客户可以使用折扣码或礼品卡付款，然后使用信用卡支付剩余金额。采购的商品将从几个仓库中的一个仓库中挑选，并发运到提供的地址。在线商店的典型访问模式包括：
+ 获取给定 customerId 的客户
+ 获取给定 productId 的商品
+ 获取给定 warehouseId 的仓库
+ 通过 productId 获取所有仓库的商品库存
+ 获取给定 orderId 的订单
+ 获取给定 orderId 的所有商品
+ 获取给定 orderId 的发票
+ 获取给定 orderId 的所有货件
+ 获取给定日期范围内给定 productId 的所有订单
+ 获取给定 invoiceId 的发票
+ 获取给定 invoiceId 的所有付款
+ 获取给定 shipmentId 的货件详细信息
+ 获取给定 warehouseId 的货件
+ 获取给定 warehouseId 的所有商品的库存
+ 获取给定日期范围内给定 customerId 的所有发票
+ 获取给定日期范围内给定 customerId 订购的所有商品

## 实体关系图
<a name="data-modeling-schema-online-shop-erd"></a>

这是实体关系图（ERD），我们将使用它来将 DynamoDB 建模为在线商店的数据存储。

![\[此 ERD 表示包含 Product、Order、Payment 和 Customer 等实体的在线商店数据模型。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-1-ERD.png)


## 访问模式
<a name="data-modeling-schema-online-shop-access-patterns"></a>

当使用 DynamoDB 作为在线商店的数据存储时，我们将考虑这些访问模式。

1. `getCustomerByCustomerId`

1. `getProductByProductId`

1. `getWarehouseByWarehouseId`

1. `getProductInventoryByProductId`

1. `getOrderDetailsByOrderId`

1. `getProductByOrderId`

1. `getInvoiceByOrderId`

1. `getShipmentByOrderId`

1. `getOrderByProductIdForDateRange`

1. `getInvoiceByInvoiceId`

1. `getPaymentByInvoiceId`

1. `getShipmentDetailsByShipmentId`

1. `getShipmentByWarehouseId`

1. `getProductInventoryByWarehouseId`

1. `getInvoiceByCustomerIdForDateRange`

1. `getProductsByCustomerIdForDateRange`

## 架构设计的演变
<a name="data-modeling-schema-online-shop-design-evolution"></a>

使用[NoSQL Workbench for DynamoDB](workbench.md)，导入 [AnOnlineShop\$11.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_1.json)，以创建名为 `AnOnlineShop` 的新数据模型和名为 `OnlineShop` 的新表。请注意，我们使用通用名称 `PK` 和 `SK` 作为分区键和排序键。这是一种用于将不同类型的实体存储在同一个表中的做法。

**步骤 1：解决访问模式 1 (`getCustomerByCustomerId`)**

导入 [AnOnlineShop\$12.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_2.json) 以处理访问模式 1（`getCustomerByCustomerId`）。有些实体与其他实体没有关系，因此我们将对它们使用相同的 `PK` 和 `SK` 值。在示例数据中，请注意，键使用前缀 `c#`，以便将 `customerId` 与稍后添加的其他实体区分开来。对于其他实体也重复这种做法。

为了解决这种访问模式，[https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html) 操作可以与 `PK=customerId` 和 `SK=customerId` 结合使用。

**步骤 2：解决访问模式 2 (`getProductByProductId`)**

导入 [AnOnlineShop\$13.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_3.json)，以解决 `product` 实体的访问模式 2（`getProductByProductId`）。产品实体的前缀为 `p#`，并且使用了相同的排序键属性来存储 `customerID` 以及 `productID`。通用命名和[垂直分区](data-modeling-blocks.md#data-modeling-blocks-vertical-partitioning)允许我们创建这样的项目集合，以实现有效的单表设计。

为了解决这种访问模式，`GetItem` 操作可以与 `PK=productId` 和 `SK=productId` 结合使用。

**步骤 3：解决访问模式 3 (`getWarehouseByWarehouseId`)**

导入 [AnOnlineShop\$14.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_4.json)，以解决 `warehouse` 实体的访问模式 3（`getWarehouseByWarehouseId`）。我们目前已将 `customer`、`product` 和 `warehouse` 实体添加到同一个表中。它们使用前缀和 `EntityType` 属性进行区分。类型属性（或前缀命名）可提高模型的可读性。如果我们只是将不同实体的字母数字 ID 存储在同一属性中，可读性就会受到影响。在没有这些标识符的情况下，很难将一个实体与另一个实体区分开来。

为了解决这种访问模式，`GetItem` 操作可以与 `PK=warehouseId` 和 `SK=warehouseId` 结合使用。

**基表：**

![\[使用前缀和 EntityType 按仓库 ID 获取仓库数据的 DynamoDB 表设计。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-2-Step3.png)


**步骤 4：解决访问模式 4 (`getProductInventoryByProductId`)**

导入 [AnOnlineShop\$15.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_5.json) 以解决访问模式 4（`getProductInventoryByProductId`）。`warehouseItem` 实体用于跟踪每个仓库中的商品数量。在仓库中添加或移除商品时，通常会更新此项目。从 ERD 中可以看出，`product` 和 `warehouse` 之间存在多对多关系。在此处，从 `product` 到 `warehouse` 的一对多关系建模为 `warehouseItem`。稍后，也将对从 `warehouse` 到 `product` 的一对多关系进行建模。

访问模式 4 可以通过查询 `PK=ProductId` 和 `SK begins_with “w#“` 来解决。

有关 `begins_with()` 以及其他可应用于排序键的表达式的更多信息，请参阅[键条件表达式](Query.KeyConditionExpressions.md)。

**基表：**

![\[这一表设计用于查询 ProductID 和 warehouseId，来跟踪给定仓库中的产品库存。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-3-Step4.png)


**步骤 5：解决访问模式 5（`getOrderDetailsByOrderId`）和 6（`getProductByOrderId`）**

通过导入 [AnOnlineShop\$16.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_6.json)，向表中添加更多的 `customer`、`product` 和 `warehouse` 项目。然后，导入 [AnOnlineShop\$17.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_7.json)，为 `order` 构建一个项目集合，该集合可以处理访问模式 5（`getOrderDetailsByOrderId`）和 6（`getProductByOrderId`）。您可以看到建模为 orderItem 实体的 `order` 和 `product` 之间的一对多关系。

要解决访问模式 5（`getOrderDetailsByOrderId`），请使用 `PK=orderId` 查询表。这将提供有关订单的所有信息，包括 `customerId` 和订购的商品。

**基表：**

![\[这一表设计使用 orderId 进行查询，来获取有关所有已订购产品的信息。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-4-Step5.png)


要解决访问模式 6（`getProductByOrderId`），我们只需要读取 `order` 中的商品。使用 `PK=orderId` 和 `SK begins_with “p#”` 查询表来实现这一目标。

**基表：**

![\[这一表设计使用 orderId 和 productId 进行查询，来获取订单中的产品。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-5-Step5.png)


**步骤 6：解决访问模式 7（`getInvoiceByOrderId`）**

导入 [AnOnlineShop\$18.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_8.json)，将 `invoice` 实体添加到*订单* 项目集合中，以处理访问模式 7（`getInvoiceByOrderId`）。为了解决这种访问模式，您可以将查询操作与 `PK=orderId` 和 `SK begins_with “i#”` 结合使用。

**基表：**

![\[这一表设计在订单项目集合中使用发票实体，来按 orderId 获取发票。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-6-Step6.png)


**步骤 7：解决访问模式 8（`getShipmentByOrderId`)**

导入 [AnOnlineShop\$19.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_9.json)，将 `shipment` 实体添加到*订单* 项目集合中，以解决访问模式 8（`getShipmentByOrderId`）。我们通过在单表设计中添加更多类型的实体来扩展相同的垂直分区模型。请注意*订单* 项目集合如何包含 `order` 实体与 `shipment`、`orderItem` 和 `invoice` 实体之间的不同关系。

要按 `orderId` 获取货件，可以使用 `PK=orderId` 和 `SK begins_with “sh#”` 执行查询操作。

**基表：**

![\[这一表设计将货件实体添加到订单项目集合中，来按订单 ID 获取货件。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-7-Step7.png)


**步骤 8：解决访问模式 9（`getOrderByProductIdForDateRange`)**

我们在上一步中创建了一个*订单* 项目集合。此访问模式具有新的查找维度（`ProductID` 和 `Date`），这要求您扫描整个表并筛选掉相关记录以获取目标项目。为了解决这种访问模式，我们需要创建[全局二级索引（GSI）](GSI.md)。导入 [AnOnlineShop\$110.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_10.json) 以使用 GSI 创建新的项目集合，从而可以从多个*订单* 项目集合中检索 `orderItem` 数据。数据现在具有 `GSI1-PK` 和 `GSI1-SK`，它们将分别是 `GSI1` 的分区键和排序键。

DynamoDB 会自动将包含 GSI 的键属性的项目从表填充到 GSI。无需手动在 GSI 中进行任何其他插入。

要解决访问模式 9，请使用 `GSI1-PK=productId` 和 `GSI1SK between (date1, date2)` 对 `GSI1` 执行查询。

**基表：**

![\[使用 GSI 的表设计，用于从多个订单项目集合中获取订单数据。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-8-Step8-Base.png)


**GSI1：**

![\[此 GSI 设计以 ProductID 和 Date 作为分区键和排序键，来按产品 ID 和日期获取订单。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-9-Step8-GSI.png)


**步骤 9：解决访问模式 10（`getInvoiceByInvoiceId`）和 11（`getPaymentByInvoiceId`）**

导入 [AnOnlineShop\$111.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_11.json) 以解决访问模式问题 10（`getInvoiceByInvoiceId`）和 11（`getPaymentByInvoiceId`），这两者都与 `invoice` 相关。尽管这些是两种不同的访问模式，但它们是使用相同的键条件实现的。`Payments` 定义为 `invoice` 实体上具有映射数据类型的属性。

**注意**  
`GSI1-PK` 和 `GSI1-SK` 已重载以存储有关不同实体的信息，因此，可以从同一 GSI 提供多种访问模式。有关 GSI 重载的更多信息，请参阅[在 DynamoDB 中重载全局二级索引](bp-gsi-overloading.md)。

要解决访问模式 10 和 11，请使用 `GSI1-PK=invoiceId` 和 `GSI1-SK=invoiceId` 查询 `GSI1`。

**GSI1：**

![\[此 GSI 设计以 invoiceId 作为分区键和排序键，来通过发票 ID 获取发票和付款。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-10-Step9.png)


**步骤 10：解决访问模式 12（`getShipmentDetailsByShipmentId`）和 13（`getShipmentByWarehouseId`）**

导入 [AnOnlineShop\$112.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_12.json) 以解决访问模式问题 12（`getShipmentDetailsByShipmentId`）和 13（`getShipmentByWarehouseId`）。

请注意，`shipmentItem` 实体添加到基表上的*订单* 项目集合中，以便能够在单个查询操作中检索有关订单的所有详细信息。

**基表：**

![\[这一表设计在订单项目集合中使用 shipmentItem 实体，来获取所有订单详细信息。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-11-Step10.png)


`GSI1` 分区和排序键已用于对 `shipment` 和 `shipmentItem` 之间的一对多关系进行建模。要解决访问模式 12（`getShipmentDetailsByShipmentId`），请使用 `GSI1-PK=shipmentId` 和 `GSI1-SK=shipmentId` 查询 `GSI1`。

**GSI1：**

![\[此 GSI1 设计以 shipmentId 作为分区键和排序键，来按货件 ID 获取货件详细信息。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-12-Step10-GSI.png)


我们需要创建另一个 GSI（`GSI2`），来为访问模式 13（`getShipmentByWarehouseId`）的 `warehouse` 和 `shipment` 之间新的一对多关系建模。要解决此访问模式，请使用 `GSI2-PK=warehouseId` 和 `GSI2-SK begins_with “sh#”` 查询 `GSI2`。

**GSI2：**

![\[此 GSI2 设计以 warehouseId 和 shipmentId 作为分区键和排序键，以便按仓库获取货件。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-13-Step10-GSI2.png)


**步骤 11：解决访问模式 14（`getProductInventoryByWarehouseId`）、15（`getInvoiceByCustomerIdForDateRange`）和 16（`getProductsByCustomerIdForDateRange`）**

导入 [AnOnlineShop\$113.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_13.json)，以添加与下一组访问模式相关的数据。要解决访问模式 14（`getProductInventoryByWarehouseId`），请使用 `GSI2-PK=warehouseId` 和 `GSI2-SK begins_with “p#”` 查询 `GSI2`。

**GSI2：**

![\[此 GSI2 设计以 warehouseId 和 productId 作为分区键和排序键，来解决访问模式 14。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-14-Step11-GSI2.png)


要解决访问模式 15（`getInvoiceByCustomerIdForDateRange`），请使用 `GSI2-PK=customerId` 和 `GSI2-SK between (i#date1, i#date2)` 查询 `GSI2`。

**GSI2：**

![\[此 GSI2 设计以 customerId 和发票日期范围作为分区键和排序键，来解决访问模式 15。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-15-Step11-GSI2.png)


要解决访问模式 16（`getProductsByCustomerIdForDateRange`），请使用 `GSI2-PK=customerId` 和 `GSI2-SK between (p#date1, p#date2)` 查询 `GSI2`。

**GSI2：**

![\[此 GSI2 设计以 customerId 和发票日期范围作为分区键和排序键，来解决访问模式 16。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-16-Step11-GSI2.png)


**注意**  
在 [NoSQL Workbench](workbench.md) 中，*分面* 表示应用程序对 DynamoDB 的不同数据访问模式。分面为您提供了一种查看表中数据子集的方法，而不必查看不符合分面约束的记录。分面是一种可视化数据建模工具，在 DynamoDB 中不作为可用的构造存在，因为它们纯粹是对访问模式建模的帮助。  
导入 [AnOnlineShop\$1facets.json](https://github.com/aws-samples/amazon-dynamodb-design-patterns/blob/master/examples/an-online-shop/json/AnOnlineShop_facets.json)，以查看此使用案例的各个分面。

下表总结了所有访问模式以及架构设计如何解决访问模式：


| 访问模式 | 基表/GSI/LSI | 操作 | 分区键值 | 排序键值 | 
| --- | --- | --- | --- | --- | 
| getCustomerByCustomerId | 基表 | GetItem |  PK=customerId | SK=customerId | 
| getProductByProductId | 基表 | GetItem |  PK=productId | SK=productId | 
| getWarehouseByWarehouseId | 基表 | GetItem |  PK=warehouseId | SK=warehouseId | 
| getProductInventoryByProductId | 基表 | Query |  PK=productId | SK begins\$1with "w\$1" | 
| getOrderDetailsByOrderId | 基表 | Query |  PK=orderId |  | 
| getProductByOrderId | 基表 | Query |  PK=orderId | SK begins\$1with "p\$1" | 
| getInvoiceByOrderId |  基表 | Query |  PK=orderId | SK begins\$1with "i\$1" | 
| getShipmentByOrderId |  基表 | Query |  PK=orderId | SK begins\$1with "sh\$1" | 
| getOrderByProductIdForDateRange |  GSI1 | 查询 |  PK=productId | date1 和 date2 之间的 SK | 
| getInvoiceByInvoiceId |  GSI1 | 查询 |  PK=invoiceId | SK=invoiceId | 
| getPaymentByInvoiceId |  GSI1 | 查询 |  PK=invoiceId | SK=invoiceId | 
| getShipmentDetailsByShipmentId |  GSI1 | 查询 |  PK=shipmentId | SK=shipmentId | 
| getShipmentByWarehouseId |  GSI2 | 查询 |  PK=warehouseId | SK begins\$1with "sh\$1" | 
| getProductInventoryByWarehouseId |  GSI2 | 查询 |  PK=warehouseId | SK begins\$1with "p\$1" | 
| getInvoiceByCustomerIdForDateRange |  GSI2 | 查询 |  PK=customerId | i\$1date1 和 i\$1date2 之间的 SK | 
| getProductsByCustomerIdForDateRange |  GSI2 | 查询 |  PK=customerId | p\$1date1 和 p\$1date2 之间的 SK | 

### 在线商店最终架构
<a name="data-modeling-schema-online-store-final-schema"></a>

这是最终的架构设计。要以 JSON 文件格式下载此架构设计，请参阅 GitHub 上的 [DynamoDB 设计模式](https://github.com/aws-samples/aws-dynamodb-examples/tree/master/schema_design/SchemaExamples)。

**基表**：

![\[带有属性（例如 EntityName 和 Name）的在线商店基表的最终架构。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-17-Final-BaseTable.png)


**GSI1**

![\[带有属性（例如 EntityType）的在线商店基表的最终 GSI1 架构。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-18-Final-GSI1.png)


**GSI2**

![\[带有属性（例如 EntityType）的在线商店基表的最终 GSI2 架构。\]](http://docs.aws.amazon.com/zh_cn/amazondynamodb/latest/developerguide/images/DataModeling/OnlineShop-19-Final-GSI2.png)


## 在此架构设计中使用 NoSQL Workbench
<a name="data-modeling-schema-online-shop-nosql"></a>

若要进一步探索和编辑新项目，您可以将此最终架构导入到 [NoSQL Workbench](workbench.md)，这是一款为 DynamoDB 提供数据建模、数据可视化和查询开发功能的可视化工具。请按照以下步骤开始使用：

1. 下载 NoSQL Workbench。有关更多信息，请参阅 [下载 NoSQL Workbench for DynamoDB](workbench.settingup.md)。

1. 下载上面列出的 JSON 架构文件，该文件已经采用 NoSQL Workbench 模型格式。

1. 将 JSON 架构文件导入到 NoSQL Workbench。有关更多信息，请参阅 [导入现有数据模型](workbench.Modeler.ImportExisting.md)。

1. 导入到 NOSQL Workbench 后，您便可编辑数据模型。有关更多信息，请参阅 [编辑现有数据模型](workbench.Modeler.Edit.md)。