TSQL: создать пользовательскую идентификацию из пользовательской идентификации? (Управление редакциями базы данных) - PullRequest
6 голосов
/ 17 декабря 2010

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

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

Пример

DRAWING
ID    | REV   | INFO
------+-------+------
1     | 0     | "Draw1"
2     | 0     | "Draw2"
2     | 1     | "Draw2Edit"
2     | 2     | "Draw2MoreEdit"
3     | 0     | "Draw3"
4     | 0     | "Draw4"

Если бы я вставил еще несколько записей в мою таблицу, например:

INSERT INTO DRAWING (INFO) VALUES ("Draw5")
INSERT INTO DRAWING (ID,INFO) VALUES (3,"Draw3Edit")

Мой стол хотел бы:

DRAWING
ID    | REV   | INFO
------+-------+------
1     | 0     | "Draw1"
2     | 0     | "Draw2"
2     | 1     | "Draw2Edit"
2     | 2     | "Draw2MoreEdit"
3     | 0     | "Draw3"
3     | 1     | "Draw3Edit"      --NEW ROW
4     | 0     | "Draw4"
5     | 0     | "Draw5"          --NEW ROW

T-SQL

CREATE TABLE DRAWING
(
    ID INT,
    REV INT,  
    INFO VARCHAR(50),
    PRIMARY KEY (ID,REV)
);

CREATE TABLE CURRENT_DRAWING
(
    ID INT IDENTITY (1,1),
    DRAWING_ID INT,
    DRAWING_REV INT,
    PRIMARY KEY (ID),
    FOREIGN KEY (DRAWING_ID,DRAWING_REV) REFERENCES DRAWING (ID,REV)
        ON UPDATE CASCADE
        ON DELETE CASCADE
);

Я использую SQL Server Management Studio 2005 и работаю над SQL Server 2000 БД.

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

Обновление:

Я думаю, что оно близко к тому, что я хочу:

DROP TABLE DRAW

GO

CREATE TABLE DRAW
(
    ID INT DEFAULT(0), 
    REV INT DEFAULT(-1), 
    INFO VARCHAR(10), 
    PRIMARY KEY(ID, REV)
)

GO

CREATE TRIGGER TRIG_DRAW ON DRAW
FOR INSERT
AS
BEGIN
    DECLARE @newId INT,
            @newRev INT,
            @insId INT,
            @insRev INT

    SET TRANSACTION ISOLATION LEVEL READ COMMITTED
    BEGIN TRANSACTION

    SELECT @insId = ID FROM inserted
    SELECT @insRev = REV FROM inserted

    PRINT 'BEGIN TRIG'
    PRINT @insId
    PRINT @insRev
    PRINT @newId
    PRINT @newRev


    --IF ID=0 THEN IT IS A NEW ID
    IF @insId <=0
    BEGIN
        --NEW DRAWING ID=MAX+1 AND REV=0
        SELECT @newId = COALESCE(MAX(ID), 0) + 1 FROM DRAW
        SELECT @newRev = 0
    END
    ELSE
    --ELSE IT IS A NEW REV
    BEGIN
        --CHECK TO ENSURE ID EXISTS
        IF EXISTS(SELECT * FROM DRAW WHERE ID=@insId AND REV=0)
        BEGIN
            PRINT 'EXISTS'
            SELECT @newId = @insId
            SELECT @newRev = MAX(REV) + 1 FROM DRAW WHERE ID=@insID
        END
        ELSE
        --ID DOES NOT EXIST THEREFORE NO REVISION
        BEGIN
            RAISERROR 50000 'ID DOES NOT EXIST.'
            ROLLBACK TRANSACTION
            GOTO END_TRIG
        END
    END

    PRINT 'END TRIG'
    PRINT @insId
    PRINT @insRev
    PRINT @newId
    PRINT @newRev

    SELECT * FROM DRAW

    UPDATE DRAW SET ID=@newId, REV=@newRev WHERE ID=@insId



    COMMIT TRANSACTION
    END_TRIG:
END

GO


INSERT INTO DRAW (INFO) VALUES ('DRAW1')
INSERT INTO DRAW (INFO) VALUES ('DRAW2')
INSERT INTO DRAW (ID,INFO) VALUES (2,'DRAW2EDIT1') --PROBLEM HERE
INSERT INTO DRAW (ID,INFO) VALUES (2,'DRAW2EDIT2')
INSERT INTO DRAW (INFO) VALUES ('DRAW3')
INSERT INTO DRAW (INFO) VALUES ('DRAW4')

GO

--SHOULD THROW
INSERT INTO DRAW (ID,INFO) VALUES (9,'DRAW9')

GO

SELECT * FROM DRAW

GO

Однако я продолжаю получать Violation of PRIMARY KEY constraint.

Я поместил отладочные операторы, и вряд ли я нарушаю свой первичный ключ:

BEGIN TRIG
0
-1


