การเพิ่มหมายเลขการแก้ไขในคีย์ผสมของตาราง

ฉันใช้ SQL Server 2014 ภายในเครื่องสำหรับฐานข้อมูลที่จะนำไปใช้กับฐานข้อมูล Azure SQL V12

ฉันมีตารางที่เก็บค่าของคุณสมบัติที่ขยายได้สำหรับออบเจ็กต์เอนทิตีธุรกิจ ในกรณีนี้ทั้งสามตารางจะมีลักษณะดังนี้:

CREATE TABLE Widgets (
    WidgetId bigint IDENTITY(1,1),
    ...
)

CREATE TABLE WidgetProperties (
    PropertyId int IDENTITY(1,1),
    Name       nvarchar(50)
    Type       int -- 0 = int, 1 = string, 2 = date, etc
)

CREATE TABLE WidgetPropertyValues (
    WidgetId   bigint,
    PropertyId int,
    Revision   int,
    DateTime   datetimeoffset(7),
    Value      varbinary(255)

    CONSTRAINT [PK_WidgetPropertyValues] PRIMARY KEY CLUSTERED (
        [WidgetId] ASC,
        [PropertyIdId] ASC,
        [Revision] ASC
    )
)

ALTER TABLE dbo.WidgetPropertyValues WITH CHECK ADD CONSTRAINT FK_WidgetPropertyValues_WidgetProperties FOREIGN KEY( PropertyId )
REFERENCES dbo.WidgetProperties ( PropertyId )

ALTER TABLE dbo.WidgetPropertyValues WITH CHECK ADD  CONSTRAINT FK_WidgetPropertyValues_Widgets FOREIGN KEY( WidgetId )
REFERENCES dbo.Widgets ( WidgetId )

