Menambah angka revisi pada kunci komposit tabel

Saya menjalankan SQL Server 2014 secara lokal untuk database yang akan disebarkan ke database Azure SQL V12.

Saya memiliki tabel yang menyimpan nilai properti yang dapat diperluas untuk objek entitas bisnis, dalam hal ini ketiga tabel tersebut terlihat seperti ini:

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 )

Jadi Anda melihat bagaimana WidgetId, PropertyId, Revision merupakan kunci komposit dan tabel menyimpan seluruh riwayat Nilai (nilai saat ini diperoleh dengan mendapatkan baris dengan angka Revision terbesar untuk setiap WidgetId + PropertyId.

Saya ingin tahu bagaimana cara mengatur kolom Revision agar bertambah 1 untuk setiap WidgetId + PropertyId. Saya ingin data seperti ini:

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

IDENTITY tidak akan berfungsi karena bersifat global pada tabel dan hal yang sama berlaku dengan objek SEQUENCE.

Pembaruan Saya dapat memikirkan kemungkinan solusi menggunakan pemicu 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

(Bagi yang belum tahu, pemicu INSTEAD OF INSERT dijalankan alih-alih operasi INSERT apa pun di tabel, dibandingkan dengan pemicu INSERT normal yang dijalankan sebelum atau sesudah operasi INSERT)

Saya pikir ini akan aman untuk konkurensi karena semua operasi INSERT memiliki transaksi implisit, dan setiap pemicu terkait dieksekusi dalam konteks transaksi yang sama, yang berarti aman. Kecuali ada yang bisa mengklaim sebaliknya?


person Dai    schedule 06.01.2016    source sumber
comment
Jika Anda memiliki IDENTITY Anda dapat menggunakan fungsi jendela row_number() untuk menghitung revisi berdasarkan urutan penyisipannya.   -  person Kamil Gosciminski    schedule 06.01.2016
comment
Atau gunakan urutan untuk melakukan saran @ConsiderMe tanpa menambahkan bidang ke tabel itu sendiri   -  person Jorge Campos    schedule 06.01.2016
comment
Persyaratan @ConsiderMe Business berarti nomor Revision harus akurat dan berdekatan sehingga kami dapat mengetahui kapan suatu baris telah dihapus.   -  person Dai    schedule 06.01.2016
comment
@ConsiderMe dan jika saya menggunakan IDENTITY maka kunci komposit tabel berisi kunci pengganti, yang tidak ideal.   -  person Dai    schedule 06.01.2016
comment
@Dai Anda memerlukan semacam kolom untuk dapat menetapkan nomor revisi yang benar jika Anda sudah memiliki data di tabel Anda. Menyimpan informasi tersebut dalam lingkungan multi-pengguna akan menimbulkan masalah karena konkurensi. Namun, ada beberapa solusi untuk hal ini, yang melibatkan penyimpanan bagian relevan yang revisinya diterapkan di tabel lain dan memperoleh kunci tingkat baris.   -  person Kamil Gosciminski    schedule 06.01.2016
comment
@ConsiderMe Bagaimana dengan batasan DEFAULT yang memanggil fungsi yang mendapatkan angka Revision maksimum untuk WidgetId+PropertyId tertentu?   -  person Dai    schedule 06.01.2016
comment
Anda mungkin menemukan beberapa klarifikasi di jawaban @ErwinBrandstetter untuk pertanyaan ini stackoverflow.com/questions/34571125/ - pastikan untuk membaca komentar   -  person Kamil Gosciminski    schedule 06.01.2016
comment
Hai @Dai Anda menginginkan sesuatu seperti ini: sqlfiddle.com/#!6/69e25/5 dengan cepat? Maksud saya pada pernyataan sisipan?   -  person Jorge Campos    schedule 06.01.2016
comment
@ConsiderMe Saya telah mengubah pertanyaan saya dengan saran mengenai penggunaan pemicu.   -  person Dai    schedule 06.01.2016
comment
@ConsiderMe Sebenarnya, ini bisa diperdebatkan. Saya telah memutuskan untuk menggunakan nilai Revision yang tidak bersebelahan dan menggunakan IDENTITY sebagai gantinya.   -  person Dai    schedule 06.01.2016
comment
@Dai sejauh menyangkut solusi pemicu, ada kesenjangan waktu antara memilih dan memasukkan - inilah mengapa solusi ini bisa rusak.   -  person Kamil Gosciminski    schedule 06.01.2016


Jawaban (1)


Kode Anda memiliki kondisi balapan - transaksi bersamaan mungkin memilih dan memasukkan Revisi yang sama antara SELECT dan INSERT Anda. Hal ini dapat menyebabkan pelanggaran kunci (utama) sesekali di lingkungan bersamaan (memaksa Anda mencoba ulang seluruh transaksi).

Daripada mencoba ulang seluruh transaksi, strategi yang lebih baik adalah mencoba lagi INSERT saja. Cukup letakkan kode Anda dalam satu lingkaran, dan jika pelanggaran kunci (dan hanya pelanggaran kunci) terjadi, tingkatkan Revisi dan coba lagi.

Sesuatu seperti ini (menulis dari kepala saya):

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