【问题标题】:How to prevent entering value in table when primary is used on other table?当primary用于其他表时,如何防止在表中输入值?
【发布时间】:2020-06-20 03:09:36
【问题描述】:

我必须构建和填充超类型-子类型关系,但我无法让它按应有的方式工作。基本上 PERSON 表是表 STUDENT 和 TEACHER(子类型)的超类型。但是一个人可以是学生也可以是老师。

属性:

PERSON(p_id, name, dob)

学生(s_id、p_id、年级)

教师(t_id,p_id,电话)


学生和老师都应该有姓名和出生日期以及作为外键的 p_id,但如果它存在于一个表上,则不应在另一个表上

CREATE TABLE PERSON ( -- SUPERTYPE
    p_id NUMBER(2) CONSTRAINT c1 PRIMARY KEY,
    name CHAR(15),
    dob DATE
);

CREATE TABLE STUDENT ( -- SUBTYPE
    s_id NUMBER(2) CONSTRAINT c2 PRIMARY KEY,
    p_id_fk,
    grade CHAR(1),
    FOREIGN KEY (p_id_fk) REFERENCING PERSON (p_id)
);

CREATE TABLE TEACHER( -- SUBTYPE
    t_id NUMBER(4) CONSTRAINT c3 PRIMARY KEY,
    p_id_fk,
    tel CHAR(8),
    FOREIGN KEY (p_id_fk) REFERENCING PERSON (p_id)
);

INSERT INTO PERSON VALUES (11, 'John', to_date('12/12/12', 'dd/mm/yy'));
INSERT INTO PERSON VALUES (22, 'Maria', to_date('01/01/01', 'dd/mm/yy'));
INSERT INTO PERSON VALUES (33, 'Philip', to_date('02/02/02', 'dd/mm/yy'));

INSERT INTO STUDENT VALUES (98, 11, 'A');

INSERT INTO TEACHER VALUES (1234, 11, 14809510);

如何防止 Person 11 (John) 出现在两个表中?

【问题讨论】:

  • 这看起来像 Oracle SQL。你真的在使用 sql-server(这个脚本会失败)吗?
  • 我忘记在上面的案例中输入 VALUES 了,但现在已经更正了。绝对适合我,但问题仍然存在。 SQL-SERVER SCREENSHOT
  • sql-server 中没有to_date()。您的屏幕截图看起来像 Oracle 的 sql*plus 命令行实用程序。我正在标记您的问题oracle,请随时回滚。
  • 好电话,我不知道 sql-server 和 sql*plus 之间有什么区别。
  • 您使用的术语表明您可能会使用对象而不是普通表。准确地包括(在问题中)你被要求做的事情 - 以及为什么你认为你所拥有的东西是错误的,这可能会有所帮助。

标签: sql oracle inheritance subtype supertype


【解决方案1】:

一种选择是使用数据库触发器,每个表一个(STUDENT 和TEACHER);它们看起来一样:

触发STUDENT:

SQL> create or replace trigger trg_bi_stu
  2    before insert on student
  3    for each row
  4  declare
  5    l_cnt number;
  6  begin
  7    -- inserting into STUDENT: check whether that person exists in TEACHER table
  8    select count(*)
  9      into l_cnt
 10      from teacher
 11      where p_id_fk = :new.p_id_fk;
 12
 13    if l_cnt > 0 then
 14       raise_application_error(-20001, 'That person is a teacher; can not be a student');
 15    end if;
 16  end;
 17  /

Trigger created.

触发TEACHER:

SQL> create or replace trigger trg_bi_tea
  2    before insert on teacher
  3    for each row
  4  declare
  5    l_cnt number;
  6  begin
  7    -- inserting into TEACHER: check whether that person exists in STUDENT table
  8    select count(*)
  9      into l_cnt
 10      from student
 11      where p_id_fk = :new.p_id_fk;
 12
 13    if l_cnt > 0 then
 14       raise_application_error(-20001, 'That person is a student; can not be a teacher');
 15    end if;
 16  end;
 17  /

