sql 最小基数为1的多对多关系插入行时如何解决死锁?

How to solve deadlock when inserting rows in sql many-to-many relationship with minimum cardinality one restriction?

今年我一直在学习关系数据库以及如何设计它们。为了加强我的知识,我正在尝试使用 Python 和 sqlite3.

设计和实现一个数据库

该数据库是关于一家纺织公司的,除其他外,他们希望保留以下信息:

关于这最后一段关系,有一些限制:

这就是我认为 ER 图给出这些指示的样子:

Entity-Relation diagram for "Provides" relationship

考虑到最小基数,我想我必须通过触发器实施完整性限制。这就是我认为的逻辑设计(数据库中的实际 tables)的样子:

Logical diagram for "Provides" relationship

具有以下完整性限制:

IR1。 Material 中的最小基数一提供: 来自 Material table 的 'cod_material' 属性的每个值必须在 Provides table.

中作为 'cod_material' 属性的值至少出现一次

IR2。 Supplier-Provides 中的最小基数: 来自 Supplier table 的 'cod_supplier' 属性的每个值必须至少出现一次作为值Provides table.

中的 'cod_supplier' 属性

所有这些意味着,在插入新供应商或 material 时,我还必须插入 material 他们提供的内容(在供应商的情况下)或供应商提供的内容它(在 materials 的情况下)。

这是我考虑到完整性限制而创建的触发器的样子(我还应该补充一点,我一直在使用 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;
    

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

这是我陷入僵局的时候:

数据库为空,如果我尝试在 tables MaterialSupplier[=86 中插入一行=] 触发器触发并且它们不允许我(因为首先我需要在 table Provides 中插入相应的行)。但是,如果我尝试在 Provides table 中插入一行,则会出现外键约束错误(显然,因为未插入供应商和 material到他们各自的tables),所以基本上我不能在我的数据库中插入行。

我能想到的唯一答案不是很令人满意:暂时禁用任何约束(外键约束或触发器的完整性约束)会使数据库完整性面临风险,因为新插入的行不会触发触发器,即使这个触发器在之后被启用。我想到的另一件事是放宽最小基数限制,但我假设多对多关系和最小基数限制在真实数据库中应该是常见的,所以必须有另一种解决方案。

我怎样才能摆脱这个僵局?也许是一个过程(虽然 sqlite 没有存储过程,但我想我可以用 Python API by create_function() 在 sqlite3 模块)会成功吗?

以防万一,如果有人想重现数据库的这一部分,这里是创建 tables 的代码(我最终决定自动增加主键,所以数据类型是整数,与表示数据类型字符的 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)
);

我相信你想要一个 DEFERRED FOREIGN KEY。但是,触发器会在触发时产生干扰。

但是,您还需要考虑您发布的代码。没有 AUTO_INCREMENT 关键字,它是 AUTOINCREMENT(但是您很可能不需要 AUTOINCREMENT,因为 INTEGER PRIMARY KEY 会满足您的所有要求)。

如果您勾选 SQLite AUTOINCREMENT 以及

The AUTOINCREMENT keyword imposes extra CPU, memory, disk space, and disk I/O overhead and should be avoided if not strictly needed. It is usually not needed.

供应商 table 是无用的,因为您已对其进行编码,它只是一个引用商店而没有其他数据的列。然而,Provides table 引用 Supplier table 但引用了一个不存在的列 (cod_supplier)。

编码 CONSTRAINT name REFERENCES table(column(s)) 不遵守 SYNTAX,因为 CONSTRAINT 是 table 级别的子句,而 REFERENCES 是列级别的子句,这似乎会引起一些混淆。

我怀疑您可能已经求助于触发器,因为 FK 冲突没有做任何事情。默认情况下 FK 处理是关闭的,必须根据 Enabling Foreign Key Support 启用。我不认为它们是必需的。

无论如何,我相信以下内容(包括克服上述问题的更改)演示了 DEFERREED FOREIGN KEYS :-

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 INTO Supplier (cod_proveedor) VALUES((SELECT max(cod_shop) + 1 FROM Shop));
  • 即对商店的引用不是现有商店(大于 1)则:-

messages/log 是:-

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

我认为您的数据库设计应该重新考虑,因为 table Provides 代表两组不同的信息:哪个商店提供哪个 material,哪个是某个 material 的供应商。更好的设计应该是将这两种信息分开,这样就可以增加通过外键表达的约束。

这是 table 的草图,未绑定到特定的 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

此约束无法自动强制执行,因为您必须先插入一个 material,然后添加相应的对 (cod_shop、cod_material),然后添加对 (cod_sup, cod_material).为此,我认为最好的选择是在应用程序级别定义一个过程,该过程同时插入 material、可以获得它的商店、它的供应商,以及删除 material 以及 ShopMaterialSupplierMaterial table 中的相关对的过程。