使用脚本确保Oracle中的数据完整性

时间:2009-07-30 20:07:14

标签: oracle database-design

使用触发器或脚本来维护Oracle不是为了强制实施的数据完整性是不是很糟糕,或者这是一个我正在以糟糕的方式对数据进行建模的标志?

从对上一篇文章(Implementing User Defined Fields)的回复中,我决定要继续使用ClassConcrete继承进行设计。我希望所有SAMPLE都有一个基类,然后是每个唯一属性集的具体表。

虽然我可以通过使SAMPLE主键具有外键约束来强制执行每个具体表在SAMPLE.sample_id中具有父条目。但是,我不知道如何强制SAMPLE条目只有一个子项,因为子条目可以在任意数量的表中。

我该如何强制执行此操作?如果解决方案是INSERT,UPDATE和DELETE触发器,这被认为是不好的做法吗?

8 个答案:

答案 0 :(得分:3)

我认为您可以通过使用物理化视图解决此问题,该视图是主表id上的TABLEA,TABLEB和TABLEC + groub的并集。您必须创建物化视图日志,以使其成为快速可刷新的物化视图。并且您添加了一个检查约束,当每个主表id在实例化视图中有多个行时,该约束会引发错误。

Rob van Wijk在这里http://rwijk.blogspot.com/2009/07/fast-refreshable-materialized-view.html解释了很多关于快速可刷新的mv的问题。 Rob van Wijk也经常出现在stackoverflow上。

在这里,您可以阅读在物化视图上使用检查约束:http://technology.amis.nl/blog/475/introducing-materialized-views-as-mechanism-for-business-rule-implementation-complex-declarative-constraints

使用快速可刷新的mv意味着在提交期间完成完整性检查,而不是在插入或更新数据期间。

我很累,我不能自己测试,我不能提供一个真实的例子。

edit1:以下是示例:

当您使用检查约束和基于唯一函数的索引创建快速刷新mv时,它可以正常工作。

首先我们创建表格:

SQL> create table mastertable (id number(10) not null primary key);

SQL> create table tablea
(id number(10) not null primary key
, master_id number(10) not null references mastertable (id));

SQL> create table tableb
(id number(10) not null primary key
, master_id number(10) not null references mastertable (id));

SQL> create table tablec
(id number(10) not null primary key
, master_id number(10) not null references mastertable (id));

然后我们创建mv日志:

SQL> create materialized view log on tablea with rowid (master_id) 
     including new values;

SQL> create materialized view log on tableb with rowid (master_id) 
     including new values;

SQL> create materialized view log on tablec with rowid (master_id) 
     including new values;

mv(真正需要umarker专栏!):

SQL> create materialized view table_abc
     refresh fast with rowid on commit
     as
     select master_id,count(*) master_count, 'A' umarker
     from   tablea
     group by master_id
     union all
     select master_id,count(*) master_count, 'B' umarker
     from   tableb
     group by master_id
     union all
     select master_id,count(*) master_count, 'C' umarker
     from   tablec
     group by master_id
     /

现在我们为这个mv添加一个检查约束,以确保你不能在每个master_id的同一个详细信息表中插入两次:

SQL> alter table table_abc add check (master_count in (0,1) );

我们为此mv添加了一个基于函数的唯一索引,以确保您不能在具有相同master_id的表a和表b中插入:

SQL> create unique index table_abc_ufbi1 on table_abc
     (case when master_count = 1 then master_id else null end);

测试1(快乐路径):

SQL>插入mastertable值(1);

1 rij是aangemaakt。

SQL>插入表格值(1,1);

1 rij是aangemaakt。

SQL>提交;

提交是voltooid。

测试2(表a中的一个插入和表b中的一个插入具有相同的master_id)

SQL>插入mastertable值(2);

1 rij是aangemaakt。

SQL>插入表格值(2,2);

1 rij是aangemaakt。

SQL>插入tableb值(3,2);

1 rij是aangemaakt。

SQL>承诺; 承诺 * 在regel 1中的FOUT: .ORA-12008:Fout in pad voor vernieuwen van snapshot。 ORA-00001:Schending van UNIQUE-beperking(TESTT.TABLE_ABC_UFBI1)。

测试3(在表格中插入两次,使用相同的master_id)

SQL>插入mastertable值(3);

1 rij是aangemaakt。

SQL>插入表格值(4,3);

1 rij是aangemaakt。

SQL>插入表格值(5,3);

1 rij是aangemaakt。

SQL>承诺; 承诺 * 在regel 1中的FOUT: .ORA-12008:Fout in pad voor vernieuwen van snapshot。 ORA-02290:CHECK-beperking(TESTT.SYS_C0015406)是geschonden。

答案 1 :(得分:2)

假设您的“主”表名为TableA,它的主键称为“ID”。创建第二个表,比如TableB,也使用名为“ID”的主键。现在将TableB(ID)定义为TableA(A)的外键。

将TableB(ID)作为外键意味着它只能具有TableA(ID)中存在的值,并且将其作为主键意味着它不能具有多于一次的值。

答案 2 :(得分:1)

如果数据只是由您自己的存储过程修改过,那么我就不会费心去检查这个约束了。

事实上,现在我想起来了,没有必要检查INSERT案例。您在同一事务中插入了SAMPLE和CONCRETE_1行。不能有CONCRETE_2行具有相同的PK,因为SAMPLE行以前不存在。

答案 3 :(得分:1)

不能依赖触发器来强制执行完整性。

Tom Kyte解释原因:http://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:599808600346047256

