Каскад с несколькими внешними ключами в таблице mssql

Я уверен, что это возможно, но для жизни я не могу понять это.

То, что я создал, представляет собой таблицу MSSQL истории пользователей для хранения изменений, внесенных пользователем и кем. Эта таблица содержит два внешних ключа, которые ссылаются на мою другую таблицу (Пользовательскую) — один fkey для затронутого пользователя, а другой fkey для пользователя, вносящего изменения.

Мне нужно, чтобы любые изменения в таблице (User) каскадировались и обновляли соответствующие записи в этой новой таблице.

Поля в новой таблице (User_History) следующие (каждый пользователь идентифицируется двумя полями):

Affected_User_House_Id  - int
Affected_User_Id - int
Modified_By_User_House_Id - int
Modified_By_User_Id – int
Modification_Date - datetime
ModificationMade - ntext

Каждое поле является первичным ключом, кроме «ModificationMade». Поле «Modification_Date» имеет точность до 1 секунды. Проблема, с которой я сталкиваюсь, заключается в создании указанного каскада. Я попытался запустить следующий код T-SQL:

ALTER TABLE [User_History] WITH CHECK
ADD CONSTRAINT [FK_User_History_User] FOREIGN KEY([Affected_User_House_Id], [Affected_User_Id])
REFERENCES [User] ([User_House_Id], [User_ID])
ON UPDATE CASCADE
GO

ALTER TABLE [User_History] CHECK CONSTRAINT [FK_User_History_User]
GO

ALTER TABLE [User_History]  WITH CHECK
ADD CONSTRAINT [FK_User_History_User_ModifiedBy] FOREIGN KEY([Modified_By_User_House_Id], [Modified_By_User_Id])
REFERENCES [User] ([User_House_Id], [User_ID])
ON UPDATE CASCADE
GO

ALTER TABLE [User_History] CHECK CONSTRAINT [FK_User_History_User_ModifiedBy]
GO

Этот T-SQL дал мне следующую ошибку:

*'User' table saved successfully
'User_History' table
- Unable to create relationship 'FK_User_History_User_ModifiedBy'.  
Introducing FOREIGN KEY constraint 'FK_User_History_User_ModifiedBy' on table 'User_History' may     cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or     modify other FOREIGN KEY constraints.
Could not create constraint. See previous errors.*

Код работает, если я удалю второй «НА КАСКАД ОБНОВЛЕНИЯ», однако это будет означать, что значения в полях «Modified_By_User_House_Id» и «Modified_By_User_Id» не будут обновлены, чтобы соответствовать их ссылочным значениям в таблице пользователей.

Я в растерянности относительно того, как достичь этой цели.


person user1625159    schedule 10.09.2012    source источник
comment
Вы пытались добавить ON DELETE NO ACTION ко второму ограничению?   -  person Kermit    schedule 10.09.2012
comment
вам нужно будет выбрать тот, который будет применяться, и применить другой каким-либо другим способом, если вообще. Как вы видите, SQL Server не может применять оба каскада. Это потому, что оба FK указывают на одного и того же родителя, он может каскадироваться только от одного FK к родителю.   -  person Beth    schedule 10.09.2012


Ответы (2)


Вы можете указать только один каскад. Вот попытка смоделировать несколько каскадов с двумя триггерами:

create table TabA (
    ID1 int not null,
    ID2 int not null,
    _RowID int IDENTITY(1,1) not null,
    constraint PK_TabA PRIMARY KEY (ID1,ID2),
    constraint UQ_TabA__RowID UNIQUE (_RowID)
)
go
create table TabB (
    ID1a int not null,
    ID2a int not null,
    ID1b int not null,
    ID2b int not null,
    constraint PK_TabB PRIMARY KEY (ID1a,ID2a,ID1b,ID2b)
)

Они проще, чем ваши таблицы, но, надеюсь, достаточно близки. Нам нужен неизменяемый идентификатор в TabA, и, очевидно, ID это не так, поскольку весь смысл в том, чтобы каскадировать их изменения. Поэтому я добавил _RowID.

Было бы неплохо реализовать по крайней мере реальный внешний ключ и просто смоделировать поведение каскада поверх него, но некоторое простое размышление продемонстрирует, что всегда есть точка, в которой FK будет нарушен. Итак, мы моделируем это:

create trigger FK_TabB_TabA on TabB
after insert,update
as
    set nocount on
    if exists (
        select
            *
        from
            inserted i
                left join
            TabA a
                on
                    i.ID1a = a.ID1 and
                    i.ID2a = a.ID2
                left join
            TabA b
                on
                    i.ID1b = b.ID1 and
                    i.ID2b = b.ID2
        where
            a._RowID is null or
            b._RowID is null)
    begin
        declare @Error varchar(max)
        set @Error = 'The INSERT statement conflicted with the Foreign Key constraint "FK_TabB_TabA". The conflict occurred in database "'+DB_NAME()+'", table "dbo.TabB".'
        RAISERROR(@Error,16,0)
        rollback
    end

И затем каскадное обновление:

