Сложное временное ограничение базы данных между таблицами - PullRequest
1 голос
/ 01 октября 2009

У меня особенно сложное деловое ограничение, которое я хотел бы применить на уровне базы данных. Данные носят финансовый характер, и поэтому должны быть защищены от несоответствий до n-й степени - не доверяя бизнес-уровню с этим материалом. Я использую слово «временный» несколько свободно, что означает, что я намерен контролировать, как сущность может и не может меняться со временем.

Затенение деталей, вот дизайн:

  • Счет может содержать несколько сборов.
  • Сборы присваиваются счету вскоре после создания счета.
  • Счет-фактура достигает стадии в процессе, после которой она «блокируется».
  • С этого момента, плата не может быть добавлена ​​или удалена из этого счета.

Вот упрощенное определение данных:

CREATE TABLE Invoices
(
    InvoiceID INT IDENTITY(1,1) PRIMARY KEY,
)

CREATE TABLE Fees
(
    FeeID INT IDENTITY(1,1) PRIMARY KEY,
    InvoiceID INT REFERENCES Invoices(InvoiceID),
    Amount MONEY
)

Вы заметите, что «блокируемая» природа счета здесь не представлена; как его представить - и нужно ли вообще его представлять - все еще остается открытым вопросом.

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

Я случайно внедряю это в SQL Server 2008 (синтаксис мог быть подсказкой), но я любопытный парень, поэтому, если есть решения, которые работают на других СУБД, я бы хотел услышать об этих а также.

Ответы [ 7 ]

9 голосов
/ 01 октября 2009

Я не могу придумать, как это сделать с помощью нормализации. Однако, если бы я хотел ограничить это для базы данных, я бы сделал это одним из двух способов:

во-первых, я бы добавил «заблокированный» столбец в «Счета-фактуры», который немного своего рода, просто способ обозначить его как заблокированный.

Тогда два пути:

  1. Триггер «до вставки», который выдаст ошибку перед выполнением вставки, если указанный счет заблокирован.
  2. Выполните эту логику в хранимой процедуре, которая создает плату.

РЕДАКТИРОВАТЬ: Я не смог найти хорошую статью MSDN о том, как сделать одну из них, но у IBM есть такая, которая очень хорошо работает в SQL Server: http://publib.boulder.ibm.com/infocenter/iseries/v5r3/index.jsp?topic=/sqlp/rbafybeforesql.htm

4 голосов
/ 08 октября 2009

Не усложняйте, я бы пошел с триггерами.В их использовании нет ничего постыдного, вот для чего они здесь.

Чтобы избежать большого количества логики в триггерах, я добавляю битовый столбец «Editable» в таблицу заголовков, затем в основном использую деление с Editable, чтобы либо сработать, либо вызвать ошибку деления на ноль, которую я CATCH ипреобразовать в Invoice is not editable, no changes permitted сообщение.Не существует СУЩЕСТВУЮЩИХ для устранения дополнительных накладных расходов.Попробуйте это:

CREATE TABLE testInvoices
(
     InvoiceID   INT      not null  IDENTITY(1,1) PRIMARY KEY
    ,Editable    bit      not null  default (1)  --1=can edit, 0=can not edit
    ,yourData    char(2)  not null  default ('xx')
)
go

CREATE TABLE TestFees
(
    FeeID     INT IDENTITY(1,1) PRIMARY KEY
   ,InvoiceID INT REFERENCES testInvoices(InvoiceID)
   ,Amount    MONEY
)
go

CREATE TRIGGER trigger_testInvoices_instead_update
ON testInvoices
INSTEAD OF UPDATE
AS
BEGIN TRY
    --cause failure on updates when the invoice is not editable
    UPDATE t 
        SET Editable =i.Editable
           ,yourData =i.yourData
        FROM testInvoices            t
            INNER JOIN INSERTED      i ON t.InvoiceID=i.InvoiceID
        WHERE 1=CONVERT(int,t.Editable)/t.Editable    --div by zero when not editable
