Как реализовать основанную на ограничениях ссылочную целостность на простом трехстороннем отношении - PullRequest
5 голосов
/ 20 февраля 2020

У меня три простых отношения. TableB и TableC оба ссылаются на TableA, а TableC также ссылаются на TableB.

Я считаю невозможным смоделировать это на SQL сервере таким образом, чтобы обеспечить ссылочную целостность через ограничения, но также позволяет удалять записи из любого объекта, не требуя сложной или неэффективной проверки ссылочной целостности на основе триггера, или удаления связанных объектов вручную в правильном порядке.

Вот моя схема.

create table TableA (
   Id int identity not null,
   constraint P_TableA_Id primary key (Id)
)

create table TableB (
   Id int identity not null,
   constraint P_TableB_Id primary key (Id),

   ARef int,
   constraint F_TableB_ARef foreign key (ARef) references TableA(Id) on delete cascade
)

create table TableC (
   Id int identity not null,
   constraint P_TableC_Id primary key (Id),

   ARef int,
   constraint F_TableC_ARef foreign key (ARef) references TableA(Id) on delete cascade,

   BRef int,

   -- Does not work.
   --constraint F_TableC_BRef foreign key (BRef) references TableB(Id) on delete cascade

   -- Works.
   constraint F_TableC_BRef foreign key (BRef) references TableB(Id)
)

Последнее on delete cascade - это то, что разрушает его, так как SQL Сервер не разрешит это. Пытаясь разорвать этот цикл, я попробовал следующее:

Использование ограничения set null и триггера after для удаления строк в TableC. Не работает, SQL Сервер отказывается это разрешить.

constraint F_TableC_BRef foreign key (BRef) references TableB(Id) on delete set null

Использование триггера Instead of для удаления TableC записей при удалении TableB записей не работает, потому что вы не можете использовать триггер instead of для любой таблицы с ограничением каскадного удаления.

create trigger T_TableB_delete on TableB instead of delete as
begin
   delete from TableC where BRef in (select Id from deleted)
   delete from TableB where Id in (select Id from deleted)
end

Триггер after не будет работать, поскольку попытка удаления из TableB не удастся из-за включения внешнего ключа TableC.BRef до запуска триггера.

Одним из решений является кодирование всей проверки ссылочной целостности с использованием триггеров, которая работает, но ужасно сложна и неэффективна.

Другое решение состоит в том, чтобы требовать от клиентов вручную удалите TableC записей перед TableB записями.

Возможно, лучшее решение, которое у меня есть на данный момент, - это создание хранимой процедуры для удаления из TableB, и в этой процедуре сначала сначала удаляются TableC записи. Но в настоящее время мы не используем хранимые процедуры, поэтому начинать использовать их для решения того, что на первый взгляд кажется очень простой проблемой проектирования, не идеально.

Существуют ли другие решения для этого, которые я пропустили?

ОБНОВЛЕНИЕ

Вот более реалистичная версия того, чего я пытаюсь достичь.

create table Users (
   Id int identity not null,
   constraint P_Users_Id primary key (Id),

   Name nvarchar(20)
)

create table Documents (
   Id int identity not null,
   constraint P_Documents_Id primary key (Id),

   CreatedBy int,
   constraint F_Documents_CreatedBy foreign key (CreatedBy) references Users(Id) on delete cascade,
)

create table Documents_LastEditedBy (
   DocumentId int,
   constraint F_Documents_LastEditedBy_DocumentId foreign key (DocumentId) references Documents(Id) on delete cascade,

   UserId int,
   constraint F_Documents_UserId foreign key (UserId) references Users(Id) on delete cascade,   
)

В этой схеме удаление пользователя следует удалить любые документы, где пользователь является CreateBy. Но удаленные пользователи, которые отображаются в LastEditedBy для документа, должны просто возвращать ноль. Я пытаюсь добиться этого, используя Documents_LastEditedBy в качестве таблицы сопоставления.

Ответы [ 4 ]

1 голос
/ 20 февраля 2020

Вы можете создать триггер вместо удаления в таблице «Пользователи», который обновляет идентификатор пользователя EditedBy до NULL:

create table Users (
   Id int identity not null,
   constraint P_Users_Id primary key (Id),

   Name nvarchar(20),
)
go

create table Documents (
   Id int identity not null,
   constraint P_Documents_Id primary key (Id),

   CreatedBy int,
   constraint F_Documents_CreatedBy foreign key (CreatedBy) references Users(Id) on delete cascade
)

create table Documents_LastEditedBy (
   DocumentId int,
   constraint F_Documents_LastEditedBy_DocumentId foreign key (DocumentId) references Documents(Id) on delete cascade,

   UserId int,
   constraint F_Documents_UserId foreign key (UserId) references dbo.Users(Id) on delete no action
)
go

insert into dbo.Users(Name) values ('UserA'), ('UserB');
insert into dbo.Documents(CreatedBy) values (1), (2); --doc1 created by userA, doc2 created by userB, doc3 created by 
insert into dbo.Documents_LastEditedBy values(1, 2) --document 1 edited by B (?? )
insert into dbo.Documents_LastEditedBy values(2, 1) --document 2 edited by userA
insert into dbo.Documents_LastEditedBy values(2, 2) --document 2 edited by userB
go

