2012-12-28 17 views
10

Vorrei chiedere aiuto per risolvere il problema con l'ordinamento della struttura gerarchica dei dati archiviata come tabella di chiusura .Ordinamento di una sottostruttura in una struttura gerarchica di dati della tabella di chiusura

Volevo utilizzare questa struttura per memorizzare il menu del mio sito web. Tutto funziona bene, ma il problema è che non so come ordinare la sottostruttura esatta in un ordine personalizzato . Al momento la struttura dell'albero viene ordinata nell'ordine in cui gli articoli sono stati aggiunti al database.

La mia struttura si basa su Bill Karwin's article su Tabelle di chiusura e alcuni altri post.

Qui è la mia struttura del database MySQL con alcuni dati DEMO:

-- 
-- Table `category` 
-- 

CREATE TABLE IF NOT EXISTS `category` (
    `id` int(11) NOT NULL AUTO_INCREMENT, 
    `name` varchar(100) COLLATE utf8_czech_ci NOT NULL, 
    `active` tinyint(1) NOT NULL, 
    PRIMARY KEY (`id`) 
) ENGINE=InnoDB; 


INSERT INTO `category` (`id`, `name`, `active`) VALUES 
(1, 'Cat 1', 1), 
(2, 'Cat 2', 1), 
(3, 'Cat 1.1', 1), 
(4, 'Cat 1.1.1', 1), 
(5, 'Cat 2.1', 1), 
(6, 'Cat 1.2', 1), 
(7, 'Cat 1.1.2', 1); 

-- 
-- Table `category_closure` 
-- 

CREATE TABLE IF NOT EXISTS `category_closure` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT, 
    `ancestor` int(11) DEFAULT NULL, 
    `descendant` int(11) DEFAULT NULL, 
    `depth` int(11) DEFAULT NULL, 
    PRIMARY KEY (`id`), 
    KEY `fk_category_closure_ancestor_category_id` (`ancestor`), 
    KEY `fk_category_closure_descendant_category_id` (`descendant`) 
) ENGINE=InnoDB; 

INSERT INTO `category_closure` (`id`, `ancestor`, `descendant`, `depth`) VALUES 
(1, 1, 1, 0), 
(2, 2, 2, 0), 
(3, 3, 3, 0), 
(4, 1, 3, 1), 
(5, 4, 4, 0), 
(7, 3, 4, 1), 
(8, 1, 4, 2), 
(10, 6, 6, 0), 
(11, 1, 6, 1), 
(12, 7, 7, 0), 
(13, 3, 7, 1), 
(14, 1, 7, 2), 
(16, 5, 5, 0), 
(17, 2, 5, 1); 

Qui è la mia query SELECT per un albero:

SELECT c2.*, cc2.ancestor AS `_parent` 
FROM category AS c1 
JOIN category_closure AS cc1 ON (cc1.ancestor = c1.id) 
JOIN category AS c2 ON (cc1.descendant = c2.id) 
LEFT OUTER JOIN category_closure AS cc2 ON (cc2.descendant = c2.id AND cc2.depth = 1) 
WHERE c1.id = __ROOT__ AND c1.active = 1 
ORDER BY cc1.depth 

Per l'istanza DEMO con __ROOT_ = 1 che ottiene la query:

id name  active  _parent 
1 Cat 1  1   NULL 
3 Cat 1.1  1   1 
6 Cat 1.2  1   1 
4 Cat 1.1.1 1   3 
7 Cat 1.1.2 1   3 

E se, ad esempio, dovessi cambiare l'ordine di Cat 1.1 e Cat 1.2 (in base al nome o ad un ordine personalizzato)?

Ho visto qualche soluzione di breadcrumb (come ordinare per breadcrumb), ma non so come generarli e modificarli.

+0

+1 grazie per la pubblicazione del campione DDL e dati. –

risposta

13

Questa domanda si presenta spesso non solo per la tabella delle chiusure ma anche per altri metodi di memorizzazione dei dati gerarchici. Non è facile in nessuno dei progetti.

La soluzione che ho trovato per Closure Table prevede un join aggiuntivo. Ogni nodo dell'albero si unisce alla catena dei suoi antenati, come una query di tipo "breadcrumb". Quindi utilizzare GROUP_CONCAT() per comprimere i breadcrumb in una stringa separata da virgole, ordinando i numeri ID per profondità nell'albero. Ora hai una stringa con cui puoi ordinare.

SELECT c2.*, cc2.ancestor AS `_parent`, 
    GROUP_CONCAT(breadcrumb.ancestor ORDER BY breadcrumb.depth DESC) AS breadcrumbs 
FROM category AS c1 
JOIN category_closure AS cc1 ON (cc1.ancestor = c1.id) 
JOIN category AS c2 ON (cc1.descendant = c2.id) 
LEFT OUTER JOIN category_closure AS cc2 ON (cc2.descendant = c2.id AND cc2.depth = 1) 
JOIN category_closure AS breadcrumb ON (cc1.descendant = breadcrumb.descendant) 
WHERE c1.id = 1/*__ROOT__*/ AND c1.active = 1 
GROUP BY cc1.descendant 
ORDER BY breadcrumbs; 

