如何正确构建触发器和FK级联更新以共存?

时间:2018-06-06 19:06:39

标签: sql sql-server

我们正在设计一个架构,以便每个" Main"表有一个额外的"批准"最多可容纳一个对象的一个​​额外版本的表,这是该对象最近批准的版本(如果它已被批准的话。)其中一些表具有子对象,这些子对象将外键保存到父表中。更新CASCADE。这是一个最小的例子,附加_A的表格是" Approval"桌子和那些没有它们的是" Main"表:

Schedule
+----+----------+
| Id | Approved |
+----+----------+
|  1 |        0 |
+----+----------+


Schedule_A
+------------+
| ScheduleId |
+------------+
Foreign Key from ScheduleId to Schedule(Id)


ScheduleAssignment
+----+----------+------------+-----------+
| Id | Approved | ScheduleId | ChannelId |
+----+----------+------------+-----------+
|  1 |        0 |          1 |         1 |
|  2 |        0 |          1 |         3 |
|  3 |        0 |          1 |         4 |
+----+----------+------------+-----------+
Composite Foreign Key from (ScheduleId, Approved) to Schedule(Id, Approved) 
with CASCADE UPDATE


ScheduleAssignment_A
+----+------------+-----------+
| Id | ScheduleId | ChannelId |
+----+------------+-----------+
Foreign Key from ScheduleId to Schedule_A(Id)

我们希望在主表(Schedule和ScheduleAssignment)上设置触发器,以便当INSERTUPDATE生成Approved列为1的记录时,值将MERGE' d放入相应的审批表中。

