使用 Amazon DynamoDB 为移动应用程序设计数据库

模块 3:核心用途:用户、照片、好友和互动

设计 DynamoDB 表的主键并实现核心访问模式

概述

在上一模块中,我们定义了移动应用程序的所有访问模式。在本模块中,我们将设计 DynamoDB 表的主键并实现核心访问模式。

设计 DynamoDB 表的主键时,请遵循以下最佳实践:

  • 设计表中的不同实体。如果您要在单个表中存储多种不同类型的数据,例如员工、部门、客户和订单,请确保您设置的主键能明确标识每个实体,并能对某个单数据项进行核心操作。
  • 使用前缀区分实体类型。使用前缀区分实体类型可以防止冲突,并有助于查询。如果同一个表中有客户和员工信息,那么客户的主键可以是 CUSTOMER#<CUSTOMERID>,而员工的主键可以是 EMPLOYEE#<EMPLOYEEID>
  • 对单数据项的操作是重点,并尽可能支持对多数据项的操作。设置主键时,需要保证您可以调用以下针对单数据项操作的 API 来实现对单数据项的读写操作:GetItemPutItemUpdateItemDeleteItem。您也可以调用 Query 接口,根据主键实现多数据项读取模式。如果不能使用 Query 操作,可以随时添加二级索引,通过二级索引来实现 Query 操作。

了解了这些最佳实践后,下面我们来设计主键并执行一些基本操作。

 时长

40 分钟

操作步骤

  • 我们需要按照前面做的规划来设计不同的实体。在移动应用程序中,我们有以下实体:

    • User
    • Photo
    • Reaction
    • Friendship

    这些实体体现了三种不同的数据关系。

    首先,应用程序上的每个用户都有个人资料,由表中的 User 实体表示。

    在应用程序中,用户会拥有多张照片,而一张照片上会有多个互动。这些都是一对多的关系。

    最后,Friendship 实体表示多对多的关系。Friendship 实体表示在应用程序中,一个用户关注另一用户的情况。这是一种多对多的关系,因为一个用户可能关注多个其他用户,而一个用户也可能有多个关注者。

    多对多映射通常表示需要实现两种 Query 模式,我们的应用程序也不例外。在 Friendship 实体中,有一种访问模式需要查找关注某个特定用户的所有用户,还有一种访问模式需要查找某个特定用户关注的所有用户。

    因此,我们将使用一个包含 HASHRANGE 值的复合主键。复合主键为我们提供了基于 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。这样就可以执行单数据项操作,如 GetItemPutItemDeleteItem

    但是,如果您将此列用作索引的 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 个示例数据项。这些数据项包括 UserPhotoFriendshipReaction 实体。打开该文件便可查看部分示例数据。

    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 表上实现更多访问模式。

查看照片互动和用户的关注者