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;
    

我已尝试分别向 MaterialSupplier 表添加新行,并且触发器正在工作(或者至少它们不允许我插入新的提供表中没有行的行)。

这是我遇到僵局的时候:

如果数据库为空,如果我尝试在 MaterialSupplier 表中插入一行,触发器会触发并且它们不允许我(因为首先我需要在表中插入相应的行提供)。但是,如果我尝试在 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)。为此,我认为最好的选择是在应用程序级别定义一个同时插入材料的程序,可以从中获得材料的商店,以及它的供应商,以及删除的程序材料,以及ShopMaterialSupplierMaterial 表中的相关对。

【讨论】:

以上是关于sql 多对多关系中插入行时如何解决死锁问题?的主要内容,如果未能解决你的问题,请参考以下文章

CoreData:如何建模循环多对多关系

CoreData 多对多关系插入和删除操作

如何插入多对多记录数据?

如何解决具有多对多关系的石墨烯 django 节点字段

Jpa多对多如何仅将数据插入2个表

在多对多 SQL 表中查找数据关系或图形