跳转到内容

py#dynamodb

此生成器创建一个由 Amazon DynamoDB 支持的新 Python 项目,使用 PynamoDB 进行实体建模。它生成应用程序代码和基础设施,用于使用 AWS CDK 或 Terraform 配置和管理 DynamoDB 表,支持单表设计并通过 DynamoDB Local 内置本地开发功能。

  1. 安装 Nx Console VSCode Plugin 如果您尚未安装
  2. 在VSCode中打开Nx控制台
  3. 点击 Generate (UI) 在"Common Nx Commands"部分
  4. 搜索 @aws/nx-plugin - py#dynamodb
  5. 填写必需参数
    • 点击 Generate
    参数 类型 默认值 描述
    name 必需 string - 要生成的 DynamoDB 项目名称
    directory string packages 存储项目的目录。
    subDirectory string - 项目放置的子目录。默认情况下为项目名称。
    framework pynamodb pynamodb 用于 DynamoDB 实体的框架。
    tableName string - DynamoDB 表名称。如果未指定则自动生成。
    infra dynamodb | none dynamodb 为 DynamoDB 表配置的基础设施。
    iac inherit | cdk | terraform inherit 首选的 IaC 提供商。默认情况下继承自您的初始选择。
    preferInstallDependencies boolean true 是否在生成器运行后优先安装依赖项。设置为 false 可在批量运行多个生成器时延迟安装(如果后续生成器需要计算 Nx 项目图,仍会运行安装);在最后统一安装一次。

    生成器在 <directory>/<name> 目录中创建以下项目结构:

    • 文件夹<name>
      • __init__.py 包导出
      • client.py DynamoDB 客户端和表名解析
      • 文件夹entities
        • base.py 带有 GSI 声明的基础 PynamoDB 模型
        • example.py 示例实体定义
        • __init__.py 实体导出
    • config.json 表配置,包括 GSI 定义和本地开发设置
    • project.json 项目配置和构建目标

    本地开发脚本在所有 DynamoDB 项目(TypeScript 和 Python)之间共享,并一次性生成到:

    • 文件夹packages/common/scripts/src/dynamodb
      • create-local-table.ts 在本地 DynamoDB Local 实例中创建 DynamoDB 表
      • pull-image.ts 拉取 DynamoDB Local 镜像
      • start-container.ts 启动 DynamoDB Local 容器

    由于该生成器会根据您选择的 iacProvider 以基础设施即代码的形式输出,它将在 packages/common 目录下创建一个包含相关 CDK 构造体或 Terraform 模块的项目。

    通用的基础设施即代码项目结构如下:

    • 文件夹packages/common/constructs
      • 文件夹src
        • 文件夹app/ 针对特定项目/生成器的基础设施构造体
        • 文件夹core/ app 目录构造体重用的通用构造体
        • index.ts 导出 app 目录构造体的入口文件
      • project.json 项目构建目标与配置
    • 文件夹packages/common/constructs/src
      • 文件夹app
        • 文件夹dynamodb
          • <name>.ts 特定于您的表的基础设施
      • 文件夹core
        • dynamodb.ts 通用 DynamoDB 表构造

    生成器配置了一个 dev 目标,用于启动 DynamoDB Local 实例并创建表。使用项目的 dev 目标:

    Terminal window
    pnpm nx dev <project-name>

    这将自动:

    1. 拉取 DynamoDB Local 镜像(pull-image 目标)
    2. 启动容器
    3. 使用 config.json 中定义的索引创建本地表

    生成的项目使用 PynamoDB 进行实体建模。所有实体必须继承自生成的 BaseModel — 它在运行时解析正确的 DynamoDB 表名,在部署时从 AWS AppConfig 读取,或在通过 DynamoDB Local 本地运行时从 config.json 读取。如果没有这个,PynamoDB 将不知道要使用哪个表。BaseModel 还使用 PynamoDB 的多态支持 在单个表中存储多种实体类型,遵循 DynamoDB 的单表设计

    <name>/entities/ 下添加或更新实体文件,使用生成的示例实体作为起点:

    packages/my_table/my_table/entities/example.py
    from collections.abc import Iterator
    from datetime import UTC, datetime
    from pynamodb.attributes import UnicodeAttribute
    from .base import BaseModel
    class ExampleModel(BaseModel, discriminator='ExampleModel'):
    """
    Key design:
    pk=EXAMPLE#<id>, sk=EXAMPLE#<id>
    gsi1pk=CATEGORY#<cat>, gsi1sk=EXAMPLE#<id> <- list items by category
    gsi2pk=EXAMPLE, gsi2sk=<created_at> <- list all items by date
    """
    name = UnicodeAttribute()
    category = UnicodeAttribute()
    created_at = UnicodeAttribute()
    updated_at = UnicodeAttribute()
    @classmethod
    def make_pk(cls, id: str) -> str:
    return f'EXAMPLE#{id}'
    @classmethod
    def create(cls, id: str, name: str, category: str) -> 'ExampleModel':
    now = datetime.now(UTC).isoformat()
    item = cls(
    pk=cls.make_pk(id),
    sk=cls.make_pk(id),
    gsi1pk=f'CATEGORY#{category}',
    gsi1sk=cls.make_pk(id),
    gsi2pk='EXAMPLE',
    gsi2sk=now,
    name=name,
    category=category,
    created_at=now,
    updated_at=now,
    )
    item.save()
    return item
    # ── Primary index ─────────────────────────────────────────────────────────
    @classmethod
    def get_by_id(cls, id: str) -> 'ExampleModel':
    return cls.get(cls.make_pk(id), cls.make_pk(id))
    # ── gsi1_index: partition=category, sort=id ───────────────────────────────
    @classmethod
    def list_by_category(cls, category: str) -> Iterator['ExampleModel']:
    return cls.gsi1_index.query(f'CATEGORY#{category}')
    # ── gsi2_index: partition=type, sort=created_at ───────────────────────────
    @classmethod
    def list_created_between(cls, start: datetime, end: datetime) -> Iterator['ExampleModel']:
    return cls.gsi2_index.query(
    'EXAMPLE',
    range_key_condition=ExampleModel.gsi2sk.between(
    start.isoformat(), end.isoformat(),
    ),
    scan_index_forward=False,
    )

    有关更多详细信息,请参阅 PynamoDB 教程

    在 DynamoDB 中,模式设计从查询开始,而不是数据形状。在编写任何模型之前,列出应用程序需要的每个访问模式,然后设计 pksk 和 GSI 键值,使每个模式都由单个表请求回答 — 无需 JOIN,无需顺序读取。

    生成的 ExampleModel 演示了三种模式:

    • 按 ID 获取 — 主索引,pk=EXAMPLE#<id>sk=EXAMPLE#<id>
    • 按类别列出gsi1pk=CATEGORY#<category>
    • 按创建日期列出gsi2pk=EXAMPLE,排序键在 ISO 时间戳之间

    类型前缀约定(例如 EXAMPLE#CATEGORY#)是有意为之的:它使浏览表时项目具有自描述性,防止共享索引的实体类型之间意外的键冲突,并允许使用 begins_with 进行排序键前缀过滤。

    在编写新实体之前,在文档字符串中预先定义其键模式。下一节中的 OrderModel 遵循此约定:

    class OrderModel(BaseModel, discriminator='OrderModel'):
    """
    Key design:
    pk=ORDER#<order_id>, sk=ORDER#<order_id>
    gsi1pk=USER#<user_id>, gsi1sk=ORDER#<order_id> <- list orders for a user
    gsi2pk=ORDER, gsi2sk=<created_at> <- list all orders by date
    """

    PynamoDB 的 DiscriminatorAttribute 在每个项目中存储一个类型标签(entity_type)。通过 BaseModel 查询时,此标签用于自动将每个结果实例化为其正确的子类 — 因此单个查询可以返回 UserModelOrderModel 和在同一表中注册的任何其他实体类型的混合。

    下面是一个完整的双实体示例 — 一个 UserModel 及其关联的 OrderModel 记录存储在同一个表中:

    packages/my_table/my_table/entities/user.py
    from collections.abc import Iterator
    from datetime import UTC, datetime
    from pynamodb.attributes import UnicodeAttribute
    from .base import BaseModel
    class UserModel(BaseModel, discriminator='UserModel'):
    """
    Key design:
    pk=USER#<user_id>, sk=USER#<user_id>
    gsi2pk=USER, gsi2sk=<created_at> <- list all users by date
    """
    username = UnicodeAttribute()
    email = UnicodeAttribute()
    created_at = UnicodeAttribute()
    @classmethod
    def make_pk(cls, user_id: str) -> str:
    return f'USER#{user_id}'
    @classmethod
    def create(cls, user_id: str, username: str, email: str) -> 'UserModel':
    now = datetime.now(UTC).isoformat()
    item = cls(
    pk=cls.make_pk(user_id),
    sk=cls.make_pk(user_id),
    gsi2pk='USER',
    gsi2sk=now,
    username=username,
    email=email,
    created_at=now,
    )
    item.save()
    return item
    @classmethod
    def get_by_id(cls, user_id: str) -> 'UserModel':
    return cls.get(cls.make_pk(user_id), cls.make_pk(user_id))
    @classmethod
    def list_recent(cls, limit: int | None = None) -> Iterator['UserModel']:
    return cls.gsi2_index.query('USER', limit=limit, scan_index_forward=False)
    packages/my_table/my_table/entities/order.py
    from collections.abc import Iterator
    from datetime import UTC, datetime
    from pynamodb.attributes import UnicodeAttribute
    from .base import BaseModel
    class OrderModel(BaseModel, discriminator='OrderModel'):
    """
    Key design:
    pk=ORDER#<order_id>, sk=ORDER#<order_id>
    gsi1pk=USER#<user_id>, gsi1sk=ORDER#<order_id> <- list orders by user
    gsi2pk=ORDER, gsi2sk=<created_at> <- list all orders by date
    """
    user_id = UnicodeAttribute()
    total = UnicodeAttribute()
    created_at = UnicodeAttribute()
    @classmethod
    def make_pk(cls, order_id: str) -> str:
    return f'ORDER#{order_id}'
    @classmethod
    def create(cls, order_id: str, user_id: str, total: str) -> 'OrderModel':
    now = datetime.now(UTC).isoformat()
    item = cls(
    pk=cls.make_pk(order_id),
    sk=cls.make_pk(order_id),
    gsi1pk=f'USER#{user_id}',
    gsi1sk=cls.make_pk(order_id),
    gsi2pk='ORDER',
    gsi2sk=now,
    user_id=user_id,
    total=total,
    created_at=now,
    )
    item.save()
    return item
    @classmethod
    def get_by_id(cls, order_id: str) -> 'OrderModel':
    return cls.get(cls.make_pk(order_id), cls.make_pk(order_id))
    # ── gsi1_index: partition=user, sort=order_id ────────────────────────────
    @classmethod
    def list_by_user(cls, user_id: str) -> Iterator['OrderModel']:
    return cls.gsi1_index.query(f'USER#{user_id}')
    # ── gsi2_index: partition=type, sort=created_at ───────────────────────────
    @classmethod
    def list_recent(cls, limit: int | None = None) -> Iterator['OrderModel']:
    return cls.gsi2_index.query('ORDER', limit=limit, scan_index_forward=False)

    __init__.py 导出新实体:

    packages/my_table/my_table/entities/__init__.py
    from .user import UserModel
    from .order import OrderModel
    from .example import ExampleModel

    BaseModel 提供两个共享的 GSI(gsi1_indexgsi2_index)。上面的 UserModelOrderModel 都写入 gsi2 — 但使用不同的 gsi2pk 值(USER vs ORDER)。这就是 GSI 重载:重用单个物理索引来服务多个独立的访问模式,而不消耗额外的 GSI 容量。

    • UserModelgsi2pk=USERgsi2sk=<created_at> → 按日期列出所有用户
    • OrderModelgsi2pk=ORDERgsi2sk=<created_at> → 按日期列出所有订单

    当多个实体类型共享同一父级时,gsi1 也可以重载。如果您稍后添加一个也属于用户的 ReviewModel,您可以为其分配 gsi1pk=USER#<user_id>REVIEW#<id> 排序键 — 无需额外的 GSI。然后通过 BaseModel 查询 gsi1 会在一个请求中返回该用户的订单和评论,PynamoDB 将每个项目实例化为其正确的子类:

    from .base import BaseModel
    from .order import OrderModel
    from .review import ReviewModel
    user_id = 'user-123'
    items = list(BaseModel.gsi1_index.query(f'USER#{user_id}'))
    orders = [i for i in items if isinstance(i, OrderModel)]
    reviews = [i for i in items if isinstance(i, ReviewModel)]

    要从重载的 GSI 中仅检索一种实体类型,请使用排序键前缀条件:

    orders_only = list(BaseModel.gsi1_index.query(
    f'USER#{user_id}',
    range_key_condition=BaseModel.gsi1sk.startswith('ORDER#'),
    ))

    一对多关系中,子实体在 GSI 分区键中存储对其父级的引用,使关系可以在两个方向上遍历而无需复制数据。上面的 UserModel / OrderModel 示例正是这种模式:

    • 按 ID 获取单个订单 — 主表:pk=ORDER#<id>sk=ORDER#<id>
    • 列出用户的所有订单gsi1pk=USER#<user_id>

    GSI 查找的替代方案是项目集合模式:给子项目与其父级相同的 pk,并使用排序键来区分它们。这使您可以在单个主表查询中检索父级及其所有子级,无需 GSI:

    packages/my_table/my_table/entities/order.py (项目集合变体)
    class OrderModel(BaseModel, discriminator='OrderModel'):
    """
    Key design (item collection):
    pk=USER#<user_id>, sk=ORDER#<order_id> <- co-located under the parent user
    """
    ...
    # 在一个主表查询中检索用户及其所有订单
    # BaseModel 通过 DiscriminatorAttribute 将每个项目分派到其正确的子类
    items = list(BaseModel.query(f'USER#{user_id}'))
    user = next(i for i in items if isinstance(i, UserModel))
    orders = [i for i in items if isinstance(i, OrderModel)]

    权衡:项目集合将所有子项放在单个分区键下,这对大多数工作负载是最优的,但在极端写入吞吐量下可能会创建热分区。GSI 方法(在上面的示例中使用)将每个实体保留在自己的分区中,通常更安全地开始。

    多对多关系需要使用邻接列表模式连接实体:一个专用项目记录每个链接,其 GSI 键反转方向,以便关系可以双向遍历。

    考虑 ArticleModelTagModel,其中一篇文章可以有多个标签,一个标签可以应用于多篇文章:

    packages/my_table/my_table/entities/article_tag.py
    from collections.abc import Iterator
    from pynamodb.attributes import UnicodeAttribute
    from .base import BaseModel
    class ArticleTagModel(BaseModel, discriminator='ArticleTag'):
    """
    Junction entity for the Article ↔ Tag many-to-many relationship.
    Key design:
    pk=ARTICLE#<article_id>, sk=TAG#<tag_name> <- list tags for an article
    gsi1pk=TAG#<tag_name>, gsi1sk=ARTICLE#<article_id> <- list articles for a tag
    """
    article_id = UnicodeAttribute()
    tag_name = UnicodeAttribute()
    @classmethod
    def add(cls, article_id: str, tag_name: str) -> 'ArticleTagModel':
    item = cls(
    pk=f'ARTICLE#{article_id}',
    sk=f'TAG#{tag_name}',
    gsi1pk=f'TAG#{tag_name}',
    gsi1sk=f'ARTICLE#{article_id}',
    article_id=article_id,
    tag_name=tag_name,
    )
    item.save()
    return item
    @classmethod
    def remove(cls, article_id: str, tag_name: str) -> None:
    cls.get(f'ARTICLE#{article_id}', f'TAG#{tag_name}').delete()
    # ── Primary index: pk=article, sk=tag ─────────────────────────────────────
    @classmethod
    def list_tags_for_article(cls, article_id: str) -> Iterator['ArticleTagModel']:
    return cls.query(f'ARTICLE#{article_id}')
    # ── gsi1_index: pk=tag, sk=article ────────────────────────────────────────
    @classmethod
    def list_articles_for_tag(cls, tag_name: str) -> Iterator['ArticleTagModel']:
    return cls.gsi1_index.query(f'TAG#{tag_name}')

    因为 ArticleTagModel 使用 pk=ARTICLE#<article_id> — 与文章本身相同的分区 — 您可以在单个主表查询中检索文章及其所有标签:

    from .base import BaseModel
    from .article import ArticleModel
    from .article_tag import ArticleTagModel
    items = list(BaseModel.query('ARTICLE#article-123'))
    article = next(i for i in items if isinstance(i, ArticleModel))
    tags = [i.tag_name for i in items if isinstance(i, ArticleTagModel)]

    有关 DynamoDB 数据建模的进一步阅读,请参阅 DynamoDB 数据建模指南使用 Amazon DynamoDB 创建单表设计

    生成的 client.py 导出两个关键实用工具:

    • is_local() — 当 LOCAL_DEV=true 时返回 True,用于在本地和 AWS 行为之间切换。
    • get_table_name() — 返回 DynamoDB 表名。当 LOCAL_DEV=true 时,从 config.json 中的 localDev.tableName 读取表名;否则使用 RUNTIME_CONFIG_APP_ID 环境变量从 AWS AppConfig 获取名称,并缓存以供后续调用使用。

    entities/base.py 中的 BaseModel 使用这两者自动配置 PynamoDB:

    • 连接 — 当 is_local()True 时,BaseModel.Metaconfig.json 设置 host 并硬编码 regionaws_access_key_idaws_secret_access_key,将 PynamoDB 指向本地 DynamoDB 实例。在 AWS 中,这些保持未设置状态,因此 PynamoDB 使用默认凭证链。
    • 表名BaseModel._get_connection() 在每次操作之前调用 get_table_name(),因此在运行时解析正确的表,无需任何手动配置。

    停止 dev(例如使用 Ctrl+C)会自动删除 DynamoDB Local 容器,但会保留命名卷,以便您的数据在重启后保持不变。

    GSI 在项目根目录的 config.json 中的 tableConfig.globalSecondaryIndexes 键下定义。为每个 GSI 添加一个条目,然后通过在 <name>/entities/base.py 中添加或删除相应的 GlobalSecondaryIndex 类和属性来反映 BaseModel 中的更改:

    config.json
    {
    ...
    "tableConfig": {
    "globalSecondaryIndexes": [
    {
    "indexName": "gsi1pk-gsi1sk-index",
    "partitionKey": "gsi1pk",
    "sortKey": "gsi1sk"
    },
    {
    "indexName": "gsi2pk-gsi2sk-index",
    "partitionKey": "gsi2pk",
    "sortKey": "gsi2sk"
    }
    ]
    }
    }

    对于仅使用哈希键的 GSI,sortKey 字段是可选的。

    此配置文件是所有使用者读取的单一数据源:

    • 本地开发dev 读取 config.json 并创建或更新本地表以匹配 GSI 列表
    • CDK — 构造在合成时读取 config.json,因此 GSI 更改会在下次 cdk deploy 时反映出来
    • Terraform — 模块在计划/应用时读取 config.json

    在任何 Python 项目中,将 DynamoDB 包添加为工作区依赖项并直接导入实体类:

    from my_db_package.entities import ExampleModel
    item = ExampleModel.get_by_id('123')

    DynamoDB 生成器会根据您选择的 iac 创建 CDK 或 Terraform 基础设施。

    CDK 构造在 common/constructs 中创建。使用示例:

    packages/infra/src/stacks/application-stack.ts
    import { MyTable } from ':my-scope/common-constructs';
    export class ApplicationStack extends Stack {
    constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);
    const table = new MyTable(this, 'Table');
    }
    }

    这将配置一个具有以下特性的 DynamoDB 表:

    • pk(分区键)和 sk(排序键),均为 String 类型
    • config.json 中定义的全局二级索引
    • 按需(PAY_PER_REQUEST)计费
    • 客户管理的 KMS 加密,并自动轮换密钥
    • 启用时间点恢复
    • 启用删除保护
    • 表名在 AWS AppConfig 的 dynamodb 命名空间下注册到运行时配置中

    要允许 Lambda 函数访问 DynamoDB 表,请在您的基础设施中授予必要的权限。

    在表构造上调用 grantReadWriteData。这将授予 Lambda 执行角色所需的 DynamoDB 和 KMS 权限:

    packages/infra/src/stacks/application-stack.ts
    import { MyTable } from ':my-scope/common-constructs';
    const table = new MyTable(this, 'Table');
    const api = new Api(this, 'Api', {
    integrations: Api.defaultIntegrations(this).build(),
    });
    Object.entries(api.integrations).forEach(([, integration]) => {
    table.grantReadWriteData(integration.handler);
    });

    默认启用删除保护以防止意外删除表。

    对于预期会删除表的环境(例如短期开发或预览堆栈),可以禁用删除保护。

    packages/infra/src/stacks/application-stack.ts
    import { MyTable } from ':my-scope/common-constructs';
    const table = new MyTable(this, 'Table', {
    deletionProtection: false,
    });

    表默认使用按需(PAY_PER_REQUEST)计费。对于可预测的高吞吐量工作负载,可以切换到预配置容量。

    packages/infra/src/stacks/application-stack.ts
    import { BillingMode } from 'aws-cdk-lib/aws-dynamodb';
    import { MyTable } from ':my-scope/common-constructs';
    const table = new MyTable(this, 'Table', {
    billingMode: BillingMode.PROVISIONED,
    readCapacity: 5,
    writeCapacity: 5,
    });

    默认启用时间点恢复,允许您将表恢复到过去 35 天内的任何时间点。

    packages/infra/src/stacks/application-stack.ts
    import { MyTable } from ':my-scope/common-constructs';
    const table = new MyTable(this, 'Table', {
    pointInTimeRecoverySpecification: { pointInTimeRecoveryEnabled: false },
    });

    用于加密表的 KMS 密钥默认启用自动密钥轮换。如果您的安全策略在外部管理轮换,可以禁用它。

    packages/infra/src/stacks/application-stack.ts
    import { MyTable } from ':my-scope/common-constructs';
    const table = new MyTable(this, 'Table', {
    enableKeyRotation: false,
    });

    使用 connection 生成器将此项目与工作区中的其他项目集成。以下连接涉及此项目:

    FastAPI Amazon DynamoDB Python
    FastAPI to Python DynamoDB 将 FastAPI 连接到 DynamoDB 表
    Strands Agents Python Amazon DynamoDB Python
    Python Agent to Python DynamoDB 将 Python Agent 连接到 DynamoDB 表
    Model Context Protocol Python Amazon DynamoDB Python
    Python MCP Server to Python DynamoDB 将 Python MCP Server 连接到 DynamoDB 表