模块 3:核心用途:用户、照片、好友和互动
设计 DynamoDB 表的主键并实现核心访问模式
概述
在上一模块中,我们定义了移动应用程序的所有访问模式。在本模块中,我们将设计 DynamoDB 表的主键并实现核心访问模式。
设计 DynamoDB 表的主键时,请遵循以下最佳实践:
- 设计表中的不同实体。如果您要在单个表中存储多种不同类型的数据,例如员工、部门、客户和订单,请确保您设置的主键能明确标识每个实体,并能对某个单数据项进行核心操作。
- 使用前缀区分实体类型。使用前缀区分实体类型可以防止冲突,并有助于查询。如果同一个表中有客户和员工信息,那么客户的主键可以是 CUSTOMER#<CUSTOMERID>,而员工的主键可以是 EMPLOYEE#<EMPLOYEEID>。
- 对单数据项的操作是重点,并尽可能支持对多数据项的操作。设置主键时,需要保证您可以调用以下针对单数据项操作的 API 来实现对单数据项的读写操作:GetItem、PutItem、UpdateItem 和 DeleteItem。您也可以调用 Query 接口,根据主键实现多数据项读取模式。如果不能使用 Query 操作,可以随时添加二级索引,通过二级索引来实现 Query 操作。
了解了这些最佳实践后,下面我们来设计主键并执行一些基本操作。
时长
40 分钟
操作步骤
-
设计主键
我们需要按照前面做的规划来设计不同的实体。在移动应用程序中,我们有以下实体:
- User
- Photo
- Reaction
- Friendship
这些实体体现了三种不同的数据关系。
首先,应用程序上的每个用户都有个人资料,由表中的 User 实体表示。
在应用程序中,用户会拥有多张照片,而一张照片上会有多个互动。这些都是一对多的关系。
最后,Friendship 实体表示多对多的关系。Friendship 实体表示在应用程序中,一个用户关注另一用户的情况。这是一种多对多的关系,因为一个用户可能关注多个其他用户,而一个用户也可能有多个关注者。
多对多映射通常表示需要实现两种 Query 模式,我们的应用程序也不例外。在 Friendship 实体中,有一种访问模式需要查找关注某个特定用户的所有用户,还有一种访问模式需要查找某个特定用户关注的所有用户。
因此,我们将使用一个包含 HASH 和 RANGE 值的复合主键。复合主键为我们提供了基于 HASH 键的 Query 功能,实现我们所需的一种查询模式。在 DynamoDB API 规范中,分区键称为 HASH,排序键称为 RANGE。在本指南中,我们交替使用 API 术语,尤其是在讨论代码或 DynamoDB JSON Wire 格式时。
请注意,一对一实体 User 不具有 RANGE 值的自然属性。该实体是一对一映射,因此访问模式是基本的键值查找。由于表设计需要 RANGE 属性,因此可以为 RANGE 键提供填充值。
了解这些信息后,下面我们为每种实体类型的 HASH 和 RANGE 值设置以下模式:
实体 HASH RANGE User USER#<USERNAME>
#METADATA#<USERNAME>
Photo USER#<USERNAME>
PHOTO#<USERNAME>#<TIMESTAMP>
Reaction REACTION#<USERNAME>#<TYPE>
PHOTO#<USERNAME>#<TIMESTAMP>
Friendship USER#<USERNAME>
#FRIEND#<FRIEND_USERNAME>
我们来看一下上表。
首先,对于 User 实体,HASH 值是 USER#<USERNAME>。请注意,我们使用了一个前缀来标识实体,防止实体类型之间可能出现的任何冲突。
对于 User 实体的 RANGE 值,我们使用了静态前缀 #METADATA# 加 username 值。对于 RANGE 值,必须要有一个已知的值,比如 username。这样就可以执行单数据项操作,如 GetItem、PutItem 和 DeleteItem。
但是,如果您将此列用作索引的 HASH 键,则还需要 RANGE 在不同的 User 实体上具有不同的值,以实现均衡分区。因此,您需要将 username 附加到 RANGE 键。
其次,Photo 实体是特定 User 实体的子实体。关于照片的主要访问模式是检索某个用户下按日期排序的照片。如果您需要按特定属性排序,则需要在 RANGE 键中包含该属性,以便进行排序。对于 Photo 实体,使用与 User 实体相同的 HASH 键,这样可以通过单个请求同时检索用户个人资料和用户照片。对于 RANGE 键,使用 PHOTO#<USERNAME>#<TIMESTAMP> 来唯一标识表中的照片。
第三,Reaction 实体是特定 Photo 实体的子实体。与 Photo 实体存在一对多关系,因此使用与 Photo 实体类似的推理。在下一个模块中,您将学习如何使用二级索引在单个查询中检索照片及其所有互动。请注意,Reaction 实体的 RANGE 键与 Photo 实体的 RANGE 键模式相同。HASH 键由互动用户的用户名以及应用的互动类型组成。添加互动类型,让用户可以在一张照片上添加多种类型互动。
最后,Friendship 实体使用与 User 实体相同的 HASH 键。这样,可以通过单个查询获取用户的元数据和该用户的所有关注者。Friendship 实体的 RANGE 键是 #FRIEND#<FRIEND_USERNAME>。在下面的步骤 4 中,您将了解为什么要在 Friendship 实体的 RANGE 键前面加上“#”。
下一步,我们将使用这种主键设计创建一个表。
-
创建表
现在我们已经设计好了主键,下面我们来创建一个表。
在模块 1 的步骤 3 下载的代码中,包含一个 Python 脚本,位于 scripts/ 目录中,名为 create_table.py。Python 脚本的内容如下:
import boto3 dynamodb = boto3.client('dynamodb') try: dynamodb.create_table( TableName='quick-photos', AttributeDefinitions=[ { "AttributeName": "PK", "AttributeType": "S" }, { "AttributeName": "SK", "AttributeType": "S" } ], KeySchema=[ { "AttributeName": "PK", "KeyType": "HASH" }, { "AttributeName": "SK", "KeyType": "RANGE" } ], ProvisionedThroughput={ "ReadCapacityUnits": 5, "WriteCapacityUnits": 5 } ) print("Table created successfully.") except Exception as e: print("Could not create table. Error:") print(e)
以上脚本通过 Boto 3 调用 CreateTable 操作。Boto3 是适用于 Python 的 AWS SDK。该操作声明了两个属性定义,它们是组成主键的类型化属性。虽然 DynamoDB 是无模式的,但您必须声明组成主键的属性名称和类型。属性必须包含在写入表的每个数据项中,因此必须在创建表时指定。
由于您要在一个表中存储不同的实体,因此主键不能使用 UserId 这样的属性名称。根据所存储实体的类型,属性的含义会有所不同。例如,用户的主键可能是 USERNAME,而互动的主键可能是 TYPE。因此,我们为属性设置通用名称:PK(表示分区键)和 SK(表示排序键)。
在键结构中配置属性后,我们需要为表指定预配吞吐量。DynamoDB 有两种容量配置模式:预配和按需。采用预配容量模式时,您需要声明想预配的读写吞吐量。无论使用与否,您都要为预配的容量付费。
在 DynamoDB 按需容量模式下,您可以按请求付费。单个请求的成本略高于完全使用预配吞吐量的单个请求成本,但您不必花时间进行容量规划,也不必担心被节流。按需模式非常适合突发性或不可预测的工作负载。我们在本实验中使用预配容量模式,因为 DynamoDB 免费套餐额度可用于预配容量。
使用以下命令运行 Python 脚本创建表。
python scripts/create_table.py
创建成功,则返回:“Table created successfully.”(表创建成功。)
下一步,我们将向表中批量加载一些示例数据。
-
向表中批量加载数据
在这一步中,我们会将一些数据批量加载到在上一步创建的 DynamoDB 表中。在后续步骤中,我们将使用这些示例数据。
在 scripts/ 目录中,有一个名为 items.json 的文件。该文件包含为实验项目随机生成的 967 个示例数据项。这些数据项包括 User、Photo、Friendship 和 Reaction 实体。打开该文件便可查看部分示例数据。
scripts/ 目录中还有一个名为 bulk_load_table.py 的文件,用于读取 items.json 文件中的数据项并将它们批量写入 DynamoDB 表中。该文件的内容如下所示:
import json import boto3 dynamodb = boto3.resource('dynamodb') table = dynamodb.Table('quick-photos') items = [] with open('scripts/items.json', 'r') as f: for row in f: items.append(json.loads(row)) with table.batch_writer() as batch: for item in items: batch.put_item(Item=item)
在这个脚本中,我们没有在 Boto 3 中使用低端客户端,而是使用了更高级的资源对象。资源对象为使用 AWS API 提供了一个更简单的接口。资源对象在这种情况下非常有用,因为它可以批量处理我们的请求。可通过一个 BatchWriteItem API 请求,最多写入 25 个数据项。资源对象会帮助我们处理批量请求,我们无需将数据分成 25 个或更少的数据项。
在终端执行以下命令,运行 bulk_load_table.py 脚本将数据载入表中。
python scripts/bulk_load_table.py
为确认是否所有数据都已加载成功,可以执行 Scan 操作,获取返回数据项计数。
在 AWS CLI 上运行以下命令,获取数据项计数:
aws dynamodb scan \ --table-name quick-photos \ --select COUNT
返回结果应如下所示。
{ "Count": 967, "ScannedCount": 967, "ConsumedCapacity": null }
返回结果中,Count 值为 967,表示所有数据项都已成功加载。
下一步,我们将展示如何通过单个请求检索多个实体类型。这种检索方法可以减少应用程序中的网络请求总数,并提高应用程序性能。
-
通过单个请求检索多个实体类型
正如我们在上一个模块中所说的,您应该根据 DynamoDB 表收到的请求数量对其进行优化。我们还介绍了,DynamoDB 不支持关系型数据库的连接功能。因此,在设计表时应考虑到请求中所需的类似 join 的行为。
在这一步中,我们将展示如何通过单个请求检索多个实体类型。在应用程序中,我们可能会想要获取某个用户的信息。这包括 User 实体上用户个人资料中的所有信息以及用户上传的所有照片。
该请求跨越两个实体类型:User 实体和 Photo 实体。不过,这并不意味着需要发起多个请求。
在您下载的代码中,application/ 目录下有一个 fetch_user_and_photos.py 文件。此脚本展示了如何构建通过一次请求同时检索 User 实体和对应用户上传的 Photo 实体的代码。
fetch_user_and_photos.py 脚本代码如下:
import boto3 from entities import User, Photo dynamodb = boto3.client('dynamodb') USER = "jacksonjason" def fetch_user_and_photos(username): resp = dynamodb.query( TableName='quick-photos', KeyConditionExpression="PK = :pk AND SK BETWEEN :metadata AND :photos", ExpressionAttributeValues={ ":pk": { "S": "USER#{}".format(username) }, ":metadata": { "S": "#METADATA#{}".format(username) }, ":photos": { "S": "PHOTO$" }, }, ScanIndexForward=True ) user = User(resp['Items'][0]) user.photos = [Photo(item) for item in resp['Items'][1:]] return user user = fetch_user_and_photos(USER) print(user) for photo in user.photos: print(photo)
在开头部分,我们导入 Boto 3 库和一些简单的类,表示应用程序代码中的对象。如果感兴趣,您可以在 application/entities.py 文件中查看这些实体的定义。
脚本的主要能力是由 fetch_user_and_photos 函数定义模块。该函数类似于在应用程序中定义的函数,供任何需要这些数据的端点使用。
使用此函数时,首先向 DynamoDB 发出一个 Query 请求。该 Query 指定 USER#<Username> 作为 HASH 键,指定只返回特定用户相关的数据项。
然后,该 Query 指定一个介于 #METADATA#<Username> 和 PHOTO$ 之间的 RANGE 键条件表达式。此 Query 将返回一个 User 实体(其排序键为 #METADATA#)以及该用户的所有 Photo 实体(其排序键以 PHOTO# 开头)。字符串类型的排序键按 ASCII 字符编码排序。在 ASCII 字符串中,美元符号 ($) 直接位于磅号 (#) 之后,这样可确保我们获得所有 Photo 实体。
收到响应后,我们会将数据项组合成应用程序能识别的对象。我们知道返回的第一项是 User 实体,因此我们根据该项创建一个 User 对象。我们为其余每项创建一个 Photo 对象,然后将用户数组附加到 User 对象。
脚本的结尾部分显示了函数的执行情况,并打印出结果对象。在终端执行以下命令运行该脚本。
python application/fetch_user_and_photos.py
该脚本执行成功后,会将 User 对象和所有 Photo 对象输出到控制台:
User<jacksonjason -- John Perry> Photo<jacksonjason -- 2018-05-30T15:42:38> Photo<jacksonjason -- 2018-06-09T13:49:13> Photo<jacksonjason -- 2018-06-26T03:59:33> Photo<jacksonjason -- 2018-07-14T10:21:01> Photo<jacksonjason -- 2018-10-06T22:29:39> Photo<jacksonjason -- 2018-11-13T08:23:00> Photo<jacksonjason -- 2018-11-18T15:37:05> Photo<jacksonjason -- 2018-11-26T22:27:44> Photo<jacksonjason -- 2019-01-02T05:09:04> Photo<jacksonjason -- 2019-01-23T12:43:33> Photo<jacksonjason -- 2019-03-03T02:00:01> Photo<jacksonjason -- 2019-03-03T18:20:10> Photo<jacksonjason -- 2019-03-11T15:18:22> Photo<jacksonjason -- 2019-03-30T02:28:42> Photo<jacksonjason -- 2019-04-14T21:52:36>
此脚本展示了如何为表建模并编写查询,通过单个 DynamoDB 请求检索多个实体类型。在关系型数据库中,您可以使用连接通过单个请求从不同表中检索多个实体类型。在 DynamoDB 中,您可以对数据进行特定建模,使您要同时访问的实体存在表中相邻的位置。这种方法取代了典型关系型数据库中的连接需要,并能在扩展时保持应用程序的高性能。
总结
在本模块中,我们设计了一个主键并创建了一个表。然后,我们将数据批量加载到表中。我们试验了如何通过单个请求查询多个实体类型。
通过当前的主键设计,我们可以实现以下访问模式:
- 创建用户个人资料(写)
- 更新用户个人资料(写)
- 获取用户个人资料(读)
- 上传照片(写)
- 查看用户的照片(读)
- 查看用户的好友(读)
在下一个模块中,我们将学习添加二级索引,并学习反向索引技术。通过二级索引,您可以在 DynamoDB 表上实现更多访问模式。