问题是交易无法看到其他未经通信的交易正在做什么。

举一个简单的例子,事务A和B都可以打算在表中插入相同的值“X”,并且都可以检查这样的值是否已经存在。事务A在事务B检查现有值“X”后提交。事务B找不到X,因此它也插入自己的X和提交。现在唯一性要求被打败了。

避免这种情况的唯一方法是检查现有值,插入并提交插入序列化的整个过程。

除此之外,触发器无法看到它触发的表内容的问题,因为它正在变异......

答案 4 :(得分:1)

我认为在这种特殊情况下,改变你的数据库模型,而不是创建脚本或触发器,就是答案。

你在之前的评论中提到过:

  

好吧,我有TABLE然后TABLE_A,TABLE_B和TABLE_C。 TABLE中的每个条目必须在EITHER TABLE_A,TABLE_B或TABLE_C中具有完全一个条目。 A,B,C上的FK约束只有一半。然后,表A和表B都可以具有相同的父(我不想要)。

我会建议:

  1. TABLE 中创建 isABC 列,指定类型为A,B或C.更好的是,创建一个新的表,其中PK(主键)为A和新列 isABC (在 TABLE.PK 上使用相同的PK和FK)。
  2. 将(PK, isABC )设置为唯一约束。
  3. 将(PK, isABC )列添加到 TABLE_A TABLE_B TABLE_C 。使其成为 TABLE (或新表)中相同列的FK(外键)约束。
  4. 在每个 TABLE_A TABLE_B TABLE_C 上,在列 isABC 上设置检查约束,检查值分别为“A”,“B”和“C”。
  5. 此设计在 TABLE_A TABLE_B TABLE_C 中创建了一个额外的 isABC 列,作为强制执行此操作的价格约束,但您可以使用脚本,触发器或过程摆脱毛茸茸的实现。

答案 5 :(得分:0)

嗯,这取决于您正在寻找的完整性。

数据库专为参照完整性而设计。因此,如果您正在寻找,请使用数据库的结构。不要自己动手。

如果您尝试维护其他类型的完整性(例如行的MAC),则触发器完全可以接受。

答案 6 :(得分:0)

在对这里的一些答案进行评论之后,我觉得有必要展示一个示例,说明如何使用触发器来执行超出RDBMS提供的基本RI的规则。

从我们自己的系统中,我们有几个表都有指向同一个表的外键(“MasterTable”)。随着时间的推移,这些表中的行将被删除;删除最后一个子行后,我们要删除父行并执行一些操作。我们通过在父表上创建一个“ChildCount”列来强制执行此规则,该列指示引用此表的行数(换句话说,引用计数)。

在所有各种子表的insert触发器中,我们的代码如下所示:

      SELECT ChildCount
        INTO numrows
        FROM MasterTable mt
       WHERE :new.MasterTableId = MasterTable.MasterTableId
         FOR UPDATE;

      UPDATE MasterTable
         SET ChildCount = ChildCount + 1
       WHERE :new.MasterTableId = MasterTable.MasterTableId;

在子表的删除触发器中,我们有:

      SELECT ChildCount
        INTO numrows
        FROM MasterTable
       WHERE :old.MasterTableId = MasterTable.MasterTableId
         FOR UPDATE;

      DELETE MasterTable
       WHERE ChildCount = 1
         AND :old.MasterTableId = MasterTable.MasterTableId;

    IF Sql%RowCount = 0 THEN
         UPDATE MasterTable
            SET ChildCount = ChildCount - 1
          WHERE :old.MasterTableId = MasterTable.MasterTableId;
    END IF;

更新触发器包含两位代码。

此逻辑中的关键位是使用带有FOR UPDATE子句的单独select语句,而不是仅使用单个语句更新列。它确保同步事务将被正确序列化。

由于MasterTable已经向所有子表声明了删除级联规则,因此上面的代码在删除具有现有子项的MasterTable行的上下文中执行时会导致ORA-04091(变异表)错误,因此这些语句在EXCEPTION块的上下文中完成,该块捕获并忽略此错误。

最后,上面的代码是从我们用于数据建模的ERE工具(ERWin)自动生成的。 ERWin允许您创建“用户定义属性”(UDP),它有一种宏语言,可用于根据您的模式生成几乎所需的任何代码,因此我们需要做的就是添加ChildCount列到相应的父表,并将“Ref-Counted Relationship”的UDP设置为true。

正如我在上面的评论中指出的那样,触发器不能用于完全替换声明的RI,因为你不能使用FOR UPDATE来使删除级联规则正常工作。但这对于像这样的补充规则来说非常棒。

注意:此代码已经生产了11年 - 它是在我们仍在使用Oracle 7时设计的。如果有人使用内置的Oracle功能有更现代的方法,我会感兴趣在听到它。

答案 7 :(得分:0)

确保在没有子项的情况下无法插入父记录的最佳方法是使用INSERT ALL语法。这允许我们在同一个语句中将记录插入到多个表中。

    INSERT ALL
        INTO parent
             (pk_col, val1, val2)
        INTO child1 
             (pk_col, val3, val4)
    SELECT some_seq.nextval as pk_col
           , val1
           , val2
           , val3
           , val4
    FROM where_ever

表WHERE_EVER可以是临时表(可能是外部表)。在你的情况下,它将是DUAL,VAL列是存储过程签名的参数。

如果没有CHILD n 记录,您将无法阻止流氓开发人员编写插入PARENT记录的代码。同样,您无法使用已拥有CHILD1记录的PARENT的主键停止将记录插入CHILD2。为此,我担心你需要代码审查。