sql 最小基数为1的多对多关系插入行时如何解决死锁?
How to solve deadlock when inserting rows in sql many-to-many relationship with minimum cardinality one restriction?
今年我一直在学习关系数据库以及如何设计它们。为了加强我的知识,我正在尝试使用 Python 和 sqlite3.
设计和实现一个数据库
该数据库是关于一家纺织公司的,除其他外,他们希望保留以下信息:
- Material他们用来制作产品
- 他们寻找 material 的商店
- 一些商店(他们确实购买 material 的商店)被视为供应商。
- 他们想知道什么供应商提供什么materials
关于这最后一段关系,有一些限制:
- 一个供应商可以提供多个material(供应商class最大基数很多)
- 一个material可以由多个供应商提供(Materialclass最大基数很多)
- 所有 material 必须由至少一个供应商提供(Material class 最小基数一个)
- 所有供应商必须至少提供一个material(供应商class最小基数一个)
这就是我认为 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 Material 和 Supplier 添加新行,并且触发器正在工作(或者至少它们不允许我在 Provides table 中插入没有一行的新行)。
这是我陷入僵局的时候:
数据库为空,如果我尝试在 tables Material 或 Supplier[=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 以及 ShopMaterial
和 SupplierMaterial
table 中的相关对的过程。
今年我一直在学习关系数据库以及如何设计它们。为了加强我的知识,我正在尝试使用 Python 和 sqlite3.
设计和实现一个数据库该数据库是关于一家纺织公司的,除其他外,他们希望保留以下信息:
- Material他们用来制作产品
- 他们寻找 material 的商店
- 一些商店(他们确实购买 material 的商店)被视为供应商。
- 他们想知道什么供应商提供什么materials
关于这最后一段关系,有一些限制:
- 一个供应商可以提供多个material(供应商class最大基数很多)
- 一个material可以由多个供应商提供(Materialclass最大基数很多)
- 所有 material 必须由至少一个供应商提供(Material class 最小基数一个)
- 所有供应商必须至少提供一个material(供应商class最小基数一个)
这就是我认为 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 Material 和 Supplier 添加新行,并且触发器正在工作(或者至少它们不允许我在 Provides table 中插入没有一行的新行)。
这是我陷入僵局的时候:
数据库为空,如果我尝试在 tables Material 或 Supplier[=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 以及 ShopMaterial
和 SupplierMaterial
table 中的相关对的过程。