create trigger FK_TabB_TabA_Cascade on TabA
after update
as
    set nocount on

    ;with Updates as (
        select
            d.ID1 as OldID1,
            d.ID2 as OldID2,
            i.ID1 as NewID1,
            i.ID2 as NewID2
        from
            inserted i
                inner join
            deleted d
                on
                    i._RowID = d._RowID
    )
    update b
    set
        ID1a = COALESCE(u1.NewID1,ID1a),
        ID2a = COALESCE(u1.NewID2,ID2a),
        ID1b = COALESCE(u2.NewID1,ID1b),
        ID2b = COALESCE(u2.NewID2,ID2b)
    from
        TabB b
            left join
        Updates u1
            on
                b.ID1a = u1.OldID1 and
                b.ID2a = u1.OldID2
            left join
        Updates u2
            on
                b.ID1b = u2.OldID1 and
                b.ID2b = u2.OldID2
    where
        u1.OldID1 is not null or
        u2.OldID1 is not null
go

Несколько простых вставок:

insert into TabA (ID1,ID2)
values (1,1),(1,2),(2,1),(2,2)
go
insert into TabB (ID1a,ID2a,ID1b,ID2b)
values (1,1,2,2)

Затем следующее получает ошибку. Не совсем похоже на встроенное нарушение FK, но достаточно близко:

insert into TabB (ID1a,ID2a,ID1b,ID2b)
values (1,1,2,3)
--Msg 50000, Level 16, State 0, Procedure FK_TabB_TabA, Line 28
--The INSERT statement conflicted with the Foreign Key constraint "FK_TabB_TabA". The conflict occurred in database "Flange", table "dbo.TabB".
--Msg 3609, Level 16, State 1, Line 1
--The transaction ended in the trigger. The batch has been aborted.

Это обновление, которое мы хотели выполнить:

update TabA set ID2 = ID2 + 1

И мы запрашиваем таблицу FK:

select * from TabB

Результат:

ID1a        ID2a        ID1b        ID2b
----------- ----------- ----------- -----------
1           2           2           3

Так что обновление пошло каскадом.


Почему нельзя использовать настоящие FK:

Вы хотите иметь каскадные обновления. Это означает, что значения идентификатора в TabA будут изменены на новое значение, которого в настоящее время не существует (предостережение — мы исключаем ситуации, когда 2n строк меняют свои значения идентификаторов) — в противном случае ограничение первичного ключа будет нарушено этим Обновить.

Таким образом, мы знаем, что новое значение ключа еще не будет существовать. Если бы мы попытались выполнить каскадные обновления с помощью триггера INSTEAD OF (чтобы обновить дочернюю таблицу перед родительской), то новые значения, которые мы пытаемся обновить в TabB, еще не существуют. В качестве альтернативы, если мы попытаемся выполнить каскадные обновления с помощью триггера AFTER — что ж, мы опоздали. Ограничение FK уже предотвратило обновление.

Я полагаю, что вы могли бы реализовать триггер INSTEAD OF, который вставляет новые строки как "дубликаты", обновляет дочерние элементы, а затем удаляет старые строки. В таких обстоятельствах, я думаю, у вас могут быть настоящие FK. Но я не хочу пытаться написать этот триггер, чтобы он был правильным при любых обстоятельствах (например, когда у вас есть три обновляемых строки. Два меняют свои значения идентификатора, а другой создает новый идентификатор)

person Damien_The_Unbeliever    schedule 10.09.2012
comment
Мне очень нравится эта идея, она дает мне все, что я хочу, но мне интересно, не нужно ли что-то из этого. Вы говорите, что реальный внешний ключ с симулированным каскадным поведением в конечном итоге сломается; можете привести пример сценария? - person user1625159; 10.09.2012
comment
@user1625159 user1625159 - Я добавил раздел внизу, объясняющий мои первоначальные мысли, отличные от реальных FK. - person Damien_The_Unbeliever; 10.09.2012
comment
Теперь я понимаю. Я думал, что триггер может отключить проверки ограничений FKey во время каскадного обновления. Я думаю, это невозможно сделать с MSSQL? - person user1625159; 11.09.2012
comment
@user1625159 user1625159 - Нет. Некоторые системы баз данных (я думаю, Oracle, может быть, и другие) позволяют вам объявлять некоторые ограничения с отложенной проверкой, и проверки происходят только при фиксации транзакции. SQL Server не предлагает такой опции — любые включенные ограничения применяются на границах операторов. - person Damien_The_Unbeliever; 11.09.2012
comment
Большое спасибо за это предложение и объяснение, я обязательно воспользуюсь им. Я поставил тебе галочку. - person user1625159; 11.09.2012

Согласно этой статье базы знаний, это сообщение об ошибке появляется, когда "таблица не может появляться более одного раза в списке всех каскадных ссылочных действий, которые запускаются либо оператором DELETE, либо оператором UPDATE».

Поскольку у вас есть два пути, исходящих из одной и той же таблицы, возможный обходной путь может заключаться в создании нового ключа в родительской таблице и создании одного внешнего ключа в дочерней ([Affected_User_House_Id], [Affected_User_Id], [Modified_By_User_House_Id], [Modified_By_User_Id]). Однако это, вероятно, создаст много накладных расходов. В крайнем случае вы можете использовать триггеры для обеспечения реляционной целостности.

person Kermit    schedule 10.09.2012
comment
Спасибо за это, было приятно узнать, почему у меня возникла такая проблема, чтобы заставить это работать. - person user1625159; 11.09.2012