Trigger created.

SQL>

测试:

SQL> INSERT INTO STUDENT VALUES (98, 11, 'A');

1 row created.

SQL>
SQL> INSERT INTO TEACHER VALUES (1234, 11, 14809510);
INSERT INTO TEACHER VALUES (1234, 11, 14809510)
            *
ERROR at line 1:
ORA-20001: That person is a student; can not be a teacher
ORA-06512: at "SCOTT.TRG_BI_TEA", line 11
ORA-04088: error during execution of trigger 'SCOTT.TRG_BI_TEA'


SQL>

【讨论】:

  • 哦,汤姆不会对我写的大量代码感到好笑。尽管您发布了一个提示(“并发插入”),但请 - 还有另一个提示 - 并发插入呢,@Marmite?
  • 对我删除的公式感到抱歉。我的观点是 1) 触发器是在数据库中强制执行约束的 错误 方式。 2)正确的做法是根据类表继承的标准映射方式来改变DDL。
  • @Littlefoot - re。 “并发插入”:插入人员并提交。然后一个会话插入学生;另一个会话插入具有相同人员 ID 的教师。两个触发器都没有看到另一个会话的未提交数据,因此两者都计数为零并继续,并且都可以提交,然后该人出现在两个表中。
  • 啊哈!现在我明白了,谢谢@Alex 和 Marmite。
【解决方案2】:

存在四种方式如何将继承映射到关系数据库。

您正在使用最奇特的第三个选项,这是导致问题的原因。考虑切换到其他选项之一作为解决方案。

前三个很好理解并记录在案,page 提供了有用的链接。

基本上可以映射

1) 一张表中的所有类

2) 为所有具体类使用一张表

3) 为 aech 类定义一张表

所有这些选项都存在一些问题,无论是过度连接还是禁用约束(例如,您不能在选项 1 中定义不可为空的列),因此为了完整性,选项 4)是 不要在关系数据库中使用继承。

您已尝试实现选项 3),但问题是所有表都必须继承相同的主键(以强制 1:1 关系)并将此 主键用作 外键。

这里是您示例的所有选项的概述

-- 1) single table
CREATE TABLE PERSON (  
    p_id NUMBER(2) CONSTRAINT pers_pk PRIMARY KEY,
    name CHAR(15),
    dob DATE,
    grade CHAR(1),
    tel CHAR(8),
    person_type VARCHAR2(10) CONSTRAINT pers_type CHECK  (person_type in ('STUDENT','TEACHER'))
);

-- 2) table per concrete class
CREATE TABLE STUDENT (  
    p_id NUMBER(2) CONSTRAINT stud_pk PRIMARY KEY,
    name CHAR(15),
    dob DATE,
    grade CHAR(1) 
);

CREATE TABLE TEACHER(  
    p_id NUMBER(2) CONSTRAINT tech_pk PRIMARY KEY,
    name CHAR(15),
    dob DATE,
    tel CHAR(8)
);

-- 3) table per class
CREATE TABLE PERSON (  
    p_id NUMBER(2) CONSTRAINT pers_pk PRIMARY KEY,
    name CHAR(15),
    dob DATE
);

CREATE TABLE STUDENT (  
    p_id NUMBER(2) CONSTRAINT stud_pk PRIMARY KEY,
    grade CHAR(1),
    FOREIGN KEY (p_id) REFERENCING PERSON (p_id)
);

CREATE TABLE TEACHER( 
    p_id NUMBER(2) CONSTRAINT tech_pk PRIMARY KEY,
    tel CHAR(8),
    FOREIGN KEY (p_id) REFERENCING PERSON (p_id)
);

INSERT INTO PERSON (P_ID, NAME, DOB) VALUES (11, 'John', to_date('12/12/2012', 'dd/mm/yyyy'));
INSERT INTO PERSON (P_ID, NAME, DOB) VALUES (22, 'Maria', to_date('01/01/2001', 'dd/mm/yyyy'));
INSERT INTO PERSON (P_ID, NAME, DOB) VALUES (33, 'Philip', to_date('02/02/2002', 'dd/mm/yyyy'));