此处的想法是广告合作伙伴可以登录并使用分配创建待处理的时间表,其中使用Approved作为0插入记录。然后,管理员登录系统并批准待处理的Schedule。这会更新Schedule表中的记录,以将Approved设置为1,这将执行以下操作:

  1. Schedule表格上设置触发器,将新批准的记录插入Schedule_A表格
  2. 通过ScheduleAssignmentScheduleIdApproved)上的CASCADE UPDATE,更新ApprovedScheduleAssignment的相关记录值
  3. 作为(2)的结果,在ScheduleAssignment上设置触发器,将新批准的记录插入ScheduleAssignment_A表。
  4. 我遇到的问题是SQL SERVER正按照我收到错误的顺序执行CASCADE UPDATE和触发器:

    The INSERT statement conflicted with the FOREIGN KEY constraint "FK_ScheduleAssignment_A_Schedule". The conflict occurred in database "Sandbox", table "dbo.Schedule_A", column 'Id'.

    大概是CASCADE UPDATE更新了值并在ScheduleAssignment表中触发了触发器,然后在Schedule中设置了触发器,因此ScheduleAssignment_A中的值发生了冲突在Schedule_A中没有父记录。

    有什么方法可以设置这些触发器或外键,以便他们解决而不会遇到这个问题?

    编辑:添加Sql表创建脚本

    CREATE TABLE [dbo].[Schedule](
        [Id] [int] IDENTITY(1,1) NOT NULL,
        [Approved] [bit] NOT NULL,
     CONSTRAINT [PK_Schedule] PRIMARY KEY CLUSTERED 
    (
        [Id] ASC
    )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY],
     CONSTRAINT [UK_ScheduleApproved] UNIQUE NONCLUSTERED 
    (
        [Id] ASC,
        [Approved] ASC
    )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
    ) ON [PRIMARY]
    
    GO
    
    CREATE TRIGGER [dbo].[tr_Schedule_Approved]
    ON [dbo].[Schedule]
    FOR INSERT, UPDATE
    AS
    BEGIN
        INSERT INTO dbo.Schedule_A (Id)
        SELECT Id
        FROM inserted
        WHERE Approved = 1
    END
    
    GO
    
    
    CREATE TABLE [dbo].[Schedule_A](
        [Id] [int] NOT NULL,
     CONSTRAINT [PK_Schedule_A] PRIMARY KEY CLUSTERED 
    (
        [Id] ASC
    )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
    ) ON [PRIMARY]
    
    GO
    
    ALTER TABLE [dbo].[Schedule_A]  WITH CHECK ADD  CONSTRAINT [FK_Schedule_A_Schedule] FOREIGN KEY([Id])
    REFERENCES [dbo].[Schedule] ([Id])
    GO
    
    ALTER TABLE [dbo].[Schedule_A] CHECK CONSTRAINT [FK_Schedule_A_Schedule]
    GO
    
    
    
    CREATE TABLE [dbo].[ScheduleAssignment](
        [Id] [int] IDENTITY(1,1) NOT NULL,
        [Approved] [bit] NOT NULL,
        [ScheduleId] [int] NOT NULL,
        [ChannelId] [int] NOT NULL,
     CONSTRAINT [PK_ScheduleAssignment] PRIMARY KEY CLUSTERED 
    (
        [Id] ASC
    )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
    ) ON [PRIMARY]
    
    GO
    
    ALTER TABLE [dbo].[ScheduleAssignment]  WITH CHECK ADD  CONSTRAINT [FK_ScheduleAssignment_Schedule] FOREIGN KEY([ScheduleId], [Approved])
    REFERENCES [dbo].[Schedule] ([Id], [Approved])
    ON UPDATE CASCADE
    ON DELETE CASCADE
    GO
    
    ALTER TABLE [dbo].[ScheduleAssignment] CHECK CONSTRAINT [FK_ScheduleAssignment_Schedule]
    GO
    
    CREATE TRIGGER [dbo].[tr_ScheduleAssignment_Approved]
    ON [dbo].[ScheduleAssignment]
    FOR INSERT, UPDATE
    AS
    BEGIN
        INSERT INTO dbo.ScheduleAssignment_A (Id, ScheduleId, ChannelId)
        SELECT Id, ScheduleId, ChannelId
        FROM inserted
        WHERE Approved = 1
    END
    
    GO
    
    
    CREATE TABLE [dbo].[ScheduleAssignment_A](
        [Id] [int] NOT NULL,
        [ScheduleId] [int] NOT NULL,
        [ChannelId] [int] NOT NULL,
     CONSTRAINT [PK_ScheduleAssignment_A] PRIMARY KEY CLUSTERED 
    (
        [Id] ASC
    )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
    ) ON [PRIMARY]
    
    GO
    
    ALTER TABLE [dbo].[ScheduleAssignment_A]  WITH CHECK ADD  CONSTRAINT [FK_ScheduleAssignment_A_Schedule] FOREIGN KEY([ScheduleId])
    REFERENCES [dbo].[Schedule_A] ([Id])
    GO
    
    ALTER TABLE [dbo].[ScheduleAssignment_A] CHECK CONSTRAINT [FK_ScheduleAssignment_A_Schedule]
    GO
    

    示例数据:

    INSERT INTO [dbo].[Schedule]
               ([Approved])
         VALUES
               (0)
    
    INSERT INTO [dbo].[ScheduleAssignment]
               ([Approved], ScheduleId, ChannelId)
         VALUES
               (0, SCOPE_IDENTITY(), 5)
               ,(0, SCOPE_IDENTITY(), 6)
               ,(0, SCOPE_IDENTITY(), 7)
               ,(0, SCOPE_IDENTITY(), 8)
               ,(0, SCOPE_IDENTITY(), 9)
    

    从这里,我希望能够做到的是批准这个时间表(及其基础任务),如下所示:

    UPDATE [dbo].[Schedule]
    SET [Approved] = 1
    WHERE Approved = 0
    GO
    

    我希望结果如此:

    Schedule
    +----+----------+
    | Id | Approved |
    +----+----------+
    |  1 |        1 |
    +----+----------+
    
    
    Schedule_A
    +------------+
    | ScheduleId |
    +------------+
    |          1 |
    +------------+
    
    
    ScheduleAssignment
    +----+----------+------------+-----------+
    | Id | Approved | ScheduleId | ChannelId |
    +----+----------+------------+-----------+
    |  1 |        1 |          1 |         5 |
    |  2 |        1 |          1 |         6 |
    |  3 |        1 |          1 |         7 |
    |  4 |        1 |          1 |         8 |
    |  5 |        1 |          1 |         9 |
    +----+----------+------------+-----------+
    
    
    ScheduleAssignment_A
    +----+------------+-----------+
    | Id | ScheduleId | ChannelId |
    +----+------------+-----------+
    |  1 |          1 |         5 |
    |  2 |          1 |         6 |
    |  3 |          1 |         7 |
    |  4 |          1 |         8 |
    |  5 |          1 |         9 |
    +----+------------+-----------+
    

    然而我收到错误:

    The INSERT statement conflicted with the FOREIGN KEY constraint "FK_ScheduleAssignment_A_Schedule". The conflict occurred in database "Sandbox", table "dbo.Schedule_A", column 'Id'.
    

1 个答案:

答案 0 :(得分:0)

最初使用批准的_A表的方法似乎很有吸引力,因为它确实允许简单地查询明显的问题“什么被批准”和“什么是最新的”。

但是,随着数据库的增长,它不能很好地扩展。您可能会发现,随着将其他关系添加到数据库中,您将经常需要创建一个链接到批准表的表和另一个链接到未批准表的表。

