Embora o OP tenha abordado brevemente a noção de uso de uma lista vinculada para armazenar a ordem de classificação, ele possui muitas vantagens nos casos em que os itens serão reordenados com frequência.
Vi pessoas usando uma auto-referência para se referir ao valor anterior (ou próximo), mas, novamente, parece que você precisaria atualizar muitos outros itens da lista.
A questão é - você não ! Ao usar uma lista vinculada, inserção, exclusão e reordenação são O(1)
operações, e a integridade referencial imposta pelo banco de dados garante que não haja referências quebradas, registros órfãos ou loops.
Aqui está um exemplo:
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;
Observe o seguinte:
- Usando uma chave primária e uma chave estrangeira compostas
FK_Sorting
para impedir que os itens se refiram acidentalmente ao item pai errado.
- O
UNIQUE INDEX UX_Sorting
executa duas funções:
- Como permite um
NULL
valor único, cada lista pode ter apenas 1 item "principal".
- Impede que dois ou mais itens reivindiquem estar no mesmo local de classificação (impedindo
SortAfter
valores duplicados ).
As principais vantagens dessa abordagem:
- Nunca requer reequilíbrio ou manutenção - como ocorre com as ordens de classificação baseadas
int
ou real
que acabam ficando sem espaço entre os itens após reordenamentos frequentes.
- Somente itens reordenados (e seus irmãos) precisam ser atualizados.
Essa abordagem tem desvantagens, no entanto:
- Você só pode classificar essa lista no SQL usando um CTE recursivo porque não pode fazer uma tarefa direta
ORDER BY
.
- Como solução alternativa, você pode criar um wrapper
VIEW
ou TVF que use um CTE para adicionar um derivado contendo uma ordem de classificação incremental - mas isso seria caro para uso em grandes operações.
- Você deve carregar a lista inteira no seu programa para exibi-la - você não pode operar em um subconjunto das linhas, porque a
SortAfter
coluna se referirá aos itens que não foram carregados no seu programa.
- No entanto, o carregamento de todos os itens para uma lista é fácil devido à chave primária composta (ou seja, basta fazê-lo
SELECT * FROM WishlistItems WHERE WishlistId = @wishlistToLoad
).
- A execução de qualquer operação enquanto
UX_Sorting
estiver ativada requer suporte do DBMS para restrições adiadas.
- ou seja, a implementação ideal dessa abordagem não funcionará no SQL Server até que eles adicionem suporte adicional a restrições e índices adiados.
- Uma solução alternativa é tornar o Índice Único um Índice Filtrado que permita vários
NULL
valores na coluna - o que infelizmente significa que uma Lista pode ter vários itens HEAD.
- Uma solução alternativa para essa solução alternativa é adicionar uma terceira coluna,
State
que é um sinalizador simples para declarar se um item da lista está "ativo" ou não - e o índice exclusivo ignora itens inativos.
- Isso é algo que o SQL Server usou para oferecer suporte na década de 1990 e, em seguida, eles inexplicavelmente removeram o suporte.
Solução alternativa 1: precisa da capacidade de executar uma tarefa trivial ORDER BY
.
Aqui está uma VIEW usando uma CTE recursiva que adiciona uma SortOrder
coluna:
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;
Você pode usar esse VIEW em outras consultas nas quais você precisa classificar valores usando 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
Solução alternativa 2: Evitando UNIQUE INDEX
restrições de violação ao executar operações:
Adicione uma State
coluna à WishlistItems
tabela. A coluna está marcada como HIDDEN
a maioria das ferramentas ORM (como o Entity Framework) não a inclui ao gerar modelos, por exemplo.
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;
Operações:
Adicionando um novo item ao final da lista:
- Carregue a lista primeiro para determinar o
ItemId
último item atual na lista e armazene @tailItemId
- ou use SELECT MAX( SortOrder ) FROM OrderableWishlistItems WHERE WishlistId = @listId
.
INSERT INTO WishlistItems ( WishlistId, [Text], SortAfter ) VALUES ( @listId, @text, @tailItemId )
.
Reordenando o item 4 para ficar abaixo do 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;
Removendo o item 4 do meio da lista:
Se um item estiver no final da lista (ou seja, onde NOT EXISTS ( SELECT 1 FROM WishlistItems WHERE SortAfter = @itemId )
), você poderá fazer um único DELETE
.
Se um item tiver um item classificado após ele, execute as mesmas etapas para reordenar um item, exceto você DELETE
depois em vez de definir 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;