对于我们像开发人员一样,从SQL转换为NOSQL是个好主意,有很多原因。我们的绝大多数工作是OLTP,即交易数据处理,我们知道访问模式是什么,可以以支持这些访问模式的方式设计我们的NOSQL数据存储。
当然,在NOSQL数据库中不可避免地,诸如DynamoDB之类的NOSQL数据库中不可避免地支持这些事情,而这些事情通常是那些希望进行过渡的人的障碍。这些事情之一是独特的约束。
定义
如果您不熟悉唯一的约束,则可以保证表格中只有一个特定值的实例(或一组值,如果是复合约束)。它与主要键不同,因为它不是……好吧……主要键。一个很好的例子是一个用户表,主要键将是某种类型的userId
,而独特的约束将是用户的email
。在SQL数据库中,您可以拥有此约束,使您无法使用两个用户使用同一电子邮件,因为该表不允许重复。
在DynamoDB中没有唯一的约束,但是有一种方法可以获得相同的行为。这是您的方式。
救援交易
最近有一个Twitter线程对DynamoDB交易的值进行了辩论。我知道有些人不喜欢它们,但这是我认为它们很有价值的一个例子。
通过DynamoDB事务,您可以在一组记录上创建有限的酸性交易。对于用户的email
约束的示例,您只需要两个(DynamoDB在撰写本文时最多支持100个记录,而4MB的总尺寸限制为4MB)。那么此交易是什么样的?
基本上,您对每个唯一约束都有一个Put
,而主要记录则有一个。如果您进行单个表设计,则可以针对同一表,但是DynamoDB交易可以在许多表中使用。每个约束记录的键(分区键和分类密钥的组合)在表格上唯一标识了它。每个Put
都包含一个条件,该条件要求记录尚未存在或由用户更新的用户拥有。
这是一个可能外观的示例:
const user = {
userId: 'User1',
email: 'john@example.com',
first: 'John',
last: 'Doe'
};
await documentClient.send(new new TransactWriteCommand({
TransactItems: [
{
Put: {
Item: { pk: user.email, sk: 'EmailConstraint', userId: user.userId },
TableName: 'User',
ConditionExpression: 'attribute_not_exists(pk) OR userId = :userId',
ExpressionAttributeValues: {
':userId': user.userId,
}
}
},
{
Put: {
Item: { ...user, pk: user.userId, sk: 'User' },
TableName: 'User'
}
}
]
}));
第一个Put
具有email
和值'EmailConstraint'
的唯一值。第二个Put
具有userId
和值'User'
的唯一值。这意味着您只能在表中使用特定电子邮件,只有一个带有特定用户ID的记录。第一个Put
上的ConditionExpression
限制了操作,说要放置的记录必须是新记录(attribute_not_exists(pk)
)或当前记录的用户ID必须与保存记录相同(userId = :userId
)。
现在想象一下,如果我们尝试添加一个看起来像这样的第二个用户:
const user = {
userId: 'User2',
email: 'john@example.com',
first: 'John',
last: 'Roe'
};
使用上述交易将失败,因为交易中的第一项将违反ConditionExpression
。具体而言,该记录已经存在,因此attribute_not_exists(pk)
是错误的,并且由于现有记录的用户是针对其他用户(user1)的,所以这也是错误的。
如果我们将记录更改为拥有不同的电子邮件,它将成功:
const user = {
userId: 'User2',
email: 'john.roe@example.com',
first: 'John',
last: 'Roe'
};
现在,假设我们要更新第一个记录,因此我们进行另一项交易以将其更新为以下:
const user = {
userId: 'User1',
email: 'john@example.com',
first: 'Johnathan',
last: 'Doe'
};
这将成功。 EmailConstraint
Record的ConditionExpression
感到满意。尽管attribute_not_exists(pk)
是错误的,但userId = :userId
是正确的。本质上,此记录上没有任何变化。
当您想更改用户的电子邮件时呢?
const user = {
userId: 'User2',
email: 'john@example.com',
first: 'John',
last: 'Roe'
};
再次,这将失败,因为EmailConstraint
记录已经存在,并且不属于此用户。
const user = {
userId: 'User1',
email: 'johnanthan@example.com',
first: 'Johnathan',
last: 'Doe'
};
这将成功,因为johnanthan@example.com没有记录是关键。当然,这会产生不同的问题。现在您有了孤立的记录john@example.com
。您的第一个想法可能是在交易中包括Delete
,但这不会完全奏效,因为我们不知道要删除的电子邮件地址。当然,您可以首先查找它,但是您无法在交易中查找它,因此,如果同时发生了两个更新,最终可能会出现超出数据。对于用户的电子邮件可能不是一个大问题,但这可能是其他数据的问题。您可以确保要更新的记录仍然是您阅读的记录,并且如果没有,则失败。在某些情况下,当碰撞的概率较低时,这是一个不错的选择。如果失败了,您可以再次查找。
拥抱最终的一致性
就个人而言,我喜欢采取不同的方法来解决这个问题。通过使用DynamoDB流,您可以在有MODIFY
时检查到电子邮件地址的更改,如果有更改,则可以在当时删除旧记录。这意味着在某个时期仍然无法使用电子邮件,但这是保证删除的好方法。您需要在REMOVE
上执行相同的操作,因为删除的问题是删除时不知道电子邮件是什么。
结论
否,DynamoDB中的独特约束并不像SQL数据库中的那样容易,并且只有在绝对需要时才能使用它,但是至少现在您知道它是一个选择。避免nosql的另一个借口消失了。