INSERT INTO STUDENT (P_ID, GRADE) VALUES (11, 'A');
INSERT INTO TEACHER (P_ID, TEL) VALUES (11, 14809510);

【讨论】:

  • 问题仍然存在我尝试使用您的建议,但它对我不起作用。在学生和教师表中添加“约翰”有什么意义?它不应该接受它。
  • @LoizosVasileiou 好吧,老师也可能是学生(在其他课程中)。如果您想禁用它,请使用选项 1) 并添加一个定义“person_type”的 discriminant 列或选项 2) 使用从单个序列分配的键,这将确保表具有没有分离键。
【解决方案3】:

使用具有适当约束的物化视图来检查诸如此类的复杂需求。

例如,让我们创建表和物化视图,将数据添加到表中,然后刷新 MV:

CREATE TABLE PERSON ( -- SUPERTYPE
    p_id NUMBER(2) CONSTRAINT c1 PRIMARY KEY,
    name CHAR(15),
    dob DATE
);

CREATE TABLE STUDENT ( -- SUBTYPE
    s_id NUMBER(2) CONSTRAINT c2 PRIMARY KEY,
    p_id_fk,
    grade CHAR(1),
    FOREIGN KEY (p_id_fk) REFERENCING PERSON (p_id)
);

CREATE TABLE TEACHER( -- SUBTYPE
    t_id NUMBER(4) CONSTRAINT c3 PRIMARY KEY,
    p_id_fk,
    tel CHAR(8),
    FOREIGN KEY (p_id_fk) REFERENCING PERSON (p_id)
);

CREATE MATERIALIZED VIEW PERSON_MV
  REFRESH COMPLETE
  AS SELECT p.P_ID,
            s.S_ID,
            t.T_ID
       FROM PERSON p
       LEFT OUTER JOIN STUDENT s
         ON s.P_ID_FK = p.P_ID
       LEFT OUTER JOIN TEACHER t
         ON t.P_ID_FK = p.P_ID;

-- Add constraint to the table underlying the MV

ALTER MATERIALIZED VIEW PERSON_MV
  ADD CONSTRAINT PERSON_MV_CK1
    CHECK( (S_ID IS NULL AND       -- either both are NULL
            T_ID IS NULL) OR
           ( (S_ID IS NULL OR      -- or only one is NULL
              T_ID IS NULL) AND
             (S_ID IS NOT NULL OR
              T_ID IS NOT NULL)));

INSERT ALL
  INTO PERSON (P_ID, NAME, DOB) VALUES (11, 'John', to_date('12/12/2012', 'dd/mm/yyyy'))
  INTO PERSON (P_ID, NAME, DOB) VALUES (22, 'Maria', to_date('01/01/2001', 'dd/mm/yyyy'))
  INTO PERSON (P_ID, NAME, DOB) VALUES (33, 'Philip', to_date('02/02/2002', 'dd/mm/yyyy'))
SELECT * FROM DUAL;

COMMIT;

INSERT INTO STUDENT VALUES (98, 11, 'A');

COMMIT;

BEGIN
  DBMS_MVIEW.REFRESH('PERSON_MV', 'C', '', TRUE, FALSE, 0, 0, 0, FALSE, FALSE);
END;
/

SELECT *
  FROM PERSON_MV;

注意添加到物化视图的约束:

ALTER MATERIALIZED VIEW PERSON_MV
  ADD CONSTRAINT PERSON_MV_CK1
    CHECK( (S_ID IS NULL AND       -- either both are NULL
            T_ID IS NULL) OR
           ( (S_ID IS NULL OR      -- or only one is NULL
              T_ID IS NULL) AND
             (S_ID IS NOT NULL OR
              T_ID IS NOT NULL)));

