4

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

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!
...

我看到两种方法:

  • 如本文(或其他 RDBMS 上的其他等效项)所示,在提交时创建物化视图。
  • 使用存储过程和触发器。

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

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

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

4

4 回答 4

4

编辑: Dana the Sane 删除了他的帖子,无论 DBA 反对如何,都将其放在数据层中。


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

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

如果您失去了对 MS Word 的使用,您是否会关心您是否仍然可以访问您的文档?不,数据对您很重要,应用程序不重要。

如果您让任何东西绕过您的应用程序来获取您的数据,那么您就失去了数据层中的约束。如果你的约束在你的数据库层,那么几十个应用程序都可以让你使用它。

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

于 2009-03-17T14:48:35.747 回答
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 没有实现断言。

因此,如果可用的话,剩下的就是通过存储过程和触发器或检查约束以程序方式实现此检查。在这种情况下,需要调用相同的存储过程来更新父子关系;所以一个过程和两个触发器或检查约束。

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

对表演的担忧

Damien_The_Unbeliever 在对同一答案的评论中认为:

1)您可能会为每次插入/更新进行全表扫描

在这里,我将解决这个反对意见,因为它很常见,甚至对于 ASSERTION 来说似乎也是有效的(这很可能是一种流行的误解,它说服用户不要向 SQL-DBMS 实现者询问它们,即使他们知道它在标准)。

好吧,是的,他是对的.. 如果有人使用糟糕的 DBMS!

有一个有趣的理论可以应用于完整性维护:微分关系微积分(在此处提供 .pdf 文件;您还可以在每本关于 DB 理论的体面书籍中找到对该主题的充分处理)。

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

... 一阶语句的形式微分对于维护数据库完整性很有用,因为一旦数据库约束被表示为一阶语句,它相对于事务的导数就为维护完整性提供了必要和充分的条件。导数通常比原始约束更容易测试,因为它通过在事务之前假设完整性来保持完整性,并且只测试新的违规行为。...

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

1 简介

... 但是,支持 SQL 的商业关系 DBMS 产品(例如,Oracle [Ora99] 或 DB2 [IBM99])不支持更高级的约束形式。即使在 SQL'92 标准发布 8 年多的今天,这些商业系统都不支持断言,这是 SQL 中最普遍的约束形式! 另一方面,致力于完整性的科学文献,即研究论文,提供了大量有希望的结果,适用于非常普遍和强大的完整性约束形式。...

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

于 2010-03-25T07:59:53.033 回答
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
于 2009-03-17T15:37:45.787 回答
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 年工作。这里的一个好处是,即使触发器被禁用(例如嵌套触发器被关闭),你也不能在基表中得到错误的数据

于 2009-03-17T17:25:14.653 回答