在数据库中强制执行表间约束的最佳方法

时间:2009-03-17 12:52:10

标签: constraints database-agnostic

我正在寻找检查表间约束的最佳方法。例如,检查日期子记录值是否在两个父行列的范围日期之间。例如:

Parent table  
ID    DATE_MIN   DATE_MAX
----- ---------- ----------
1     01/01/2009 01/03/2009
...

Child table
PARENT_ID  DATE
---------- ----------
1          01/02/2009
1          01/12/2009   <--- HAVE TO FAIL!
...

我看到两种方法:

  • this article(或其他RDBMS上的其他等效文件)所示,在提交时创建实体化视图。
  • 使用存储过程和触发器。

还有其他方法吗?哪个是最佳选择?

更新:此问题的动机是关于“将限制放在数据库或应用程序上”。我认为这是一个疲惫的问题,任何人都会以她喜欢的方式行事。而且,对于批评者我很抱歉,我正在开发数据库的限制。 从这里,问题是“哪个是管理数据库的表间约束的最佳选择?”。我在问题标题上添加了“内部数据库”。

更新2 :有人添加了“ oracle ”标记。当然,物化视图是oracle-tools,但我对任何选项感兴趣,无论它是在oracle还是其他RDBMS上。

4 个答案:

答案 0 :(得分:4)

编辑: Dana the Sane删除了他的帖子,无论DBA异议,都将其放入数据层。


DBA对像Dana这样的开发人员尖叫的原因是他们认为应用程序和数据库之间的比例为1:1。他们看到了这一点,因为他们看到数据支持应用程序,他们的应用程序只需要将数据存储在一个地方。

DBA将数据视为最重要的事情,并不关心应用程序是否出现。

如果您丢失了对MS Word的使用,您是否仍然关心您是否还能找到您的文件?不,数据对您很重要,应用程序不是。

如果你曾经绕过你的应用程序,为了获取你的数据,你已经失去了数据层中的约束。如果你的约束在数据库层中,那么几十个应用程序都可以使用它。

理想情况下,您永远不会向任何人授予INSERT,UPDATE或DELETE。相反,您将授予EXECUTE On软件包以便为您执行CRUD。如果你从一开始就这样做,那么将规则添加到CHILD的INSERT(比如检查出生是否在父日期之间)的能力实际上是无限的。

答案 1 :(得分:2)

数据库约束

强制执行数据库约束的最佳方法(跨越两个或更多关系的约束 - 其中参照完整性约束是具有语法速记的特定情况, foreign key / references语句将通过标准SQL语句声明性地

create assertion <name> check (<condition>)

在您的情况下,例如:

create assertion Child_DATE_between_MIN_MAX check (
  not exists (
    select DATE_MIN, DATE, DATE_MAX
      from Parent, Child
     where ID = PARENT_ID
       and DATE < DATE_MIN
       and DATE > DATE_MAX
  )
)

更新:我忘了<condition>严格来说是布尔值,因此旧代码不正确。

不幸的是(这里适度的讽刺)大多数SQL-DBMS都没有实现ASSERTION。

因此,可以使用存储过程和触发器或检查约束(如果可用)来执行此检查程序性。在这种情况下,需要调用相同的存储过程来更新父和子关系;所以一个程序和两个触发器或检查约束。

Lurker Indeed's answer显示了这样的解决方案,但它需要对Child关系进行类似的检查。

对表演的担忧

Damien_The_Unbeliever在对the same answer的评论中辩称:

  

1)你可能会发生充分的事   每次插入/更新的表扫描