select *
from dbo.Users
select *
from dbo.Documents
select *
from Documents_LastEditedBy
go


delete from dbo.Users
where name = 'UserA' --fk violation
go

create trigger dbo.insteadofdeleteusers on dbo.users
instead of delete
as
begin
    if not exists(select * from deleted)
    begin
        return;
    end

    update dbo.Documents_LastEditedBy
    set UserId = null
    where UserId in (select id from deleted);

    delete 
    from dbo.Users
    where id in (select id from deleted);

end
go

delete from dbo.Users
where name = 'UserA' 
go

select *
from dbo.Users --userA gone
select *
from dbo.Documents--document created by userA gone
select *
from Documents_LastEditedBy --last edited userA set to NULL
go
0 голосов
/ 04 марта 2020

Создать триггер для замены удаления в пользовательской таблице:

CREATE TRIGGER [dbo].[Trigger_Users]
ON [dbo].[Users]
INSTEAD OF DELETE
AS
BEGIN

    SET NoCount ON
    SELECT Id INTO #TEMP FROM deleted

    DELETE FROM [dbo].[Documents_LastEditBy] 
    WHERE Exists(select top 1 1 FROM #TEMP WHERE Id = UserId)
    DELETE FROM [dbo].[Users] 
    WHERE Exists(select top 1 1 FROM #TEMP T WHERE T.Id = [Users].Id)
END

Вам не нужно вызывать удаление в таблице [dbo]. [Documents], так как она будет удалена каскадом когда пользователь удаляется

У ограничений F_Documents_UserId не должно быть каскадного удаления.

0 голосов
/ 04 марта 2020

Примеры таблиц документов не так реальны по сравнению с примером TableA.

Как будто я не могу понять назначение таблицы Documents_LastEditedBy в первую очередь. Так что, как и в таком случае, существует такая сложная ситуация, как ваша эти вещи имеют значение.

Одним из решений является кодирование всей проверки ссылочной целостности с использованием триггеров, которая работает, но ужасно сложна и неэффективна.

Смотрите, есть две вещи в ваше ограничение, foreign key and delete cascade.

Цель Foriegn key - сохранить ссылочную целостность и повысить производительность, если ваш FK является доверенным.

On Delete cascade: если запись родительской таблицы удалена, то дочерний запись таблицы будет автоматически удалена.

ТАК одним способом является частичная реализация ограничения FK без каскада удаления.

Implement Delete Cascade in trigger: Фактически триггер предназначен для такой ситуации.

Trigger and constraint оба отвратительны. Вы можете генерировать или регистрировать ошибку из триггера, как ограничение. При любой ошибке вы можете откатить назад и выбросить ошибку.

Триггер не так неэффективен, как вы думаете, если код написан правильно и индекс хорошо настроен.

Кстати, как часто будет происходить удаление? От этого также зависит беспокойство по поводу неэффективности.

Второй метод : Поскольку я не уверен в назначении каждой таблицы, мой второй подход дает хорошую идею.

Мой сценарий не тестировались,

Вы можете перепроектировать свои таблицы таким образом,

create table Users (
   Id int identity not null,
   constraint P_Users_Id primary key (Id),
   Name nvarchar(20)
)

create table Documents (
   Id int identity not null,
   constraint P_Documents_Id primary key (Id) 
)

create table Documents_LastEditedBy (
   DocumentId int,
   constraint F_Documents_LastEditedBy_DocumentId foreign key (DocumentId) references Documents(Id) on delete cascade,
)

create table Documents_UserMapping (
   Documents_UserMappingid int identity(1,1) primary key
   DocumentId int not null,
   UserId int not null,
   constraint F_Documents_UserId foreign key (UserId) references Users(Id) on delete cascade,
   UserType tinyint not null    
)

UserType : Indicate that userid belong to Documents table  or Documents_LastEditedBy

Есть преимущества и недостатки этого дизайна, но я не обсуждаю это.

Во-первых, я хочу понять полное требование, прежде чем принимать какое-либо решение.

Насколько я понял, я буду go с подходом триггера.

Ваш вопрос превосходен.

0 голосов
/ 03 марта 2020

Надеюсь, что это поможет вам.

это отношение является ошибкой дизайна и неправильным, как эта картина:

enter image description here

Мы не Не нужны отношения между пользователями и документом, потому что это задание LastEDITEDBY (таблица B) лучше удалить отношение сорняков,

Но по какой-либо причине вы знаете себя. Если бы у вас была такая таблица.

Решение: вам следует использовать новый столбец , называемый flag в Таблице a и c , затем УДАЛИТЕ таблицу b (в этом примере имя Documents_LastEditedBy) и Trriger после удаления установят флаг C и A True. Теперь удалите ту же строку в C, используя флаг, подобный Where flag=1, а затем удалите эту же строку в A, используя Where flag=1 с помощью триггера вместо.

Я надеюсь, что это usabel

...