END TRY
BEGIN CATCH

    IF ERROR_NUMBER()=8134 --catch div by zero error
        RAISERROR('Invoice is not editable, no changes permitted',16,1)
    ELSE
    BEGIN
        DECLARE @ErrorMessage nvarchar(400), @ErrorNumber int, @ErrorSeverity int, @ErrorState int, @ErrorLine int
        SELECT @ErrorMessage = N'Error %d, Line %d, Message: '+ERROR_MESSAGE(),@ErrorNumber = ERROR_NUMBER(),@ErrorSeverity = ERROR_SEVERITY(),@ErrorState = ERROR_STATE(),@ErrorLine = ERROR_LINE()
        RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber,@ErrorLine)
    END

END CATCH
GO


CREATE TRIGGER trigger_testInvoices_instead_delete
ON testInvoices
INSTEAD OF DELETE
AS
BEGIN TRY
    --cause failure on deletes when the invoice is not editable
    DELETE t
    FROM testInvoices            t
        INNER JOIN DELETED       d ON t.InvoiceID=d.InvoiceID
        WHERE 1=CONVERT(int,t.Editable)/t.Editable    --div by zero when not editable
END TRY
BEGIN CATCH

    IF ERROR_NUMBER()=8134 --catch div by zero error
        RAISERROR('Invoice is not editable, no changes permitted',16,1)
    ELSE
    BEGIN
        DECLARE @ErrorMessage nvarchar(400), @ErrorNumber int, @ErrorSeverity int, @ErrorState int, @ErrorLine int
        SELECT @ErrorMessage = N'Error %d, Line %d, Message: '+ERROR_MESSAGE(),@ErrorNumber = ERROR_NUMBER(),@ErrorSeverity = ERROR_SEVERITY(),@ErrorState = ERROR_STATE(),@ErrorLine = ERROR_LINE()
        RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber,@ErrorLine)
    END

END CATCH
GO

CREATE TRIGGER trigger_TestFees_instead_insert
ON TestFees
INSTEAD OF INSERT
AS
BEGIN TRY
    --cause failure on inserts when the invoice is not editable
    INSERT INTO TestFees
            (InvoiceID,Amount)
        SELECT
            f.InvoiceID,f.Amount/i.Editable  --div by zero when invoice is not editable
            FROM INSERTED                f
                INNER JOIN testInvoices  i ON f.InvoiceID=i.invoiceID
END TRY
BEGIN CATCH

    IF ERROR_NUMBER()=8134 --catch div by zero error
        RAISERROR('Invoice is not editable, no changes permitted',16,1)
    ELSE
    BEGIN
        DECLARE @ErrorMessage nvarchar(400), @ErrorNumber int, @ErrorSeverity int, @ErrorState int, @ErrorLine int
        SELECT @ErrorMessage = N'Error %d, Line %d, Message: '+ERROR_MESSAGE(),@ErrorNumber = ERROR_NUMBER(),@ErrorSeverity = ERROR_SEVERITY(),@ErrorState = ERROR_STATE(),@ErrorLine = ERROR_LINE()
        RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber,@ErrorLine)
    END

END CATCH
GO

CREATE TRIGGER trigger_TestFees_instead_update
ON TestFees
INSTEAD OF UPDATE
AS
BEGIN TRY
    --cause failure on updates when the invoice is not editable
    UPDATE f 
        SET InvoiceID =ff.InvoiceID
           ,Amount    =ff.Amount/i.Editable --div by zero when invoice is not editable
        FROM TestFees                f
            INNER JOIN INSERTED     ff ON f.FeeID=ff.FeeID
            INNER JOIN testInvoices  i ON f.InvoiceID=i.invoiceID
