使用DynamoDB创建唯一的约束
#database #dynamodb #nosql

对于我们像开发人员一样,从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的另一个借口消失了。