在这里,我将解决这个异议,因为它很常见,即使对于ASSERTION也似乎有效(很可能这是一种流行的误解,说服用户不要求SQL-DBMS实现者关于它们,即使他们知道它符合标准。

嗯,是的,他是对的..如果一个人使用的DBMS很糟糕!

有一个有趣的理论认为可以应用于完整性维护:Differential Relational Calculus(以.pdf here的形式提供;您还可以在每本关于数据库的书籍中找到对该主题的充分处理理论)。

核心思想是,可以强制执行完整性约束,通常只检查更新所涉及的关系子集。更严格地引用链接论文的摘要:

  

......一阶的正式分化   句子在维护中很有用   数据库完整性,自从一次   数据库约束表示为a   一阶句,其衍生词   关于交易提供   必要和充分的条件   保持诚信。的的   衍生物通常要简单得多   测试比原始约束   因为它保持完整性   通过假设完整性来差异化   在交易之前,并进行测试   仅适用于新的违规行为。 ...

还有其他技术可以处理增量完整性约束维护。 DBMS开发人员没有充分理由忽视这种理论。事实上,An Amateur's Introduction to Integrity Constraints and Integrity Checking in SQL.pdf)的作者在引言中写道:

  

1简介

     

...商业关系DBMS产品   然而,支持SQL(例如,   Oracle [Ora99]或DB2 [IBM99])没有   支持更高级的表单   约束。即使在今天,超过8个   SQL'92标准之后的几年   已发布,这些广告都没有   系统支持断言,最多   SQL中一般的约束形式!   科学文献,即研究   论文,致力于诚信   另一方面提供了丰富的财富   有希望的结果非常适用   一般和强大的形式   完整性约束。 ...

所以,请:请求您的SQL-DBMS供应商(商业或免费/开源)实施ASSERTION 现在并且至少具有合理的性能。

答案 2 :(得分:1)

我会去存储过程并触发路由;他们的主要目的之一是确保数据库级别的数据完整性。

大多数数据库也有某种形式的检查约束,其中几乎任何可以放在WHERE子句中的东西都可以用作对数据的检查:

CREATE FUNCTION CheckFnctn()
RETURNS int
AS 
BEGIN
   DECLARE @retval int
   SELECT @retval = COUNT(*) 
   FROM PARENT
   INNER JOIN CHILD ON PARENT.ID = CHILD.PARENT_ID
   WHERE CHILD.DATE < PARENT.DATE_MIN OR CHILD.DATE > PARENT.DATE_MAX
   RETURN @retval
END;
GO
ALTER TABLE CHILD
ADD CONSTRAINT chkDates CHECK (dbo.CheckFnctn() = 0 );
GO

答案 3 :(得分:0)

好的,在具体的例子中,我会去冗余存储冗余数据。通过CHECK和FK(以及超级键)的组合,我们确保数据始终是正确的,然后我们包装一个视图并触发它以隐藏实现细节:

create table dbo.Parents (
    ParentID int IDENTITY(1,1) not null,
    ValidFrom datetime not null,
    ValidTo datetime not null,
    /* Natural Key column(s) */
    CONSTRAINT PK_dbo_Parents PRIMARY KEY (ParentID),
    CONSTRAINT UQ_dbo_Parents_DRI UNIQUE (ParentID, ValidFrom, ValidTo),
    /* Unique constraint on Natural Key */
    CONSTRAINT CK_dbo_Parents_ValidDates CHECK (ValidFrom <= ValidTo) /* Semi-open interval */
)
go
alter table dbo.Parents add constraint DF_dbo_Parents_ValidFrom DEFAULT (CURRENT_TIMESTAMP) for ValidFrom
go
alter table dbo.Parents add constraint DF_dbo_Parents_ValidTo DEFAULT (CONVERT(datetime,'99991231')) for ValidTo
go
create table dbo._Children (
    ChildID int IDENTITY(1,1) not null, /* We'll need this in the update trigger */
    ParentID int not null,
    ChildDate datetime not null,
    _ParentValidFrom datetime not null,
    _ParentValidTo datetime not null,
    CONSTRAINT PK_dbo__Children PRIMARY KEY (ChildID),
    CONSTRAINT FK_dbo__Children_Parents FOREIGN KEY (ParentID,_ParentValidFrom,_ParentValidTo) REFERENCES dbo.Parents (ParentID,ValidFrom,ValidTo) ON UPDATE CASCADE,
    CONSTRAINT CK_dbo__Children_ValidDate CHECK (_ParentValidFrom <= ChildDate and ChildDate < _ParentValidTo) /* See, semi-open */
)
go
alter table dbo._Children add constraint DF_dbo__Children_ChildDate DEFAULT (CURRENT_TIMESTAMP) for ChildDate
go
create view dbo.Children (ChildID,ParentID,ChildDate)
with schemabinding
as
select ChildID,ParentID,ChildDate from dbo._Children
go
create trigger dbo.T_Children_I on dbo.Children instead of insert
as
begin
    set nocount on

    insert into dbo._Children (ParentID,ChildDate,_ParentValidFrom,_ParentValidTo)
    select i.ParentID,i.ChildDate,p.ValidFrom,p.ValidTo
    from
        inserted i
            inner join
        dbo.Parents p
            on
                i.ParentID = p.ParentID
end
go
create trigger dbo.T_Children_U on dbo.Children instead of update
as
begin
    set nocount on
    if UPDATE(ChildID)
    begin
        RAISERROR('Updates to ChildID are not allowed',16,1)
        return
    end

    update c
    set
        ParentID = i.ParentID,
        ChildDate = i.ChildDate,
        _ParentValidFrom = p.ValidFrom,
        _ParentValidTo = p.ValidTo
    from
        inserted i
            inner join
        dbo._Children c
            on
                i.ChildID = c.ChildID
            inner join
        dbo.Parents p
            on
                i.ParentID = p.ParentID
end
go
insert into dbo.Parents(ValidFrom,ValidTo)
select '20081201','20090101' union all
select '20090201','20090301'
/* (2 row(s) affected) */
go
insert into dbo.Children (ParentID,ChildDate)
select 1,'20081215'
/* (1 row(s) affected) */
go
insert into dbo.Children (ParentID,ChildDate)
select 1,'20090115'
/*
Msg 547, Level 16, State 0, Procedure T_Children_I, Line 6
The INSERT statement conflicted with the CHECK constraint "CK_dbo__Children_ValidDate". The conflict occurred in database "Play", table "dbo._Children".
The statement has been terminated.
*/
go
update dbo.Parents set ValidTo = '20090201' where ParentID = 1
/* (1 row(s) affected) */
go
insert into dbo.Children (ParentID,ChildDate)
select 1,'20090115'
/* (1 row(s) affected) */
go
update dbo.Parents set ValidTo = '20090101' where ParentID = 1
/*
Msg 547, Level 16, State 0, Line 1
The UPDATE statement conflicted with the CHECK constraint "CK_dbo__Children_ValidDate". The conflict occurred in database "Play", table "dbo._Children".
The statement has been terminated.
*/
go
insert into dbo.Children (ParentID,ChildDate)
select 2,'20090215'
/* (1 row(s) affected) */
go
update dbo.Children set ChildDate = '20090115' where ParentID=2 and ChildDate = '20090215'
/*
Msg 547, Level 16, State 0, Procedure T_Children_U, Line 11
The UPDATE statement conflicted with the CHECK constraint "CK_dbo__Children_ValidDate". The conflict occurred in database "Play", table "dbo._Children".
The statement has been terminated.
*/
go
delete from dbo.Children
/* (3 row(s) affected) */
go
/* Clean up after testing */
drop view dbo.Children
drop table dbo._Children
drop table dbo.Parents
go

这适用于SQL Server。在2005年进行了测试,但至少应该在2000年和2008年进行测试。这里的一个好处是即使触发器被禁用(例如,嵌套触发器被关闭),你也不能在基表中找到错误的数据