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

模块 6:添加互动,关注用户

使用 DynamoDB 事务通过两种方法处理复杂操作

概述

到目前为止,我们已经实现了在移动应用程序中创建和检索核心实体(如 User 和 Photo)的访问模式。我们还学习了如何使用反向索引对实体实现其他查询模式。

在本模块中,我们将实现两种访问模式:

  • 添加照片互动(
  • 关注好友(

请注意,这两种访问模式都是向 DynamoDB 写入数据,与我们目前为止所做的重读取模式不同。

 时长

20 分钟

DynamoDB 事务

为了实现以下步骤中的两种访问模式,我们将使用 DynamoDB ACID 事务。DynamoDB 事务于 2018 年 11 月发布。我们来快速了解一下 DynamoDB 中事务的运作方式。

事务在关系型数据库系统中广泛用于同时处理多个数据元素的操作。例如,假设您在经营一家银行。一位名叫 Karen 的顾客向另一位名叫 Sujith 的顾客转账 100 美元。在记录这笔交易时,您需要使用事务确保两位顾客的余额都得到相应的调整,而不只是其中一位。

在 DynamoDB 中添加事务后,可以更轻松地构建应用程序,以便在单个操作中更改多个数据项。通过 DynamoDB 事务,您可以通过一个事务请求完成对最多 10 个数据项的操作。

在调用 TransactWriteItem API 时,您可以执行以下类型的操作。

  • Put:用于插入或覆盖数据项;
  • Update:用于更新现有数据项;
  • Delete:用于删除数据项;
  • ConditionCheck:用于对现有数据项进行条件判断,而不对数据项进行任何更改。

在下面的步骤中,我们将使用 DynamoDB 事务,通过两种方法处理复杂操作。

操作步骤

  • 我们将在本模块中实现的第一种访问模式是添加照片互动。

    在添加用户对照片的互动时,我们需要完成以下操作:

    • 确认用户未在这张照片上使用过此互动类型
    • 创建一个新的 Reaction 实体来存储互动数据
    • 在 Photo 实体的互动属性中增加适当的互动类型,以便可以在照片上显示互动详情

    请注意,这需要在两个不同的数据项(现有 Photo 实体和新的 Reaction 实体)之间执行写入操作,并为其中一个数据项设置条件逻辑。这种操作非常适合用 DynamoDB 事务来完成。

    在您下载的代码中,application/ 目录下有一个名为 add_reaction.py 的脚本,其中包含一个添加照片互动的函数。该文件中的这个函数使用 DynamoDB 事务添加互动。

    此文件的内容如下所示:

    import datetime
    
    import boto3
    
    dynamodb = boto3.client('dynamodb')
    
    REACTING_USER = 'kennedyheather'
    REACTION_TYPE = 'sunglasses'
    PHOTO_USER = 'ppierce'
    PHOTO_TIMESTAMP = '2019-04-14T08:09:34'
    
    
    def add_reaction_to_photo(reacting_user, reaction_type, photo_user, photo_timestamp):
        reaction = "REACTION#{}#{}".format(reacting_user, reaction_type)
        photo = "PHOTO#{}#{}".format(photo_user, photo_timestamp)
        user = "USER#{}".format(photo_user)
        try:
            resp = dynamodb.transact_write_items(
                TransactItems=[
                    {
                        "Put": {
                            "TableName": "quick-photos",
                            "Item": {
                                "PK": {"S": reaction},
                                "SK": {"S": photo},
                                "reactingUser": {"S": reacting_user},
                                "reactionType": {"S": reaction_type},
                                "photo": {"S": photo},
                                "timestamp": {"S": datetime.datetime.now().isoformat() }
                            },
                            "ConditionExpression": "attribute_not_exists(SK)",
                            "ReturnValuesOnConditionCheckFailure": "ALL_OLD"
                        },
                    },
                    {
                        "Update": {
                            "TableName": "quick-photos",
                            "Key": {"PK": {"S": user}, "SK": {"S": photo}},
                            "UpdateExpression": "SET reactions.#t = reactions.#t + :i",
                            "ExpressionAttributeNames": {
                                "#t": reaction_type
                            },
                            "ExpressionAttributeValues": {
                                ":i": { "N": "1" },
                            },
                            "ReturnValuesOnConditionCheckFailure": "ALL_OLD"
                        }
                    }
                ]
            )
            print("Added {} reaction from {}".format(reaction_type, reacting_user))
            return True
        except Exception as e:
            print("Could not add reaction to photo")
    
    add_reaction_to_photo(REACTING_USER, REACTION_TYPE, PHOTO_USER, PHOTO_TIMESTAMP)

    add_reaction_to_photo 函数中,我们使用 transact_write_items() 方法执行写入事务。我们的事务包含两项操作。

    首先,执行 Put 操作,插入新的 Reaction 实体。在进行该操作时,我们设定了一个条件,即该数据项不应存在 SK 属性。这样可以确保不存在同时具有 PKSK 的数据项。如果存在,那就意味着用户已对这张照片添加了此互动。

    第二项操作是对 User 实体执行 Update 操作,增加互动属性映射中的互动类型。DynamoDB 强大的更新表达式支持执行原子增量,而无需先检索数据项再更新。

    在终端使用以下命令运行该脚本。

    python application/add_reaction.py
    

    终端输出应显示已为照片添加互动。

    Added sunglasses reaction from kennedyheather
    

    请注意,如果您尝试再次运行该脚本,函数将失败。用户 kennedyheather 已在这张照片上添加了此互动。因此,如果再次执行此操作就违反创建 Reaction 实体操作中的条件表达式。换句话说,该函数是幂等的,使用相同的输入重复调用,它不会产生不同的结果。

    DynamoDB 事务的引入极大简化了此类复杂操作的处理流程。以前,需要调用多个带有复杂条件的 API,并在发生冲突时需要手动回滚。现在只需不到 50 行代码就能实现。

    在下一步中,我们将了解如何处理“关注用户”访问模式。

  • 在您的应用程序中,一个用户可以关注另一用户。当应用程序后端收到关注用户的请求时,我们需要执行以下四项操作:

    • 检查关注者用户是否尚未关注请求的用户;
    • 创建一个 Friendship 实体来记录关注关系;
    • 增加被关注用户的关注者数;
    • 增加关注者用户的关注数。

    在您下载的代码中,application/ 目录下有一个名为 follow_user.py 的文件。此文件的内容如下所示:

    import datetime
    
    import boto3
    
    dynamodb = boto3.client('dynamodb')
    
    FOLLOWED_USER = 'tmartinez'
    FOLLOWING_USER = 'john42'
    
    
    def follow_user(followed_user, following_user):
        user = "USER#{}".format(followed_user)
        friend = "#FRIEND#{}".format(following_user)
        user_metadata = "#METADATA#{}".format(followed_user)
        friend_user = "USER#{}".format(following_user)
        friend_metadata = "#METADATA#{}".format(following_user)
        try:
            resp = dynamodb.transact_write_items(
                TransactItems=[
                    {
                        "Put": {
                            "TableName": "quick-photos",
                            "Item": {
                                "PK": {"S": user},
                                "SK": {"S": friend},
                                "followedUser": {"S": followed_user},
                                "followingUser": {"S": following_user},
                                "timestamp": {"S": datetime.datetime.now().isoformat()},
                            },
                            "ConditionExpression": "attribute_not_exists(SK)",
                            "ReturnValuesOnConditionCheckFailure": "ALL_OLD",
                        }
                    },
                    {
                        "Update": {
                            "TableName": "quick-photos",
                            "Key": {"PK": {"S": user}, "SK": {"S": user_metadata}},
                            "UpdateExpression": "SET followers = followers + :i",
                            "ExpressionAttributeValues": {":i": {"N": "1"}},
                            "ReturnValuesOnConditionCheckFailure": "ALL_OLD",
                        }
                    },
                    {
                        "Update": {
                            "TableName": "quick-photos",
                            "Key": {"PK": {"S": friend_user}, "SK": {"S": friend_metadata}},
                            "UpdateExpression": "SET following = following + :i",
                            "ExpressionAttributeValues": {":i": {"N": "1"}},
                            "ReturnValuesOnConditionCheckFailure": "ALL_OLD",
                        }
                    },
                ]
            )
            print("User {} is now following user {}".format(following_user, followed_user))
            return True
        except Exception as e:
            print(e)
            print("Could not add follow relationship")
    
    follow_user(FOLLOWED_USER, FOLLOWING_USER)

    文件中的 follow_user 函数与应用程序中需要实现的函数类似。该函数使用两个用户名(被关注用户的用户名和关注者用户的用户名),然后运行请求来创建一个 Friendship 实体,并更新这两个 User 实体。

    在终端使用以下命令运行该脚本。

    python application/follow_user.py
    

    您应当会在终端看到表示操作成功的输出。

    User john42 is now following user tmartinez
    

    尝试在终端中再次运行脚本。这时,您应该会收到一条错误消息,表示您无法添加关注关系。这是因为该用户现在关注了请求的用户,因此请求未通过对该数据项的条件检查。

总结

在本模块中,我们了解了如何在应用程序中实现两种高级写操作。首先,我们使用 DynamoDB 事务实现用户对照片添加互动。通过事务,我们在单个请求中完成了对多个数据项的复杂条件写操作。此外,我们还了解了如何使用 DynamoDB 更新表达式,增加映射属性中的嵌套属性。

其次,我们实现了一个用户关注另一个用户的功能。这需要在单个请求中更改三个数据项,同时还要对其中一个数据项执行条件检查。虽然这项操作通常很复杂,但 DynamoDB 通过 DynamoDB 事务让这项操作变得简单。

在下一个模块中,我们将清理创建的资源,并了解 DynamoDB 学习路径中的一些后续步骤。

清理资源和后续步骤