END TRY
BEGIN CATCH

    IF ERROR_NUMBER()=8134 --catch div by zero error
        RAISERROR('Invoice is not editable, no changes permitted',16,1)
    ELSE
    BEGIN
        DECLARE @ErrorMessage nvarchar(400), @ErrorNumber int, @ErrorSeverity int, @ErrorState int, @ErrorLine int
        SELECT @ErrorMessage = N'Error %d, Line %d, Message: '+ERROR_MESSAGE(),@ErrorNumber = ERROR_NUMBER(),@ErrorSeverity = ERROR_SEVERITY(),@ErrorState = ERROR_STATE(),@ErrorLine = ERROR_LINE()
        RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber,@ErrorLine)
    END

END CATCH
GO

CREATE TRIGGER trigger_TestFees_instead_delete
ON TestFees
INSTEAD OF DELETE
AS
BEGIN TRY
    --cause failure on deletes when the invoice is not editable
    DELETE f
    FROM TestFees                f
        INNER JOIN DELETED      ff ON f.FeeID=ff.FeeID
        INNER JOIN testInvoices  i ON f.InvoiceID=i.invoiceID AND 1=CONVERT(int,i.Editable)/i.Editable --div by zero when invoice is not editable
END TRY
BEGIN CATCH

    IF ERROR_NUMBER()=8134 --catch div by zero error
        RAISERROR('Invoice is not editable, no changes permitted',16,1)
    ELSE
    BEGIN
        DECLARE @ErrorMessage nvarchar(400), @ErrorNumber int, @ErrorSeverity int, @ErrorState int, @ErrorLine int
        SELECT @ErrorMessage = N'Error %d, Line %d, Message: '+ERROR_MESSAGE(),@ErrorNumber = ERROR_NUMBER(),@ErrorSeverity = ERROR_SEVERITY(),@ErrorState = ERROR_STATE(),@ErrorLine = ERROR_LINE()
        RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber,@ErrorLine)
    END

END CATCH
GO

Вот простой тестовый скрипт для проверки различных комбинаций:

INSERT INTO testInvoices VALUES(default,default) --works
INSERT INTO testInvoices VALUES(default,default) --works
INSERT INTO testInvoices VALUES(default,default) --works

INSERT INTO TestFees (InvoiceID,Amount) VALUES (1,111)  --works
INSERT INTO TestFees (InvoiceID,Amount) VALUES (1,1111) --works
INSERT INTO TestFees (InvoiceID,Amount) VALUES (2,22)   --works
INSERT INTO TestFees (InvoiceID,Amount) VALUES (2,222)  --works
INSERT INTO TestFees (InvoiceID,Amount) VALUES (2,2222) --works

update testInvoices set Editable=0 where invoiceid=3 --works
INSERT INTO TestFees (InvoiceID,Amount) VALUES (3,333) --error<<<<<<<

UPDATE TestFees SET Amount=1 where feeID=1 --works
UPDATE testInvoices set Editable=0 where invoiceid=1 --works
UPDATE TestFees SET Amount=11111 where feeID=1 --error<<<<<<<
UPDATE testInvoices set Editable=1 where invoiceid=1 --error<<<<<<<

UPDATE testInvoices set Editable=0 where invoiceid=2 --works
DELETE TestFees WHERE invoiceid=2 --error<<<<<

DELETE FROM testInvoices where invoiceid=2 --error<<<<<

UPDATE testInvoices SET Editable='A' where invoiceid=1 --error<<<<<<< Msg 245, Level 16, State 1, Line 1 Conversion failed when converting the varchar value 'A' to data type bit.
1 голос
/ 10 октября 2009

Я согласен с общим мнением, что в таблицу «Счета-фактуры» следует добавить бит блокировки, чтобы указать, могут ли быть добавлены сборы. Затем необходимо добавить код TSQL для обеспечения соблюдения бизнес-правил, связанных с заблокированными счетами. Ваша первоначальная публикация, похоже, не содержит подробностей об условиях, при которых счет блокируется, но разумно предположить, что бит блокировки можно установить соответствующим образом (этот аспект проблемы может стать сложным, но давайте разберемся с этим в другом нить).

