Mentre l'OP ha brevemente toccato l'idea di utilizzare un Elenco collegato per memorizzare l'ordinamento, presenta molti vantaggi per i casi in cui gli articoli verranno riordinati frequentemente.
Ho visto persone che usano un riferimento personale per riferirsi al valore precedente (o successivo), ma sembra che dovresti aggiornare un sacco di altri elementi nell'elenco.
Il fatto è che non lo fai ! Quando si utilizza un elenco collegato, l'inserimento, l'eliminazione e il riordino sono O(1)
operazioni e l'integrità referenziale imposta dal database assicura che non vi siano riferimenti interrotti, record orfani o loop.
Ecco un esempio:
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;
Nota quanto segue:
- Utilizzo di una chiave primaria composita e una chiave esterna
FK_Sorting
per impedire che gli articoli si riferiscano accidentalmente all'elemento padre errato.
- Il
UNIQUE INDEX UX_Sorting
esegue due funzioni:
- Poiché consente un singolo
NULL
valore, ogni elenco può contenere solo 1 elemento "head".
- Impedisce a due o più articoli di dichiarare di trovarsi nello stesso posto di ordinamento (impedendo
SortAfter
valori duplicati ).
I principali vantaggi di questo approccio:
- Non richiede mai il ribilanciamento o la manutenzione, come nel caso degli ordini di ordinamento basati su
int
o real
che alla fine esauriscono lo spazio tra gli oggetti dopo frequenti riordini.
- Solo gli articoli riordinati (e i loro fratelli) devono essere aggiornati.
Questo approccio presenta degli svantaggi, tuttavia:
- È possibile ordinare questo elenco solo in SQL utilizzando un CTE ricorsivo perché non è possibile eseguire un'operazione semplice
ORDER BY
.
- Per ovviare al problema, è possibile creare un wrapper
VIEW
o TVF che utilizza un CTE per aggiungere un derivato contenente un ordinamento incrementale, ma questo sarebbe costoso da utilizzare in operazioni di grandi dimensioni.
- È necessario caricare l'intero elenco nel programma per visualizzarlo: non è possibile operare su un sottoinsieme delle righe perché la
SortAfter
colonna farà riferimento agli elementi che non sono stati caricati nel programma.
- Tuttavia, caricare tutti gli elementi per un elenco è facile grazie alla chiave primaria composita (ovvero basta
SELECT * FROM WishlistItems WHERE WishlistId = @wishlistToLoad
).
- L'esecuzione di qualsiasi operazione mentre
UX_Sorting
è abilitata richiede il supporto DBMS per i vincoli differiti.
- ovvero l'implementazione ideale di questo approccio non funzionerà in SQL Server fino a quando non aggiungeranno il supporto per vincoli e indici differibili.
- Una soluzione alternativa consiste nel rendere l'Indice univoco un indice filtrato che consenta più
NULL
valori nella colonna, il che sfortunatamente significa che un elenco può contenere più elementi HEAD.
- Una soluzione alternativa per questa soluzione alternativa consiste nell'aggiungere una terza colonna
State
che è un semplice flag per dichiarare se un elemento dell'elenco è "attivo" o meno - e l'indice univoco ignora gli elementi inattivi.
- Questo è qualcosa che SQL Server ha usato per supportare negli anni '90 e quindi ha rimosso inspiegabilmente il supporto per esso.
Soluzione 1: è necessaria la capacità di eseguire un'operazione banale ORDER BY
.
Ecco una VISTA che utilizza un CTE ricorsivo che aggiunge una SortOrder
colonna:
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;
È possibile utilizzare questa VISTA in altre query in cui è necessario ordinare i valori utilizzando 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
Soluzione 2: prevenzione dei UNIQUE INDEX
vincoli di violazione durante l'esecuzione delle operazioni:
Aggiungi una State
colonna alla WishlistItems
tabella. La colonna è contrassegnata in HIDDEN
modo tale che la maggior parte degli strumenti ORM (come Entity Framework) non la includerà durante la generazione di modelli, ad esempio.
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;
operazioni:
Aggiunta di un nuovo elemento alla fine dell'elenco:
- Caricare prima l'elenco per determinare l'
ItemId
ultimo dell'ultimo elemento nell'elenco e archiviarlo in @tailItemId
- o utilizzare SELECT MAX( SortOrder ) FROM OrderableWishlistItems WHERE WishlistId = @listId
.
INSERT INTO WishlistItems ( WishlistId, [Text], SortAfter ) VALUES ( @listId, @text, @tailItemId )
.
Riordinare l'articolo 4 in modo che sia inferiore all'articolo 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;
Rimozione dell'elemento 4 dal centro dell'elenco:
Se un elemento si trova alla fine della lista (cioè dove NOT EXISTS ( SELECT 1 FROM WishlistItems WHERE SortAfter = @itemId )
), allora puoi fare un singolo DELETE
.
Se un articolo ha un oggetto ordinato dopo di esso, esegui gli stessi passaggi del riordino di un oggetto, tranne te DELETE
in seguito invece di impostare 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;