编程学习网 > 编程语言 > Python > pynamodb,一个神奇的 Python 库!
2026
07-21

pynamodb,一个神奇的 Python 库!


DynamoDB 最烦人的地方,不是慢,也不是贵,是你一写代码就像在拼一坨 JSON

一个简单的保存订单状态,boto3 原生写法能写到人怀疑人生。字段类型要自己包,表达式要自己拼,条件更新还要注意名字冲突。代码跑是能跑,但半年后再看,第一反应一般是:这玩意儿谁敢改?

PynamoDB 就是专门治这个毛病的。

它不是把 DynamoDB 变成 MySQL,也不是让你忘掉分区键、排序键、GSI 这些东西。这个库更像是在 DynamoDB 外面套了一层 Python 写法,让表结构、字段、查询、更新看起来像正常业务代码。官方文档也把它定位成 DynamoDB Pythonic 接口,用 Model 抽象去包 DynamoDB 表。

先看一段我比较愿意放进项目里的写法。

from datetime import datetime, timezone
from pynamodb.attributes import UnicodeAttribute, NumberAttribute, UTCDateTimeAttribute, VersionAttribute
from pynamodb.indexes import GlobalSecondaryIndex, AllProjection
from pynamodb.models import Model

class BuyerTimeIndex(GlobalSecondaryIndex):
    class Meta:
        index_name = "idx_buyer_created"
        projection = AllProjection()
        read_capacity_units = 5
        write_capacity_units = 2
    buyer_id = UnicodeAttribute(hash_key=True)
    created_at = UTCDateTimeAttribute(range_key=True)

class OrderSnapshot(Model):
    class Meta:
        table_name = "order_snapshot"
        region = "ap-southeast-1"
    order_id = UnicodeAttribute(hash_key=True)
    sku_id = UnicodeAttribute(range_key=True)
    buyer_id = UnicodeAttribute()
    status = UnicodeAttribute()
    pay_amount = NumberAttribute(default=0)
    created_at = UTCDateTimeAttribute(default_for_new=lambda: datetime.now(timezone.utc))
    updated_at = UTCDateTimeAttribute(null=True)
    version = VersionAttribute()
    buyer_time_index = BuyerTimeIndex()

这段代码有几个点我比较喜欢。

表结构直接贴在 Python 类上,哪个字段是 hash key,哪个字段是 range key,一眼能扫出来。GSI 也没有藏在 Terraform 或控制台里让人猜,代码里就能看到 buyer_id + created_at 是一条查询路径。

当然,DynamoDB 的老规矩还在。GSI 不是免费午餐,它有自己的读写容量,写主表时索引也要跟着更新。AWS 文档里说得很清楚,GSI 的吞吐和主表是分开的,写入表时索引也会被更新。

所以我一般不建议一上来就建一堆索引。DynamoDB 不是关系库,别拿以后可能会查当理由乱建 GSI。能确认查询路径,再建。

保存数据就干净多了。

def save_paid_order(event: dict) -> None:
    item = OrderSnapshot(
        event["order_id"],
        event["sku_id"],
        buyer_id=event["buyer_id"],
        status="PAID",
        pay_amount=event.get("pay_amount", 0),
    )
    item.save()

要是用 boto3 原生写,SNExpressionAttributeValues 会铺满屏幕。PynamoDB 把这些脏活藏掉了,业务代码看起来终于像业务代码。

但这里别误会,PynamoDB 不是让你随便 scan 的护身符。

我见过有人把它当 ORM 用,写出这种东西:

for item in OrderSnapshot.scan(OrderSnapshot.status == "PAID"):
    handle(item)

这地方我第一眼就不太信。

小表无所谓,线上订单表一大,scan 就开始拖后腿。PynamoDB 写法再优雅,也挡不住 DynamoDB 的访问模型。该 query query,该走索引就走索引。PynamoDB 的文档也明确提供 query 这层高阶 API,用来封装 DynamoDB 的查询能力。

正常一点的查法应该这样:

def list_buyer_orders(buyer_id: str, limit: int = 20):
    rows = OrderSnapshot.buyer_time_index.query(
        buyer_id,
        scan_index_forward=False,
        limit=limit,
    )
    return [
        {
            "order_id": row.order_id,
            "sku_id": row.sku_id,
            "status": row.status,
            "amount": int(row.pay_amount),
            "created_at": row.created_at.isoformat(),
        }
        for row in rows
    ]

这个接口查的是某个用户最近订单。查询路径明确:buyer_id 作为 GSI 分区键,created_at 倒序。

这才是 DynamoDB 舒服的姿势。

更新也挺顺手,尤其是条件更新。

订单状态这种东西,最怕重复回调。支付网关抖一下,MQ 重投一下,接口多进来一次,状态就可能被覆盖。这里我一般不直接 save(),而是用条件更新把边界卡死。

from pynamodb.exceptions import UpdateError

def mark_order_shipped(order_id: str, sku_id: str, express_no: str) -> bool:
    try:
        item = OrderSnapshot.get(order_id, sku_id)
        item.update(
            actions=[
                OrderSnapshot.status.set("SHIPPED"),
                OrderSnapshot.updated_at.set(datetime.now(timezone.utc)),
            ],
            condition=(OrderSnapshot.status == "PAID"),
        )
        return True
    except OrderSnapshot.DoesNotExist:
        print(f"[order-miss] order_id={order_id}, sku_id={sku_id}")
        return False
    except UpdateError as e:
        print(f"[order-skip] order_id={order_id}, sku_id={sku_id}, reason={e}")
        return False

这段代码的味道就对了。

只有 PAID 状态才能改成 SHIPPED。如果已经发货了,或者订单被取消了,这次更新直接失败。失败不是坏事,悄悄覆盖才是坏事。

还有一个我挺常用的地方:批量写。

比如每天从上游同步一批商品库存快照,不要一条一条 save(),那样网络往返会把人磨死。

def flush_stock_snapshot(rows: list[dict]) -> None:
    with OrderSnapshot.batch_write() as batch:
        for row in rows:
            batch.save(
                OrderSnapshot(
                    row["order_id"],
                    row["sku_id"],
                    buyer_id=row["buyer_id"],
                    status=row["status"],
                    pay_amount=row.get("pay_amount", 0),
                    updated_at=datetime.now(timezone.utc),
                )
            )

PynamoDB 好用,但别把它吹成万能。

它解决的是 Python DynamoDB 时的啰嗦难维护,不是解决数据建模问题。分区键设计错了,热点还是热点;查询路径没想清楚,最后还是 scanGSI 建多了,账单照样不好看。

我对它的判断很简单:如果项目里已经选了 DynamoDB,又是 Python 技术栈,PynamoDB 值得上。尤其是 Lambda、小型后端服务、异步任务、数据同步脚本,这库用起来很顺。

但上之前要把三件事想清楚。

第一,表的主查询路径是什么。

第二,哪些字段需要条件更新。

第三,哪些地方绝对不能 scan

这三件事没想清楚,换什么库都救不了。PynamoDB 只是让代码更像人写的,DynamoDB 那套规矩,它一点都没替你取消。

上就是“pynamodb,一个神奇的 Python 库!的详细内容,想要了解更多Python教程欢迎持续关注编程学习网。  

扫码二维码 获取免费视频学习资料

Python编程学习

查 看2022高级编程视频教程免费获取