ดังนั้นคุณจะเห็นว่า WidgetId, PropertyId, Revision เป็นคีย์ผสมอย่างไร และตารางจะจัดเก็บประวัติของค่าทั้งหมด (ค่าปัจจุบันได้มาจากการรับแถวที่มีหมายเลข Revision มากที่สุดสำหรับ WidgetId + PropertyId แต่ละตัว

ฉันต้องการทราบว่าฉันสามารถตั้งค่าคอลัมน์ Revision ให้เพิ่มขึ้นทีละ 1 สำหรับแต่ละ WidgetId + PropertyId ได้อย่างไร ฉันต้องการข้อมูลเช่นนี้:

WidgetId, PropertyId, Revision, DateTime, Value
------------------------------------------------
       1           1         1               123
       1           1         2               456
       1           1         3               789
       1           2         1               012

IDENTITY จะไม่ทำงานเนื่องจากเป็นแบบโกลบอลในตารางและเช่นเดียวกันกับวัตถุ SEQUENCE

อัปเดต ฉันสามารถคิดถึงวิธีแก้ปัญหาที่เป็นไปได้โดยใช้ทริกเกอร์ INSTEAD OF INSERT:

CREATE TRIGGER WidgetPropertyValueInsertTrigger ON WidgetPropertyValues
    INSTEAD OF INSERT
AS
BEGIN
    DECLARE @maxRevision int
    SELECT @maxRevision = ISNULL( MAX( Revision ), 0 ) FROM WidgetPropertyValues WHERE WidgetId = INSERTED.WidgetId AND PropertyId = INSERTED.PropertyId

    INSERT INTO WidgetPropertyValues VALUES (
        INSERTED.WidgetId,
        INSERTED.PropertyId,
        @maxRevision + 1,
        INSERTED.DateTime,
        INSERTED.Value,
    )
END

(สำหรับผู้ที่ไม่ได้ฝึกหัด ทริกเกอร์ INSTEAD OF INSERT จะทำงานแทนการดำเนินการ INSERT ใดๆ บนโต๊ะ เมื่อเปรียบเทียบกับทริกเกอร์ INSERT ปกติที่ทำงานก่อนหรือหลังการดำเนินการ INSERT)

ฉันคิดว่านี่จะปลอดภัยในการทำงานพร้อมกันเนื่องจากการดำเนินการ INSERT ทั้งหมดมีธุรกรรมโดยนัย และทริกเกอร์ที่เกี่ยวข้องใด ๆ จะถูกดำเนินการในบริบทธุรกรรมเดียวกัน ซึ่งหมายความว่าปลอดภัย เว้นแต่จะมีใครสามารถเรียกร้องเป็นอย่างอื่นได้?


person Dai    schedule 06.01.2016    source แหล่งที่มา
comment
หากคุณมี IDENTITY คุณสามารถใช้ฟังก์ชัน row_number() window เพื่อระบุการแก้ไขตามลำดับการแทรก   -  person Kamil Gosciminski    schedule 06.01.2016
comment
หรือใช้ลำดับเพื่อทำตามคำแนะนำ @ConsiderMe โดยไม่ต้องเพิ่มฟิลด์ลงในตาราง   -  person Jorge Campos    schedule 06.01.2016
comment
ข้อกำหนดทางธุรกิจของ @ConsiderMe หมายความว่าหมายเลข Revision จะต้องมีความถูกต้องและต่อเนื่องกัน เพื่อให้เราสามารถบอกได้ว่าแถวใดถูกลบไปแล้ว   -  person Dai    schedule 06.01.2016
comment
@ConsiderMe และถ้าฉันใช้ IDENTITY คีย์คอมโพสิตของตารางจะมีคีย์ตัวแทนซึ่งไม่เหมาะ   -  person Dai    schedule 06.01.2016
comment
@Dai คุณต้องมีคอลัมน์บางประเภทเพื่อให้สามารถกำหนดหมายเลขการแก้ไขที่ถูกต้องได้หากคุณมีข้อมูลในตารางอยู่แล้ว การจัดเก็บข้อมูลดังกล่าวในสภาพแวดล้อมที่มีผู้ใช้หลายคนอาจทำให้เกิดปัญหาเนื่องจากการทำงานพร้อมกัน มีวิธีแก้ไขปัญหาบางอย่างสำหรับสิ่งนี้ ซึ่งเกี่ยวข้องกับการจัดเก็บส่วนที่เกี่ยวข้องซึ่งมีการนำการแก้ไขไปใช้ในตารางอื่นและการรับการล็อกระดับแถว   -  person Kamil Gosciminski    schedule 06.01.2016
comment
@ConsiderMe แล้วข้อ จำกัด DEFAULT ที่เรียกใช้ฟังก์ชันที่ได้รับหมายเลข Revision สูงสุดสำหรับ WidgetId+PropertyId ที่กำหนดล่ะ   -  person Dai    schedule 06.01.2016
comment
คุณอาจพบคำชี้แจงใน @ErwinBrandstetter คำตอบสำหรับคำถามนี้ stackoverflow.com/questions/34571125/ - อย่าลืมอ่านความคิดเห็น   -  person Kamil Gosciminski    schedule 06.01.2016
comment
@ConsiderMe ฉันได้แก้ไขคำถามพร้อมข้อเสนอแนะเกี่ยวกับการใช้งานทริกเกอร์   -  person Dai    schedule 06.01.2016
comment
@ConsiderMe จริงๆแล้วนี่เป็นที่น่าสงสัย ฉันตัดสินใจที่จะใช้ค่า Revision ที่ไม่ต่อเนื่องกัน และใช้ IDENTITY แทน   -  person Dai    schedule 06.01.2016
comment
@Dai เท่าที่เกี่ยวข้องกับโซลูชันทริกเกอร์ มีช่องว่างเวลาระหว่างการเลือกและการแทรก - นี่คือสาเหตุว่าทำไมมันถึงพัง   -  person Kamil Gosciminski    schedule 06.01.2016


คำตอบ (1)


รหัสของคุณมีสภาวะการแข่งขัน - ธุรกรรมที่เกิดขึ้นพร้อมกันอาจเลือกและแทรกการแก้ไขเดียวกันระหว่าง SELECT และ INSERT ของคุณ ซึ่งอาจทำให้เกิดการละเมิดคีย์เป็นครั้งคราว (หลัก) ในสภาพแวดล้อมที่เกิดขึ้นพร้อมกัน (บังคับให้คุณลองธุรกรรมทั้งหมดอีกครั้ง)

แทนที่จะลองธุรกรรมทั้งหมดอีกครั้ง กลยุทธ์ที่ดีกว่าคือลองเฉพาะ INSERT อีกครั้ง เพียงใส่โค้ดของคุณวนซ้ำ และหากเกิดการละเมิดคีย์ (และการละเมิดคีย์ เท่านั้น) ให้เพิ่มการแก้ไขแล้วลองอีกครั้ง

บางอย่างเช่นนี้ (เขียนจากหัวของฉัน):

DECLARE @maxRevision int = (
    SELECT
        @maxRevision = ISNULL(MAX(Revision), 0)
    FROM
        WidgetPropertyValues
    WHERE
        WidgetId = INSERTED.WidgetId
        AND PropertyId = INSERTED.PropertyId
);

WHILE 0 = 0 BEGIN

    SET @maxRevision = @maxRevision + 1;

    BEGIN TRY

        INSERT INTO WidgetPropertyValues
        VALUES (
            INSERTED.WidgetId,
            INSERTED.PropertyId,
            @maxRevision,
            INSERTED.DateTime,
            INSERTED.Value,
        );

        BREAK;

    END TRY
    BEGIN CATCH

        -- The error was different from key violation,
        -- in which case we just pass it back to caller.
        IF ERROR_NUMBER() <> 2627
            THROW;

        -- Otherwise, this was a key violation, and we can let the loop 
        -- enter the next iteration (to retry with the incremented value).

    END CATCH

END
person Branko Dimitrijevic    schedule 06.01.2016