尽管检查是否存在,但重复的 PRIMARY KEY 条目

Duplicate PRIMARY KEY entry despite checking for existence

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

订单结构如下:

{
    "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 错误 'Duplicate entry for PRIMARY KEY email'。当我尝试保存产品时也会发生这种情况,其中 sku 是主键(不过,我没有在此处包含代码,因为它与上面的函数类似)。

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

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

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

值得注意的是这些请求:

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

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

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

如果联系人已经存在,可以让插入失败。如果您使用 UNIQUE 约束定义列(实际上,通过将其设为主键,您实际上已经拥有)它将不允许任何重复条目。然后,如果确实发生冲突,您需要处理由此引发的错误。这让数据库成为单个 'source of truth'。你确实需要做一些反省来区分错误:

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)