此约束允许数据存在于:

  • 存在 PERSON 行,但不存在相关的 STUDENT 或 TEACHER 行
  • PERSON 行与任何一个相关的 STUDENT 或 TEACHER 行存在,但不能同时存在

所以当我们从物化视图中执行最终的 SELECT 时,我们得到:

P_ID  S_ID  T_ID
11     98    - 
33     -     - 
22     -     - 

现在让我们修改上面的脚本,在INSERT INTO STUDENT之后添加以下内容:

INSERT INTO TEACHER VALUES (1234, 11, 14809510);

COMMIT;

如果我们重新运行整个脚本,我们发现当调用 DBMS_MVIEW.REFRESH 刷新我们得到的物化视图时:

ORA-12008: error in materialized view or zonemap refresh path ORA-06512: at "SYS.DBMS_SNAPSHOT_KKXRCA", line 3012
ORA-06512: at "SYS.DBMS_SNAPSHOT_KKXRCA", line 2424
ORA-06512: at "SYS.DBMS_SNAPSHOT_KKXRCA", line 88
ORA-06512: at "SYS.DBMS_SNAPSHOT_KKXRCA", line 253
ORA-06512: at "SYS.DBMS_SNAPSHOT_KKXRCA", line 2405
ORA-06512: at "SYS.DBMS_SNAPSHOT_KKXRCA", line 2968
ORA-06512: at "SYS.DBMS_SNAPSHOT_KKXRCA", line 3255
ORA-06512: at "SYS.DBMS_SNAPSHOT_KKXRCA", line 3287
ORA-06512: at "SYS.DBMS_SNAPSHOT", line 16
ORA-06512: at line 2
ORA-06512: at "SYS.DBMS_SQL", line 1721

这是甲骨文相当冗长的说法,即违反了约束。

See this LiveSQL Oracle session

【讨论】:

  • 这是一个不错的方法,+1。但问题是它仅在您刷新 MV 时检测到重复之后。它不像使用触发器的规范方法那样防止重复。
  • 我对物化视图并不像我希望/应该的那样熟悉,但我相信您可以设置 MV,以便它在提交时刷新,以便随后检测到问题。但我不知道提交是否会正常完成,然后 MV 刷新会抱怨或什么。不过,这是一个想法。
【解决方案4】:

继@Bob 的物化视图方法之后,在提交时捕获问题的版本可能如下所示:

CREATE MATERIALIZED VIEW LOG ON STUDENT WITH PRIMARY KEY, ROWID;

CREATE MATERIALIZED VIEW LOG ON TEACHER WITH PRIMARY KEY, ROWID;

CREATE MATERIALIZED VIEW PERSON_HACK (p_id_fk, marker, rid) 
BUILD IMMEDIATE 
REFRESH ON COMMIT AS 
SELECT p_id_fk, 1, ROWID FROM STUDENT 
UNION ALL 
SELECT p_id_fk, 2, ROWID FROM TEACHER;

ALTER MATERIALIZED VIEW PERSON_HACK 
ADD CONSTRAINT PERSON_HACK_PK PRIMARY KEY (p_id_fk);

这应该类似于延迟约束,在提交时刷新 MV(并且违反其主键)时出错。使用并发插入,第二个要提交的会话会看到错误。

Live SQL,尽管 MV 似乎有一些问题(在别处报告)。它应该报告约束违规,而不是抛出 ORA-12008。但我目前无法在其他地方进行测试 - SQL Fiddle 或 dbfiddle 都不允许创建 MV。


当然,如果您还没有学过 MV,那么在作业中使用 MV 可能看起来有点奇怪。似乎更有可能你已经被教导过对象并且作业期待那些 - 即使它们在现实世界中(在数据库中)很少使用,它们似乎无论如何都被教导,以及旧的连接语法和其他不良做法...

【讨论】:

    猜你喜欢
    • 2016-05-11
    • 2022-01-20
    • 2021-12-05
    • 1970-01-01
    • 2019-05-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多