首页 > 解决方案 > 尽管检查存在重复 PRIMARY KEY 条目

问题描述

所以我有一个非常简单的要求,并且是 Node/Knex 的新手,我正在苦苦挣扎。我有一个正在运行的 Web 服务器,它接受单个订单的订单数据(电子商务),然后将其写入数据库。我目前正在通过从另一个本地脚本发送请求来在本地对其进行测试,这意味着每秒有多个订单请求到达(我认为这并不重要,但仍然提供详细信息)。

这是订单结构的样子:

{
    "id": 123,
    "created_at": "date here",
    "product" : {
      "sku": "sku1",
      "name": "Product 1",
      "description" : "description here",
      // and so on
    },
    "contact": {
      "email" : "me@me.com"
      "first_name" : "First",
      // and so on
    }
}

这是我处理订单数据的方式:

// express app
app.post('/write-db-order', (req, res) => {
    const order = req.body.order;

    // check if order already exists
    knex('orders')
    .where('id', order.id)
    .select('id')
    .then((temp) => {
        if (temp.length == 0) {
            saveOrder(order);
        }
    });   

    res.sendStatus(200);
});

saveOrder()函数如下所示:

function saveOrder(order) {
    knex('orders').insert({
        id: order.id,
        title: order.title,
        // other fields
    }).then(() => {
        saveOrderItems(order);
        saveOrderShippingInfo(order);
        saveOrderContact(order);
    }).catch(error => console.log(error));
}

因此,基本订单详细信息首先被插入到数据库中,然后我将保存其余的详细信息。这是saveOrderContact()函数的样子:

function saveOrderContact(order) {
    let contact = order.contact;

    if(!contact) {
        return;
    }

    knex('contacts')
    .where('email', contact.email)
    .select('email')
    .then((result) => {
        if (result.length == 0) {
            knex('contacts').insert({
                email: contact.email,
                first_name: contact.first_name,
                last_name: contact.last_name,
                company_name: contact.company_name,
                job_title: contact.job_title
            }).then(() => {}).catch(error => { 
                console.log(error);
            });;
        }
    });
}

在典型的试运行中,大约 1000-1200 个订单被发送到此服务器,其中一些联系人和产品预计会包含重复项。这就是为什么saveOrderContact()我检查主键(电子邮件),测试联系人是否已经存在。

无论如何,重点是,我遇到了许多 SQL 错误,上面写着“主键电子邮件的重复条目”。当我尝试保存产品时也会发生这种情况,其中sku是主键(不过,我没有在此处包含代码,因为它类似于上面的函数)。

对我来说,Knex 似乎有奇怪的缓存行为,它一次性将所有插入发送到数据库并导致这些重复。但即便如此,我很惊讶select在插入之前进行检查不起作用。我知道 Node 的异步特性,这就是为什么我在里面添加了插入,then()但看起来我错过了一些东西。

这是 Node/Knex 中的常见问题吗?如果是,你如何避免它?如果没有,好吧,请帮助我!:-)

标签: knex.js

解决方案


如果您连续快速地向服务器发送大量请求,那么您很可能会遇到竞争条件。该contacts行很可能是在检查它和尝试插入它之间插入的。

值得注意的是,这些请求:

    }).then(() => {
        saveOrderItems(order);
        saveOrderShippingInfo(order);
        saveOrderContact(order);
    }).catch(error => console.log(error));

将在很短的时间内相互触发(执行不会等到 promise 被解决。另一种表达相同事物但严格顺序的方式是这样的:

  .then(() => saveOrderItems(order))
  .then(() => saveOrderShippingInfo(order))
  .then(() => saveOrderContact(order))

如果联系人已经退出,则可以让插入失败。如果您使用约束定义列UNIQUE(实际上,通过将其设为主键,您实际上已经拥有),它将不允许任何重复条目。然后,如果确实发生冲突,您需要处理引发的错误。这让数据库成为唯一的“事实来源”。你必须做一些自省来区分错误:

knex('contacts')
  .insert({
    email: 'foo@example.com'
    // ...
  })
  .catch(e => {
    // Example for Postgres, where code 23505 is a unique_violation
    if (e.code && e.code === '23505') {
      return
    }
    console.error('Something bad happened while inserting a contact.')
  })

这默默地吞下了独特的约束违规。允许程序继续执行。不幸的是,它是特定于数据库的。它可能比检查联系人然后插入的查询便宜,并且降低了竞争条件的风险。

或者,您可以使用可以说更优雅的原始 SQL(假设 Postgres):

  const insert = knex('contacts').insert({ email: foo@example.com })
  knex.raw('? ON CONFLICT DO NOTHING', [insert]).then(console.log)

推荐阅读