END TRIG
0
-1
1
0

(1 row(s) affected)

(1 row(s) affected)

(1 row(s) affected)
BEGIN TRIG
0
-1


END TRIG
0
-1
2
0

(2 row(s) affected)

(1 row(s) affected)

(1 row(s) affected)
BEGIN TRIG
2
-1


EXISTS
END TRIG
2
-1
2
1

(3 row(s) affected)
Msg 2627, Level 14, State 1, Procedure TRIG_DRAW, Line 58
Violation of PRIMARY KEY constraint 'PK__DRAW__56D3D912'. Cannot insert duplicate key in object 'DRAW'.
The statement has been terminated.

печатает

ID  | REV    | INFO
----+--------+------------
1   |   0    |  DRAW1
2   |  -1    |  DRAW2EDIT1  --This row is being updated to 2 1 
2   |   0    |  DRAW2

Непосредственно перед тем, как происходит сбой и строка 2 -1 обновляется до 2 1. Это не должно нарушать мой первичный ключ.

Ответы [ 2 ]

2 голосов
/ 14 января 2011

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

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

CREATE TABLE DRAWING
(
    ID INT IDENTITY(1, 1),
    PRIMARY KEY (ID)
);

CREATE TABLE DRAWING_REVISION
(    
    ID INT IDENTITY(1, 1),
    DRAWING_ID INT,
    INFO VARCHAR(50),
    PRIMARY KEY (ID),
    CONSTRAINT FK_DRAWING_REVISION_DRAWING FOREIGN KEY (DRAWING_ID) REFERENCES DRAWING(ID)
);

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

Очевидное решение и его недостатки

Если вам нуженВпрочем, читаемый человеком номер ревизии, а не только для глаз вашего сервера ID, это можно сделать двумя способами.Они оба начинают с добавления REV INT к определению данных для DRAWING_REVISION вместе с CONSTRAINT UK_DRAWING_REVISION_DRAWING_ID_REV UNIQUE (DRAWING_ID, REV).Хитрость тогда, конечно, состоит в том, чтобы найти следующий номер ревизии для данного чертежа.

Если вы ожидаете, что только у каждого будет крошечное число одновременных пользователей, вы можете просто SELECT MAX(REV) + 1 FROM DRAWING_REVISION WHERE DRAWING_ID = @DRAWING_ID, либо в вашемкод приложения или в триггере INSTEAD OF INSERT.Однако при высоком параллелизме или неудаче пользователи могут блокировать друг друга, потому что они могут попытаться вставить одну и ту же комбинацию DRAWING_ID и REV в DRAWING_REVISION.

Some Background

На самом деле есть только одно решение этой проблемы, хотя для объяснения, почему есть только одно решение, требуется немного справочной информации.Рассмотрим следующий код:

BEGIN TRAN

INSERT DRAWING DEFAULT VALUES;
INSERT DRAWING DEFAULT VALUES;
SELECT ID FROM DRAWING; -- Output: 1, 2

ROLLBACK TRAN

BEGIN TRAN

INSERT DRAWING DEFAULT VALUES;
SELECT ID FROM DRAWING; -- Output: 3

ROLLBACK TRAN

Конечно, выходные данные будут отличаться при последующих выполнениях.За кулисами SQL-сервер выдает значения IDENTITY и увеличивает счетчик.Если вы никогда не зафиксируете значение, сервер не будет пытаться «заполнить» дыры в последовательности - значения предоставляются только для пересылки.

Это функция, а не ошибка.IDENTITY колонны предназначены для заказа и уникальны, но не обязательно плотно упакованы.Единственный способ гарантировать плотную упаковку - это сериализовать все входящие запросы, убедившись, что каждый из них завершает или завершает работу до начала следующего;в противном случае сервер может попытаться выполнить обратное заполнение значения IDENTITY, которое было выдано полчаса назад, только для того, чтобы длительная транзакция (т. е. первоначальный получатель этого значения IDENTITY) зафиксировала строку с дубликатом.Первичный ключ.

(Стоит отметить, что когда я говорю «транзакция», это не обязательно означает TSQL TRANSACTION, хотя я бы порекомендовал их использовать. Это может быть абсолютно любая процедура насторона приложения или SQL-сервера, которая может занять любое количество времени, даже если это время - только время, необходимое для SELECT следующего номера редакции и сразу после этого INSERT нового DRAWING_REVISION.)

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

Подход SELECT MAX(REV) + 1 имеет тот же недостаток.Естественно, подход MAX не предпринимает никаких попыток обратного заполнения значений, но он заставляет каждый параллельный запрос бороться за один и тот же номер ревизии с одинаковыми результатами.

Почему это плохо?Системы баз данных рассчитаны на параллелизм и валюту: эта возможность является одним из основных преимуществ управляемой базы данных по сравнению с форматом плоских файлов.

Поддельное правильное

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

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