С учетом этого консенсуса существует 2 варианта реализации, которые эффективно обеспечат соблюдение бизнес-правила на уровне данных: триггеры и стандартные хранимые процедуры. Чтобы использовать стандартные хранимые процедуры, нужно, конечно, DENY UPDATES, DELETES и INSERTS для таблиц Invoices и Fees и требовать, чтобы все изменения данных выполнялись с использованием хранимых процедур.

Преимущество использования триггеров в том, что клиентский код приложения можно упростить, поскольку к таблицам можно получить прямой доступ. Это может быть важным преимуществом, если вы используете LINQ to SQL, например.

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

Следует отметить, что это обсуждение не является независимым от базы данных: мы обсуждаем варианты реализации SQL Server. Мы могли бы использовать аналогичный подход с Oracle или любым другим сервером, который обеспечивает процедурную поддержку SQL, но это бизнес-правило не может быть применено с использованием статических ограничений и не может быть применено нейтральным образом базы данных.

1 голос
/ 07 октября 2009

Почему бы просто не иметь столбец 'Locked', который является логическим (или один символ, например, 'y', 'n'), и настроить запрос на обновление, чтобы использовать подзапрос:

INSERT INTO Fees (InvoiceID, Amount) VALUES ((SELECT InvoiceID FROM Invoices WHERE InvoiceID = 3 AND NOT Locked), 13.37);

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

PS. Приведенный выше запрос на вставку использует синтаксис MySQL, боюсь, я не слишком знаком с вариантом SQL Server TQL.

1 голос
/ 05 октября 2009

Я думаю, вам лучше явно сохранить состояние «заблокировано / разблокировано» для счета в таблице счетов, а затем применить триггеры на INSERT и DELETE (и UPDATE, хотя на самом деле вы не говорите, что хотите оплатить услуги на счет-фактуру заморожен) для предотвращения изменений, если счет-фактура находится в заблокированном состоянии.

Флаг блокировки необходим, если только нет надежного алгоритмического метода определения, когда счет заблокирован - возможно, через 2 часа после его выставления. Конечно, вам нужно обновить строку счета-фактуры, чтобы заблокировать ее, поэтому лучше использовать алгоритмический метод (меньше обновлений).

1 голос
/ 05 октября 2009

Вы можете ограничить добавления в таблицу FEES, изменив используемую модель данных:

СЧЕТА

  • INVOICE_ID
  • INVOICE_LOCKED_DATE, ноль

ТАРИФЫ

  • FEE_ID (шт.)
  • INVOICE_ID (пк, фк INVOICES.INVOICE_ID)
  • INVOICE_LOCKED_DATE (пк, фк INVOICES.INVOICE_LOCKED_DATE)
  • СУММА

На первый взгляд, это избыточно, но пока инструкция INSERT для таблицы FEES не включает в себя поиск в таблице INVOICES для заблокированной даты (по умолчанию - ноль) - она ​​гарантирует, что в новых записях будет дата, указанная в счете был заблокирован.

Другим вариантом является наличие двух таблиц, касающихся обработки платежей - PRELIMINARY_FEES и CONFIRMED_FEES.

Хотя сборы по счетам по-прежнему доступны для редактирования, они находятся в таблице PRELIMINIARY_FEES и после подтверждения переносятся в CONFIRMED_FEES. Мне не очень нравится эта из-за необходимости поддерживать две идентичные таблицы вместе с последствиями запроса, но это позволило бы использовать GRANT s (для роли, а не для пользователя) только для разрешения SELECT доступ к CONFIRMED_FEES с разрешением INSERT, UPDATE, DELETE для таблицы PRELIMINARY_FEES. Вы не можете ограничить гранты в одной настройке таблицы FEES, потому что грант не знает данных - вы не можете проверить данный статус.

0 голосов
/ 05 октября 2009

Вы не можете просто использовать ограничения FK и тому подобное - по крайней мере, ни в коем случае не имеет большого смысла. Я бы предложил использовать триггер INSTEAD OF в SQL Server, чтобы применить это ограничение. Это должно быть довольно легко написать и довольно просто.

...