Sementara OP secara singkat menyentuh gagasan menggunakan Linked-List untuk menyimpan sort-order, ia memiliki banyak keuntungan untuk kasus-kasus di mana item akan sering dipesan ulang.
Saya telah melihat orang-orang menggunakan referensi-diri untuk merujuk pada nilai sebelumnya (atau selanjutnya), tetapi sekali lagi, sepertinya Anda harus memperbarui banyak item lain dalam daftar.
Masalahnya adalah - Anda tidak ! Saat menggunakan daftar-tertaut, penyisipan, penghapusan, dan pemesanan ulang adalah O(1)
operasi, dan integritas referensial yang dipasok oleh basis data memastikan tidak ada referensi yang rusak, catatan yatim, atau loop.
Ini sebuah contoh:
CREATE TABLE Wishlists (
WishlistId int NOT NULL IDENTITY(1,1) PRIMARY KEY,
[Name] nvarchar(200) NOT NULL
);
CREATE TABLE WishlistItems (
ItemId int NOT NULL IDENTITY(1,1),
WishlistId int NOT NULL,
Text nvarchar(200) NOT NULL,
SortAfter int NULL,
CONSTRAINT PK_WishlistItem PRIMARY KEY ( ItemId, WishlistId ),
CONSTRAINT FK_Wishlist_WishlistItem FOREIGN KEY ( WishlistId ) REFERENCES Wishlists ( WishlistId ),
CONSTRAINT FK_Sorting FOREIGN KEY ( SortAfter, WishlistId ) REFERENCES WishlistItems ( ItemId, WishlistId )
);
CREATE UNIQUE INDEX UX_Sorting ON WishlistItems ( SortAfter, WishlistId );
-----
SET IDENTITY_INSERT Wishlists ON;
INSERT INTO Wishlists ( WishlistId, [Name] ) VALUES
( 1, 'Wishlist 1' ),
( 2, 'Wishlist 2' );
SET IDENTITY_INSERT Wishlists OFF;
SET IDENTITY_INSERT WishlistItems ON;
INSERT INTO WishlistItems ( ItemId, WishlistId, [Text], SortAfter ) VALUES
( 1, 1, 'One', NULL ),
( 2, 1, 'Two', 1 ),
( 3, 1, 'Three', 2 ),
( 4, 1, 'Four', 3 ),
( 5, 1, 'Five', 4 ),
( 6, 1, 'Six', 5 ),
( 7, 1, 'Seven', 6 ),
( 8, 1, 'Eight', 7 );
SET IDENTITY_INSERT WishlistItems OFF;
Perhatikan yang berikut ini:
- Menggunakan kunci primer gabungan dan kunci asing di
FK_Sorting
untuk mencegah item agar tidak sengaja merujuk ke item induk yang salah.
- The
UNIQUE INDEX UX_Sorting
melakukan dua peran:
- Karena memungkinkan
NULL
nilai tunggal, setiap daftar hanya dapat memiliki 1 item "head".
- Ini mencegah dua atau lebih item dari mengklaim berada di tempat penyortiran yang sama (dengan mencegah
SortAfter
nilai duplikat ).
Keuntungan utama dari pendekatan ini:
- Tidak pernah membutuhkan penyeimbangan kembali atau pemeliharaan - seperti dengan
int
atau real
-sortir pesanan yang akhirnya kehabisan ruang antara item setelah pemesanan ulang sering.
- Hanya barang yang dipesan ulang (dan saudara mereka) yang perlu diperbarui.
Pendekatan ini memang memiliki kelemahan, namun:
- Anda hanya dapat mengurutkan daftar ini dalam SQL menggunakan CTE Rekursif karena Anda tidak dapat melakukan secara langsung
ORDER BY
.
- Sebagai solusinya, Anda bisa membuat pembungkus
VIEW
atau TVF yang menggunakan CTE untuk menambahkan turunan yang mengandung urutan tambahan - tetapi ini akan mahal untuk digunakan dalam operasi besar.
- Anda harus memuat seluruh daftar ke dalam program Anda untuk menampilkannya - Anda tidak dapat beroperasi pada bagian dari baris karena dengan demikian
SortAfter
kolom akan merujuk ke item yang tidak dimuat ke dalam program Anda.
- Namun memuat semua item untuk daftar mudah karena kunci primer komposit (yaitu lakukan saja
SELECT * FROM WishlistItems WHERE WishlistId = @wishlistToLoad
).
- Melakukan operasi apa pun saat
UX_Sorting
diaktifkan memerlukan dukungan DBMS untuk Kendala yang Ditangguhkan.
- yaitu implementasi ideal dari pendekatan ini tidak akan bekerja di SQL Server sampai mereka menambahkan kembali dukungan untuk Deferrable Constraints dan indeks.
- Solusinya adalah membuat Indeks Unik sebagai Indeks yang Difilter yang memungkinkan beberapa
NULL
nilai di kolom - yang sayangnya berarti bahwa Daftar dapat memiliki beberapa item KEPALA.
- Solusi untuk solusi ini adalah menambahkan kolom ketiga
State
yang merupakan flag sederhana untuk menyatakan jika item daftar "aktif" atau tidak - dan indeks unik mengabaikan item yang tidak aktif.
- Ini adalah sesuatu yang digunakan SQL Server untuk mendukung kembali pada 1990-an dan kemudian mereka secara tidak sengaja menghapus dukungan untuk itu.
Solusi 1: Butuh kemampuan untuk melakukan hal-hal sepele ORDER BY
.
Berikut ini VIEW menggunakan CTE rekursif yang menambahkan SortOrder
kolom:
CREATE VIEW OrderableWishlistItems AS
WITH c ( ItemId, WishlistId, [Text], SortAfter, SortOrder )
AS
(
SELECT
ItemId, WishlistId, [Text], SortAfter, 1 AS SortOrder
FROM
WishlistItems
WHERE
SortAfter IS NULL
UNION ALL
SELECT
i.ItemId, i.WishlistId, i.[Text], i.SortAfter, c.SortOrder + 1
FROM
WishlistItems AS i
INNER JOIN c ON
i.WishlistId = c.WishlistId
AND
i.SortAfter = c.ItemId
)
SELECT
ItemId, WishlistId, [Text], SortAfter, SortOrder
FROM
c;
Anda bisa menggunakan LIHAT ini di kueri lain di mana Anda perlu mengurutkan nilai menggunakan ORDER BY
:
Query:
SELECT * FROM OrderableWishlistItems
Results:
ItemId WishlistId Text SortAfter SortOrder
1 1 One (null) 1
2 1 Two 1 2
3 1 Three 2 3
4 1 Four 3 4
5 1 Five 4 5
6 1 Six 5 6
7 1 Seven 6 7
8 1 Eight 7 8
Solusi 2: Mencegah UNIQUE INDEX
kendala pelanggaran saat melakukan operasi:
Tambahkan State
kolom ke WishlistItems
tabel. Kolom ditandai sebagai HIDDEN
sebagian besar alat ORM (seperti Entity Framework) tidak akan memasukkannya saat membuat model, misalnya.
CREATE TABLE WishlistItems (
ItemId int NOT NULL IDENTITY(1,1),
WishlistId int NOT NULL,
Text nvarchar(200) NOT NULL,
SortAfter int NULL,
[State] bit NOT NULL HIDDEN,
CONSTRAINT PK_WishlistItem PRIMARY KEY ( ItemId, WishlistId ),
CONSTRAINT FK_Wishlist_WishlistItem FOREIGN KEY ( WishlistId ) REFERENCES Wishlists ( WishlistId ),
CONSTRAINT FK_Sorting FOREIGN KEY ( SortAfter, WishlistId ) REFERENCES WishlistItems ( ItemId, WishlistId )
);
CREATE UNIQUE INDEX UX_Sorting ON WishlistItems ( SortAfter, WishlistId ) WHERE [State] = 1;
Operasi:
Menambahkan item baru ke ujung daftar:
- Muat daftar terlebih dahulu untuk menentukan
ItemId
item terakhir saat ini dalam daftar dan simpan @tailItemId
- atau gunakan SELECT MAX( SortOrder ) FROM OrderableWishlistItems WHERE WishlistId = @listId
.
INSERT INTO WishlistItems ( WishlistId, [Text], SortAfter ) VALUES ( @listId, @text, @tailItemId )
.
Menyusun ulang item 4 menjadi di bawah item 7
BEGIN TRANSACTION
DECLARE @itemIdToMove int = 4
DECLARE @itemIdToMoveAfter int = 7
DECLARE @prev int = ( SELECT SortAfter FROM WishlistItems WHERE ItemId = @itemIdToMove )
UPDATE WishlistItems SET [State] = 0 WHERE ItemId IN ( @itemIdToMove , @itemIdToMoveAfter )
UPDATE WishlistItems SET [SortAfter] = @itemIdToMove WHERE ItemId = @itemIdToMoveAfter
UPDATE WishlistItems SET [SortAfter] = @prev WHERE SortAfter = @itemIdToMove
UPDATE WishlistItems SET [State] = 1 WHERE ItemId IN ( @itemIdToMove, @itemIdToMoveAfter )
COMMIT;
Menghapus item 4 dari tengah daftar:
Jika suatu item ada di ujung ekor daftar (yaitu di mana NOT EXISTS ( SELECT 1 FROM WishlistItems WHERE SortAfter = @itemId )
) maka Anda dapat melakukan satu DELETE
.
Jika suatu item memiliki item yang diurutkan setelahnya, Anda melakukan langkah yang sama seperti menata ulang item, kecuali Anda DELETE
setelah itu alih-alih pengaturan State = 1;
.
BEGIN TRANSACTION
DECLARE @itemIdToRemove int = 4
DECLARE @prev int = ( SELECT SortAfter FROM WishlistItems WHERE ItemId = @itemIdToRemove )
UPDATE WishlistItems SET [State] = 0 WHERE ItemId = @itemIdToRemove
UPDATE WishlistItems SET [SortAfter] = @prev WHERE SortAfter = @itemIdToRemove
DELETE FROM WishlistItems WHERE ItemId = @itemIdToRemove
COMMIT;