“干净”的解决方案是创建一个专用表user_order_pending,其中包含两列:user_id 和order_id(最好都带有外键约束),并在user_id 上设置唯一约束。然后,在一个事务中,将订单插入orders 和users_order_pending 中的相应条目。如果两个并发事务尝试同时插入新的挂单,只有一个事务会成功,另一个事务会回滚。
如果此更改过于复杂,则还有另一个涉及GENERATED 列的mysql 特定解决方案。我们创建一个新列is_pending,即BOOLEAN,并且可以为空。然后,我们将此列的值设置为true,当且仅当status 列是pending。最后,我们在user_id 和is_pending 列上设置UNIQUE 约束。粗略的草图如下所示:
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
status SMALLINT NOT NULL DEFAULT 0,
is_pending BOOLEAN GENERATED ALWAYS AS (
CASE
WHEN status = 0 THEN 1
END
),
CONSTRAINT unique_user_id_is_pending UNIQUE (user_id, is_pending)
);
在上面的示例中,0 中的 status 代表 pending。现在让我们测试我们的解决方案。首先,我们在表中插入一个新行:
INSERT INTO orders(user_id) VALUES(1);
并检查结果:
SELECT * FROM orders;
+----+---------+--------+------------+
| id | user_id | status | is_pending |
+----+---------+--------+------------+
| 1 | 1 | 0 | 1 |
+----+---------+--------+------------+
1 row in set (0.00 sec)
到目前为止,一切都很好。让我们尝试为该用户添加另一个订单:
INSERT INTO orders(user_id) VALUES(1);
ERROR 1062 (23000): Duplicate entry '1-1' for key 'orders.unique_user_id_is_pending'
这个插入被理所当然地拒绝了,太棒了!现在让我们更新现有条目并赋予它另一个状态:
UPDATE orders SET status = 1 WHERE id = 1;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings: 0
再次检查结果:
SELECT * FROM orders;
+----+---------+--------+------------+
| id | user_id | status | is_pending |
+----+---------+--------+------------+
| 1 | 1 | 1 | NULL |
+----+---------+--------+------------+
1 row in set (0.00 sec)
生成的列已更新,整洁!最后,让我们为user_id 1 的用户插入一个新条目:
INSERT INTO orders(user_id) VALUES(1);
Query OK, 1 row affected (0.01 sec)
果然,我们在数据库中有第二个用户订单:
SELECT * FROM orders;
+----+---------+--------+------------+
| id | user_id | status | is_pending |
+----+---------+--------+------------+
| 1 | 1 | 1 | NULL |
| 3 | 1 | 0 | 1 |
+----+---------+--------+------------+
2 rows in set (0.00 sec)
由于约束在user_id 和is_pending,我们可以添加新的挂单,例如user_id 2:
INSERT INTO orders(user_id) VALUES(2);
Query OK, 1 row affected (0.01 sec)
SELECT * FROM orders;
+----+---------+--------+------------+
| id | user_id | status | is_pending |
+----+---------+--------+------------+
| 1 | 1 | 1 | NULL |
| 3 | 1 | 0 | 1 |
| 4 | 2 | 0 | 1 |
+----+---------+--------+------------+
3 rows in set (0.00 sec)
最后:由于约束忽略了NULL-values,我们可以将user_id 1 的二阶移动到非挂起状态:
UPDATE orders SET status=1 WHERE id = 3;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings: 0
SELECT * FROM orders;
+----+---------+--------+------------+
| id | user_id | status | is_pending |
+----+---------+--------+------------+
| 1 | 1 | 1 | NULL |
| 3 | 1 | 1 | NULL |
| 4 | 2 | 0 | 1 |
+----+---------+--------+------------+
3 rows in set (0.00 sec)
这个解决方案的好处是,如果数据库处于合法状态,即如果每个用户最多有一个pending 订单,它可以添加到现有数据库中。可以在不破坏现有代码的情况下将新列和约束添加到表中(除了某些进程可能无法在上述场景中插入数据的事实,这是所需的行为)。