sql 多对多关系中插入行时如何解决死锁问题?
Posted
技术标签:
【中文标题】sql 多对多关系中插入行时如何解决死锁问题?【英文标题】:How to solve deadlock when inserting rows in sql many-to-many relationship with minimum cardinality one restriction? 【发布时间】:2021-09-26 08:46:44 【问题描述】:今年我一直在学习关系数据库以及如何设计它们。为了加强我的知识,我正在尝试使用 Python 和 sqlite3 设计和实现一个数据库。
该数据库是关于一家纺织公司的,除其他外,他们希望保留以下信息:
他们用于制造产品的材料 寻找材料的商店 一些商店(他们确实购买材料的商店)被视为供应商。 他们想知道哪些供应商提供哪些材料关于这最后一个关系,有一些限制:
供应商可以提供不止一种材料(供应商类最大基数很多) 一种材料可以由多个供应商提供(材料类别最大基数很多) 所有材料必须由至少一个供应商提供(材料类别最小基数之一) 所有供应商必须提供至少一种材料(供应商等级最低基数之一)这就是我认为 ER 图给出这些指示的方式:
Entity-Relation diagram for "Provides" relationship
鉴于最小基数之一,我认为我必须通过触发器实现完整性限制。这就是我认为逻辑设计(数据库中的实际表)的样子:
Logical diagram for "Provides" relationship
具有以下完整性限制:
IR1。 Material-Provides 中的最小基数之一: Material 表中的“cod_material”属性的每个值必须作为 中的“cod_material”属性的值至少出现一次提供表格。
IR2。 Supplier-Provides 中的最小基数: Supplier 表中“cod_supplier”属性的每个值必须作为 中“cod_supplier”属性的值至少出现一次提供表格。
所有这一切意味着,在插入新的供应商或材料时,我还必须插入他们提供的材料(在供应商的情况下)或供应商提供的材料(在材料的情况下)。
这是我考虑完整性限制的触发器的样子(我还应该补充一点,我一直在使用 pl-sql,而 sqlite 使用 sql,所以我不习惯这种语法,可能会有一些错误):
CREATE TRIGGER IF NOT EXISTS check_mult_provides_supl
AFTER INSERT ON Supplier
BEGIN
SELECT
CASE WHEN ((SELECT p.cod_supplier FROM Provides p WHERE p.cod_supplier = new.cod_supplier) IS NULL)
THEN RAISE(ABORT, 'Esta tienda no ha provisto aun ningun material')
END;
END;
CREATE TRIGGER IF NOT EXISTS check_mult_provides_mat
AFTER INSERT ON Material
BEGIN
SELECT
CASE WHEN ((SELECT m.cod_material FROM Material m WHERE m.cod_material = new.cod_material) IS NULL)
THEN RAISE(ABORT, 'Este material no ha sido provisto por nadie')
END;
END;
我已尝试分别向 Material 和 Supplier 表添加新行,并且触发器正在工作(或者至少它们不允许我插入新的提供表中没有行的行)。
这是我遇到僵局的时候:
如果数据库为空,如果我尝试在 Material 或 Supplier 表中插入一行,触发器会触发并且它们不允许我(因为首先我需要在表中插入相应的行提供)。但是,如果我尝试在 Provides 表中插入一行,则会出现外键约束错误(显然,因为该供应商和材料尚未插入各自的表中),所以基本上我不能在我的数据库中插入行。
我能想到的唯一答案不是很令人满意:暂时禁用任何约束(外键约束或触发器的完整性)会使数据库完整性面临风险,因为新插入的行不会触发触发器即使这个之后启用。我想到的另一件事是放宽最小基数限制,但我假设与最小基数的多对多关系,一个限制在真实数据库中应该是常见的,所以必须有另一种解决方案。
我怎样才能摆脱这个僵局?也许一个过程(虽然 sqlite 没有存储过程,但我想我可以通过 sqlite3 模块中的 create_function() 使用 Python API 来制作它们)可以解决问题?
以防万一,如果有人想重现这部分数据库,这里是创建表的代码(我最后决定自动递增主键,所以数据类型是整数,而不是 ER图和表示数据类型字符的逻辑图)
CREATE TABLE IF NOT EXISTS Material (
cod_material integer AUTO_INCREMENT PRIMARY KEY,
descriptive_name varchar(100) NOT NULL,
cost_price float NOT NULL
);
CREATE TABLE IF NOT EXISTS Shop (
cod_shop integer AUTO_INCREMENT PRIMARY KEY,
name varchar(100) NOT NULL,
web varchar(100) NOT NULL,
phone_number varchar(12),
mail varchar(100),
address varchar(100)
);
CREATE TABLE IF NOT EXISTS Supplier (
cod_proveedor integer PRIMARY KEY CONSTRAINT FK_Supplier_Shop REFERENCES Shop(cod_shop)
);
CREATE TABLE IF NOT EXISTS Provides (
cod_material integer CONSTRAINT FK_Provides_Material REFERENCES Material(cod_material),
cod_supplier integer CONSTRAINT FK_Provides_Supplier REFERENCES Supplier(cod_supplier),
CONSTRAINT PK_Provides PRIMARY KEY (cod_material, cod_supplier)
);
【问题讨论】:
【参考方案1】:我相信你想要一个DEFERRED FOREIGN KEY。但是,触发器会在触发时产生干扰。
但是,您还需要考虑您发布的代码。没有 AUTO_INCREMENT
关键字,它是 AUTOINCREMENT
(但是您很可能不需要 AUTOINCREMENT,因为 INTEGER PRIMARY KEY 会满足您的所有要求)。
如果您检查SQLite AUTOINCREMENT 以及
AUTOINCREMENT 关键字会带来额外的 CPU、内存、磁盘空间和磁盘 I/O 开销,如果不是严格需要,应避免使用。通常不需要。
Supplier 表没有用,因为您已经编写了代码,它只是一个引用没有其他数据的商店的单列。但是,提供表将供应商表引用到一个不存在的列 (cod_supplier)。
编码CONSTRAINT name REFERENCES table(column(s))
不遵守语法,因为CONSTRAINT 是表级子句,而 REFERENCES 是列级子句,这似乎会引起一些混淆。
我怀疑您可能使用了触发器,因为 FK 冲突没有做任何事情。默认情况下,FK 处理是关闭的,必须按照Enabling Foreign Key Support 启用。我不认为它们是必需的。
无论如何,我相信以下内容(包括克服上述问题的更改)演示了延迟外键:-
DROP TABLE IF EXISTS Provides;
DROP TABLE IF EXISTS Supplier;
DROP TABLE IF EXISTS Shop;
DROP TABLE IF EXISTS Material;
DROP TRIGGER IF EXISTS check_mult_provides_supl;
DROP TRIGGER IF EXISTS check_mult_provides_mat;
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS Material (
cod_material integer PRIMARY KEY,
descriptive_name varchar(100) NOT NULL,
cost_price float NOT NULL
);
CREATE TABLE IF NOT EXISTS Shop (
cod_shop integer PRIMARY KEY,
name varchar(100) NOT NULL,
web varchar(100) NOT NULL,
phone_number varchar(12),
mail varchar(100),
address varchar(100)
);
CREATE TABLE IF NOT EXISTS Supplier (
cod_supplier INTEGER PRIMARY KEY, cod_proveedor integer /*PRIMARY KEY*/ REFERENCES Shop(cod_shop) DEFERRABLE INITIALLY DEFERRED
);
CREATE TABLE IF NOT EXISTS Provides (
cod_material integer REFERENCES Material(cod_material) DEFERRABLE INITIALLY DEFERRED,
cod_supplier integer REFERENCES Supplier(cod_supplier) DEFERRABLE INITIALLY DEFERRED,
PRIMARY KEY (cod_material, cod_supplier)
);
/*
CREATE TRIGGER IF NOT EXISTS check_mult_provides_supl
AFTER INSERT ON Supplier
BEGIN
SELECT
CASE WHEN ((SELECT p.cod_supplier FROM Provides p WHERE p.cod_supplier = new.cod_supplier) IS NULL)
THEN RAISE(ABORT, 'Esta tienda no ha provisto aun ningun material')
END;
END;
CREATE TRIGGER IF NOT EXISTS check_mult_provides_mat
AFTER INSERT ON Material
BEGIN
SELECT
CASE WHEN ((SELECT m.cod_material FROM Material m WHERE m.cod_material = new.cod_material) IS NULL)
THEN RAISE(ABORT, 'Este material no ha sido provisto por nadie')
END;
END;
*/
-- END TRANSACTION; need to use this if it fails before getting to commit
BEGIN TRANSACTION;
INSERT INTO Shop (name,web,phone_number,mail,address)VALUES('shop1','www.shop1.com','000000000000','shop1@email.com','1 Somewhere Street, SomeTown etc');
INSERT INTO Supplier (cod_proveedor) VALUES((SELECT max(cod_shop) FROM Shop));
INSERT INTO Material (descriptive_name,cost_price)VALUES('cotton',10.5);
INSERT INTO Provides VALUES((SELECT max(cod_material) FROM Material),(SELECT max(cod_supplier) FROM Supplier ));
COMMIT;
SELECT * FROM shop
JOIN Supplier ON Shop.cod_shop = cod_proveedor
JOIN Provides ON Provides.cod_supplier = Supplier.cod_supplier
JOIN Material ON Provides.cod_material = Material.cod_material
;
DROP TABLE IF EXISTS Provides;
DROP TABLE IF EXISTS Supplier;
DROP TABLE IF EXISTS Shop;
DROP TABLE IF EXISTS Material;
DROP TRIGGER IF EXISTS check_mult_provides_supl;
DROP TRIGGER IF EXISTS check_mult_provides_mat;
按原样运行时,结果是:-
但是,如果对供应商的 INSERT 更改为:-
INSERT INTO Supplier (cod_proveedor) VALUES((SELECT max(cod_shop) + 1 FROM Shop));
即对商店的引用不是现有商店(大于 1 个)然后:-
消息/日志是:-
BEGIN TRANSACTION
> OK
> Time: 0s
INSERT INTO Shop (name,web,phone_number,mail,address)VALUES('shop1','www.shop1.com','000000000000','shop1@email.com','1 Somewhere Street, SomeTown etc')
> Affected rows: 1
> Time: 0.002s
INSERT INTO Supplier (cod_proveedor) VALUES((SELECT max(cod_shop) + 1 FROM Shop))
> Affected rows: 1
> Time: 0s
INSERT INTO Material (descriptive_name,cost_price)VALUES('cotton',10.5)
> Affected rows: 1
> Time: 0s
INSERT INTO Provides VALUES((SELECT max(cod_material) FROM Material),(SELECT max(cod_supplier) FROM Supplier ))
> Affected rows: 1
> Time: 0s
COMMIT
> FOREIGN KEY constraint failed
> Time: 0s
这是延迟插入成功但提交失败。
您不妨参考SQLite Transaction
【讨论】:
关于 Provides 表,我完全搞砸了代码:我使用的是西班牙名字(因为我是西班牙人)所以“proveedor”的意思是“供应商”,只是忘了翻译那一点。不过,您对 Supplier 表的设计是正确的。感谢您的快速答复!我会检查一下,但你提供的良好参考让我相信它会起作用【参考方案2】:我认为应该重新考虑数据库的设计,因为表Provides
代表了两组不同的信息:哪个商店提供哪些材料,哪个是某种材料的供应商。更好的设计应该是将这两种信息分开,这样可以增加外键表达的约束。
这里是表格的草图,不依赖于特定的 RDBMS。
Material (cod_material, descriptive_name, cost_price)
PK (cod_material)
Shop (cod_shop, name, web. phone_number, mail, address)
PK (cod_shop)
ShopMaterial (cod_shop, cod_material)
PK (cod_shop, cod_material),
cod_shop FK for Shop, cod_material FK for Material
SupplierMaterial (cod_sup, cod_material)
PK (cod_sup, cod_material)
cod_sup FK for Shop, cod_material FK for material
(cod_sup, cod_material) FK for ShopMaterial
不同的外键已经考虑了几个约束。我认为唯一没有强制执行的约束是:
All materials must be provided by at least one supplier
此约束不能自动执行,因为您必须先插入材料,然后添加相应的对(cod_shop、cod_material),然后再添加对(cod_sup、cod_material)。为此,我认为最好的选择是在应用程序级别定义一个同时插入材料的程序,可以从中获得材料的商店,以及它的供应商,以及删除的程序材料,以及ShopMaterial
和SupplierMaterial
表中的相关对。
【讨论】:
以上是关于sql 多对多关系中插入行时如何解决死锁问题?的主要内容,如果未能解决你的问题,请参考以下文章