+----+------------+--------+---------+-------------+ 
| id | name  | active | _parent | breadcrumbs | 
+----+------------+--------+---------+-------------+ 
| 1 | Cat 1  |  1 | NULL | 1   | 
| 3 | Cat 1.1 |  1 |  1 | 1,3   | 
| 4 | Cat 1.1.1 |  1 |  3 | 1,3,4  | 
| 7 | Cat 1.1.2 |  1 |  3 | 1,3,7  | 
| 6 | Cat 1.2 |  1 |  1 | 1,6   | 
+----+------------+--------+---------+-------------+ 

Avvertenze:

  • i valori ID devono avere lunghezza uniforme, perché l'ordinamento "1,3" e "1,6" e "1327" potrebbe non dare l'ordine che si intende. Ma ordinando "001,003" e "001,006" e "001,327" lo farebbero. Pertanto, è necessario avviare i valori di identificazione a 1000000+, oppure utilizzare ZEROFILL per antenato e discendente nella tabella category_closure.
  • In questa soluzione l'ordine di visualizzazione dipende dall'ordine numerico degli ID di categoria. Questo ordine numerico di valori id non può rappresentare l'ordine in cui si desidera visualizzare l'albero. O si potrebbe desiderare la libertà di modificare l'ordine di visualizzazione indipendentemente dai valori di identificazione numerica. In alternativa, è possibile che gli stessi dati di categoria vengano visualizzati in più di un albero, ciascuno con un diverso ordine di visualizzazione.
    Se è necessaria maggiore libertà, è necessario memorizzare i valori di ordinamento separatamente dagli ID e la soluzione diventa ancora più complessa. Ma nella maggior parte dei progetti, è accettabile utilizzare una scorciatoia, dando il doppio dovere dell'ID della categoria come ordine di visualizzazione dell'albero.

Re tuo commento:

Sì, è possibile memorizzare "ordinamento fratello", come un'altra colonna della tabella di chiusura, quindi utilizzare tale valore, invece di ancestor per costruire la stringa di pangrattato. Ma se lo fai, finisci con un sacco di ridondanza dei dati. Cioè, un dato antenato è memorizzato su più righe, una per ogni percorso che discende da esso. Quindi è necessario memorizzare lo stesso valore per l'ordinamento di pari livello su tutte quelle righe, il che crea il rischio di un'anomalia.

L'alternativa sarebbe quella di creare un'altra tabella, con solo una riga per ciascun antenato distinto nell'albero e unirsi a quella tabella per ottenere l'ordine di pari livello.

CREATE TABLE category_closure_order (
    ancestor INT PRIMARY KEY, 
    sibling_order SMALLINT UNSIGNED NOT NULL DEFAULT 1 
); 

SELECT c2.*, cc2.ancestor AS `_parent`, 
    GROUP_CONCAT(o.sibling_order ORDER BY breadcrumb.depth DESC) AS breadcrumbs 
FROM category AS c1 
JOIN category_closure AS cc1 ON (cc1.ancestor = c1.id) 
JOIN category AS c2 ON (cc1.descendant = c2.id) 
LEFT OUTER JOIN category_closure AS cc2 ON (cc2.descendant = c2.id AND cc2.depth = 1) 
JOIN category_closure AS breadcrumb ON (cc1.descendant = breadcrumb.descendant) 
JOIN category_closure_order AS o ON breadcrumb.ancestor = o.ancestor 
WHERE c1.id = 1/*__ROOT__*/ AND c1.active = 1 
GROUP BY cc1.descendant 
ORDER BY breadcrumbs; 

+----+------------+--------+---------+-------------+ 
| id | name  | active | _parent | breadcrumbs | 
+----+------------+--------+---------+-------------+ 
| 1 | Cat 1  |  1 | NULL | 1   | 
| 3 | Cat 1.1 |  1 |  1 | 1,1   | 
| 4 | Cat 1.1.1 |  1 |  3 | 1,1,1  | 
| 7 | Cat 1.1.2 |  1 |  3 | 1,1,2  | 
| 6 | Cat 1.2 |  1 |  1 | 1,2   | 
+----+------------+--------+---------+-------------+ 
+0

Grazie mille Mr. Karwin, quindi quando ho bisogno di più libertà su un certo livello dell'albero e creare un ordine personalizzato lì (per impostare l'ordine delle voci di menu che hanno lo stesso genitore), allora posso aggiungere sth come "lo stesso colonna "livello di priorità" o è una cattiva idea? – JCZ

+0

Grazie mille signor Karwin, ho (spero che sia successo) implementato l'opzione con un terzo tavolo. Sei grande. – JCZ

+0

Curioso, perché non memorizzare l'ordinamento nella tabella delle categorie, in questo modo non è necessaria una terza tabella? Quando ho fatto l'ordinamento nell'albero ancestrale, ho sempre usato un double, in modo che quando un nuovo nodo deve essere inserito tra i nodi 1 e 2, posso dargli sortValue 1.5 – TheSmileyCoder

Problemi correlati