本地DynamoDB插入遇ValidationException:TransactWriteItems缺必填键
问题排查与解决方案
核心错误原因
- 缺失表主键值:你的
Profile表主键是email(HASH键),但在transact_write_items的第二个Put操作中,Item只包含pk,未提供必填的email键——DynamoDB要求每个Item必须包含主键属性,这是触发ValidationException的直接原因。 - 引用不存在的字段:测试数据仅包含
email,但代码中尝试访问profile['username'],会引发KeyError(当前被主键错误掩盖)。 - ConditionExpression逻辑错误:表主键是
email,但你用了attribute_not_exists(pk)作为条件,pk并非表的主键或预定义属性,逻辑与表结构不匹配。 - Mock与本地实例冲突:使用
@mock_dynamodb装饰器后,应在测试的setUp中创建模拟表,而非直接连接本地DynamoDB实例,二者不能混用。
修复步骤
1. 修正TransactItems的Put操作,确保每个Item包含主键
基于你的表结构,每个写入的Item必须包含email。如果要实现username的唯一约束,需调整逻辑(参考官方博客的设计可通过全局二级索引实现),先修复主键问题:
def insertProfile(self, profile): message = f"Profile was not created" # 使用moto mock时无需指定endpoint_url,moto会自动模拟客户端 dynamodb = boto3.client('dynamodb') response = dynamodb.transact_write_items( TransactItems=[ { 'Put': { 'TableName': 'Profile', 'Item':{ 'email' : { 'S': profile['email'], } }, # 改为检查主键email是否已存在 'ConditionExpression':'attribute_not_exists(email)' }, 'Put': { 'TableName': 'Profile', 'Item':{ 'email' : { 'S': profile['email'], }, # 处理username缺失的情况,用邮箱前缀作为默认值 'pk': { 'S': f"username#{profile.get('username', profile['email'].split('@')[0])}" } }, 'ConditionExpression':'attribute_not_exists(pk)' } } ] ) return { "status_code": response['ResponseMetadata']['HTTPStatusCode'], "message": message }
2. 补充测试数据或处理字段缺失
修改测试数据event.json,添加username字段:
{ "email": "email@b.com", "username": "testuser" }
或在代码中通过get方法设置默认值(如上面代码所示),避免KeyError。
3. 正确使用moto Mock功能
在setUp中创建模拟表,不再依赖本地DynamoDB实例:
def setUp(self) -> None: self.logging = logging.getLogger(__name__) self.re_dict = { "email": "(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)", # 若需验证username,添加对应正则 "username": "^[a-zA-Z0-9_]{3,20}$" } # 创建模拟DynamoDB表,若要实现username唯一,添加全局二级索引 dynamodb = boto3.client('dynamodb') dynamodb.create_table( TableName='Profile', AttributeDefinitions=[ {'AttributeName': 'email', 'AttributeType': 'S'}, {'AttributeName': 'pk', 'AttributeType': 'S'} ], KeySchema=[ {'AttributeName': 'email', 'KeyType': 'HASH'} ], # 添加唯一GSI实现username的唯一性约束 GlobalSecondaryIndexes=[ { 'IndexName': 'pk-index', 'KeySchema': [{'AttributeName': 'pk', 'KeyType': 'HASH'}], 'Projection': {'ProjectionType': 'ALL'}, 'ProvisionedThroughput': {'ReadCapacityUnits': 1, 'WriteCapacityUnits': 1}, 'IndexStatus': 'ACTIVE' } ], ProvisionedThroughput={'ReadCapacityUnits': 1, 'WriteCapacityUnits': 1} ) def testHandler(self) -> None: response = { "status_code": HTTPStatus.BAD_REQUEST, "message": "Creation was not successful." } # 直接构造测试event,无需读取文件,更便于测试 event = { "email": "email@b.com", "username": "testuser" } self.logging.info(event) is_valid, profileData = self.validateProfile(event) self.assertEqual(True, is_valid) response = self.insertProfile(profileData) # 注意response是字典,需取status_code字段断言 self.assertEqual(HTTPStatus.OK, response['status_code'])
4. 调整唯一约束逻辑
若要实现username的唯一性,需依赖全局二级索引的唯一性配置,此时ConditionExpression应检查GSI的键是否存在,而非非主键属性pk。
总结
当前最直接的触发错误是第二个Put操作的Item缺失表主键email,导致DynamoDB验证失败。同时需调整Mock使用方式,避免与本地实例冲突,并处理字段缺失问题。
内容的提问来源于stack exchange,提问作者Hardrock302
相关产品推荐
相关产品推荐