ALTER TABLE DRAWING ADD REV INT NOT NULL DEFAULT(0);

GO

CREATE PROCEDURE GET_REVISION_NUMBER (@DRAWING_ID INT) AS
BEGIN
    DECLARE @ATTEMPTS INT;
    SET @ATTEMPTS = 0;
    DECLARE @ATTEMPT_LIMIT INT;
    SET @ATTEMPT_LIMIT = 5;
    DECLARE @CURRENT_REV INT;
    LOOP:
        SET @CURRENT_REV = (SELECT REV FROM DRAWING WHERE DRAWING.ID = @DRAWING_ID);
        UPDATE DRAWING SET REV = @CURRENT_REV + 1 WHERE DRAWING.ID = @DRAWING_ID AND REV = @CURRENT_REV;
        SET @ATTEMPTS = @ATTEMPTS + 1;
        IF (@@ROWCOUNT = 0)
        BEGIN
            IF (@ATTEMPTS >= @ATTEMPT_LIMIT) RETURN NULL;
            GOTO LOOP;
        END
    RETURN @CURRENT_REV + 1;
END

Проверка @@ ROWCOUNT очень важна - эта процедура должна быть нетранзакционной, потому что вы не хотитескрыть конфликты от одновременных запросов;Вы хотите разрешить их.Единственный способ убедиться, что ваше обновление определенно прошло, - это проверить, обновлялись ли какие-либо строки.

Конечно, вы могли догадаться, что этот подход не защищен от ошибок.Единственный способ «разрешить» конфликты - это попробовать несколько раз, прежде чем сдаться.Ни одно решение для домашнего пивоварения не будет столь же хорошим, как жестко запрограммированное в программное обеспечение сервера базы данных.Но это может быть довольно близко!

Хранимая процедура не устраняет конфликты, но она значительно сокращает промежуток времени, в течение которого может возникнуть конфликт.Вместо того, чтобы «резервировать» номер редакции для ожидающей транзакции INSERT, вы получаете последний номер редакции и обновляете статический счетчик как можно быстрее, убирая путь для следующего вызова на GET_REVISION_NUMBER.(Это, конечно, сериализуется, но только для очень маленькой части процедуры, которая должна выполняться последовательно; в отличие от многих других методов, остальная часть алгоритма может выполняться параллельно.)

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

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

Еда на вынос

Конечно, весь этот обман основан напри необходимости локализованного, полупоследовательного номера.Если вы можете жить с менее удобными номерами ревизий, вы можете просто выставить DRAWING_REVISION.ID.(Разоблачение суррогатных ключей по-своему неприятно, если вы спросите меня.)

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

2 голосов
/ 17 декабря 2010

Вы можете создать триггер вставки, который устанавливает значение оборота

CREATE TRIGGER RevTrigger ON DRAWING
FOR INSERT
AS
WITH ins AS
    (
    SELECT ID, ROW_NUMBER() OVER (PARTITION BY ID ORDER BY {another-column}) AS sequence
    FROM inserted
    WHERE REV IS NULL  -- only update rows where REV is not included
    ),
  draw AS
    (
    SELECT ID, MAX(REV) AS REV
    FROM DRAWING
    GROUP BY ID
    )

UPDATE DRAWING
SET REV = COALESCE(draw.REV + ins.sequence, 0)
FROM DRAWING
JOIN ins ON DRAWING.ID = ins.ID AND DRAWING.{another-column} = ins.{another-column}
JOIN draw ON DRAWING.ID = draw.ID

Вы не указываете, как назначить значение REV, если одновременно вставлено несколько строк, имеющих одинаковое значение идентификатора. Другими словами, как будет назначаться ревизия, если будет добавлено более одной ревизии одновременно?

Это решение предполагает наличие дополнительного столбца, который будет определять последовательность изменений в этом случае (см. {Другой столбец} выше). Если у вас нет такого столбца, измените ORDER BY {another-column} на ORDER BY 0 в функции ROW_NUMBER. И удалите следующее AND DRAWING.{another-column} = ins.{another-column}. После внесения этого изменения все строки вставки с одинаковым идентификатором получат одинаковое значение REV.

РЕДАКТИРОВАТЬ
Сценарий выше работает только на SQL Server 2005 и более поздних версиях. Вот решение, которое будет работать на SQL Server 2000, но не решает проблему нескольких редакций за одну вставку.

CREATE TRIGGER RevTrigger ON DRAWING
FOR INSERT
AS

UPDATE DRAWING
SET REV = COALESCE(draw.REV + 1, 0)
FROM DRAWING
JOIN inserted ON DRAWING.ID = inserted.ID AND DRAWING.{another-column} = inserted.{another-column}
            AND inserted.REV IS NULL
JOIN
    (
    SELECT ID, MAX(REV) AS REV
    FROM DRAWING
    GROUP BY ID
    ) AS draw ON DRAWING.ID = draw.ID
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...