反过来,这将导致为已批准和未批准的实体编写几乎相同的查询。就像拥有两个非常相似的数据库一样:一个用于批准的实体,一个用于未批准的数据库,使两件事情并行运行,其难度是使一件事情并行的两倍:-)

我意识到上述陈述没有事实依据,只是我过去几年在处理类似问题时的经验。

我的建议是将“批准”更像是一个州,而不是不同的实体。为了处理具有零ScheduleAssignments的Schedule,该Schedule需要一个“ EditInProgress”标志,并且每个ScheduleAssignment都记录它是否已批准。

因此Schedule和ScheduleAssignment表将为

CREATE TABLE [dbo].[Schedule](
    [Id] [int] NOT NULL,
    [EditInProgress] [bit] NOT NULL,
 CONSTRAINT [PK_Schedule] PRIMARY KEY CLUSTERED 
( [Id] ASC )
) 
GO

CREATE TABLE [dbo].[ScheduleAssignment](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [Approved] [bit] NOT NULL,
    [ScheduleId] [int] NOT NULL,
    [ChannelId] [int] NOT NULL,
 CONSTRAINT [PK_ScheduleAssignment] PRIMARY KEY CLUSTERED 
( [Id] ASC )
)
GO

ALTER TABLE [dbo].[ScheduleAssignment] WITH CHECK ADD CONSTRAINT [FK_ScheduleAssignment_Schedule] 
FOREIGN KEY([ScheduleId])
REFERENCES [dbo].[Schedule] ([Id])
GO

CREATE UNIQUE NONCLUSTERED INDEX [UQ_ScheduleAssignment_1] ON [dbo].[ScheduleAssignment]
(
    [ScheduleId] ASC,
    [Approved] ASC,
    [ChannelId] ASC
)
GO

初始数据加载看起来像这样

INSERT INTO [dbo].[Schedule] ([Id],[EditInProgress]) 
VALUES   (1,0) -- Schedule 1 is not being edited
        ,(2,1) -- Schedule 2 is being edited

INSERT INTO [dbo].[ScheduleAssignment] ([Approved], ScheduleId, ChannelId)
VALUES   (1, 1, 101) -- Schedule 1 has two approved assignments
        ,(1, 1, 102)
        ,(0, 2, 201) -- Schedule 2 has two unapproved assignments
        ,(0, 2, 202)

按时间表执行编辑看起来像

declare @CurrentScheduleId int = 1;

update dbo.Schedule set EditInProgress = 1 where Id = @CurrentScheduleId;

INSERT INTO [dbo].[ScheduleAssignment] ([Approved], ScheduleId, ChannelId)
VALUES   (0, @CurrentScheduleId, 105) 
        ,(0, @CurrentScheduleId, 106)
        ,(0, @CurrentScheduleId, 107)

批准时间表是

declare @CurrentScheduleId int = 1;

if exists (select * from dbo.Schedule where Id = @CurrentScheduleId and EditInProgress = 1) begin

    update dbo.Schedule 
    set EditInProgress = 0 
    where Id = @CurrentScheduleId;

    delete dbo.ScheduleAssignment 
    where ScheduleId = @CurrentScheduleId and Approved = 1;

    update dbo.ScheduleAssignment 
    set Approved = 1 
    where ScheduleId = @CurrentScheduleId;
end
else begin
    raiserror (N'Approval denied for Schedule %d because EditInProgress <> 1',11,0,@CurrentScheduleId)
end;

选择批准的任务和最新任务是:

select 'Approved' as ApprovedQuery, * 
from [dbo].[Schedule]
join [dbo].[ScheduleAssignment] on ScheduleAssignment.ScheduleId = Schedule.Id
where ScheduleAssignment.Approved = 1
order by 2,3,4
GO

select 'Latest' as LatestQuery, * 
from [dbo].[Schedule]
join [dbo].[ScheduleAssignment] on ScheduleAssignment.ScheduleId = Schedule.Id
where [Schedule].EditInProgress = 0 and ScheduleAssignment.Approved = 1 
   or [Schedule].EditInProgress = 1 and ScheduleAssignment.Approved = 0
order by 2,3,4
GO

所有这些都可以在http://www.sqlfiddle.com/#!18/ac135/1上作为SQL提琴手使用

我并不是说以上内容非常适合您的问题,但我希望这是值得探索的方法,以避免过多的桌子。

除非绝对必要,否则我也不建议使用触发器。它们占有一席之地,但是维护起来却令人难以置信,因为在尝试调试现有问题以及添加新功能时,它们经常被忽略。