pynamodb,一个神奇的 Python 库!
线上表不大,代码先乱了。
同样是往 DynamoDB 写一条数据,团队里常见两种写法:一堆 boto3.client("dynamodb") 手搓字典,{"S": "...", "N": "..."} 满天飞;另一种更离谱,业务字段已经改了,序列化层还在偷偷兼容旧字段,最后排查半天,不是表有问题,是人把自己绕进去了。
PynamoDB 这东西,我第一眼就挺信。它干的不是“替你理解 DynamoDB”,而是把那层最烦的样板代码先铲掉:模型定义、属性序列化、条件更新、批量写、事务操作,基本都给你接住了。Meta 里至少要告诉它表名,区域也得配清楚;新版本里它已经不再默认给你兜一个 AWS region 了,这种细节不盯着,代码本地能跑,上线就容易翻车。
我一般不会上来就吹“它像 ORM”。真到 DynamoDB 这儿,你要是把它当 Django ORM 那样随便使,后面八成要吃亏。PynamoDB 更像一个很顺手的 Python 包装层:模型还是你自己控,分区键、排序键、索引、条件表达式这些核心东西,一样要老老实实想明白。只不过写出来没那么硌手。
先看个我更愿意在线上落的模型,不花哨,但够用:
from datetime import datetime
from pynamodb.models import Model
from pynamodb.attributes import (
UnicodeAttribute,
NumberAttribute,
UTCDateTimeAttribute,
JSONAttribute,
VersionAttribute,
)classOrderModel(Model):
classMeta:
table_name = "order_center"
region = "ap-southeast-1"
order_id = UnicodeAttribute(hash_key=True)
biz_type = UnicodeAttribute(range_key=True)
status = UnicodeAttribute(default="CREATED")
amount = NumberAttribute(default=0)
ext = JSONAttribute(null=True)
created_at = UTCDateTimeAttribute(default=datetime.utcnow)
updated_at = UTCDateTimeAttribute(default=datetime.utcnow)
version = VersionAttribute()
这个写法的好处很直接。字段类型、默认值、乐观锁,全在一个地方。后面你查单、改状态、补扩展字段,脑子里不用再来回翻“这个字段进库前是不是还要手动转一下”。PynamoDB 自带的属性类会负责 DynamoDB 属性的序列化和反序列化,像 JSONAttribute、UTCDateTimeAttribute 这种,在线上确实省事。
真正让我觉得它顺手的,是更新操作。很多人写 DynamoDB,喜欢先 get,改对象,再 save。能不能用?能。只是你一旦碰上并发更新、库存扣减、状态流转,这么写就开始发虚了。我更愿意把条件和动作一起塞进更新里,少一次心跳,少一层侥幸。
from datetime import datetimedefpay_order(order_id: str, biz_type: str, paid_amount: int):
item = OrderModel(order_id, biz_type)
item.update(
actions=[
OrderModel.status.set("PAID"),
OrderModel.amount.set(paid_amount),
OrderModel.updated_at.set(datetime.utcnow()),
OrderModel.ext.set({"pay_channel": "wx", "confirmed": True}),
],
condition=(
(OrderModel.status == "CREATED") &
(OrderModel.amount >= 0)
)
)
这里值钱的不是 set() 这个动作本身,而是这个味道:改数据的时候,把“只有 CREATED 才能改成 PAID”一并写进去。PynamoDB 支持用属性对象拼 update expression,也支持条件表达式;底下走的还是 DynamoDB 那套能力,但代码终于像人写的了。
再往前走一步,线上更常见的是“不是不会写,是怕写重”。比如补偿任务、重复回调、消费者重试,这些地方最烦的不是异常,而是悄悄把数据写坏。PynamoDB 有 VersionAttribute,本质就是乐观锁。读到什么版本,写回去时就校验什么版本,不一致直接失败,别装作没看见。官方文档里也明确说了,它会把版本号作为并发冲突检测的一部分。
defclose_order(order_id: str, biz_type: str):
order = OrderModel.get(order_id, biz_type)
order.status = "CLOSED"
order.updated_at = datetime.utcnow()
order.save() # version 不对会直接抛异常
这种代码我挺喜欢。它不帮你“解决并发”,但至少不让你悄无声息地覆盖别人刚写进去的数据。很多脏数据,不是业务不会判断,是更新时太随便。
批量写这块,PynamoDB 也很实用。DynamoDB 原生批量写有上限,自己手搓分组很烦;PynamoDB 的 batch_write() 会帮你按批处理,官方文档写得很明白:批量写一次最多 25 个写请求,它会自动帮你拆。
defbatch_init_orders(rows: list[dict]):
with OrderModel.batch_write() as batch:
for row in rows:
batch.save(
OrderModel(
row["order_id"],
row["biz_type"],
status="CREATED",
amount=row["amount"],
ext={"source": row.get("source", "import")}
)
)
但这里我得多说一句,别一看到 batch 就上头。批量写不是批量更新,DynamoDB 的 BatchWriteItem 只支持 Put 和 Delete,不支持带条件的 Update。你要做“多条数据要么一起成功,要么一起失败”,该上事务就上事务,别在业务里自己拼半套补偿。PynamoDB 也支持事务操作,而且是用上下文管理器包起来的,这种写法比你手动调底层 API 稳很多。
所以 PynamoDB 神奇在哪?
不是因为它把 DynamoDB 变简单了。DynamoDB 该你想的建模、热分区、访问模式,它一点都没替你想。它神奇在另一层:把 Python 里那堆又臭又长、还容易写错的 DynamoDB 操作,收成了能维护的模型代码。字段是字段,条件是条件,更新动作是更新动作,读代码的人不需要先把脑子切到 AWS SDK 的报文模式。
真要我给一句不那么工整的评价:这库很适合“已经决定用 DynamoDB,但不想在 Python 里一直揉 boto3 细节”的团队。小项目用它,代码利索;线上项目用它,改字段、加条件、补乐观锁,至少不容易把自己写恶心。行,差不多就写到这。