Veröffentlicht am
Nested Set
Vor einiger Zeit musste ich mir ein Tool zum bearbeiten der Menüleiste überlegen. Es musste relativ einfach zu bedienen sein. Im Netz bin ich auf das Nested Set Modell gestoßen was mich gleich faszinierte. Ich hatte schon etwas Erfahrung mit hierachischen Strukturen, aber einfach zu bedienen sind die nicht. Einem Eintrag muss immer ein Vorgänger zugewiesen werden und das Auslesen gestalltet sich auch etwas schwerer.
Ein Beispiel: Test
ID | Bezeichnung | parent_ID | Sortierung |
1 | Reptilien | 0 | 1 |
2 | Frosch | 1 | 4 |
3 | Krokodil | 1 | 5 |
4 | Primaten | 0 | 2 |
5 | Schimpanse | 4 | 6 |
6 | Orangutan | 4 | 7 |
7 | Bonobo | 4 | 11 |
8 | Vögel | 0 | 3 |
9 | Star | 8 | 13 |
10 | Rabe | 8 | 14 |
11 | Elster | 8 | 15 |
12 | Kanari | 8 | 16 |
13 | Test 01 | 6 | 8 |
14 | Test 02 | 6 | 9 |
15 | Test 03 | 6 | 10 |
16 | Gorilla | 4 | 12 |
An diesem Beispiel sieht man wie eine Hierarchische Struktur aussieht. Jedes Element besitzt eine ID, eine Bezeichnung und eine parrent_ID.
Die parent_ID spiegelt die ID wieder. Reptielien hat die ID 1. Bei Frosch und Krokodil ist diese in der parent_ID. Somit kann der Frosch und das Krokodil den Reptilien zugeordnet werden.
Sortiert soll das Ganze auch noch ausgegeben werden. Dazu wird die Spalte Sortierung benötigt.
Nun kann man mit einer Schleife die Elemente nach parrent_ID auslesen und zusammenbauen. Dazu muss in jeder Schleife auf die Datenbank zugegriffen werden was für die Performanz eher nicht so toll ist.
Da die beiden Felder für parent_ID und Sortierung schon vorhanden sind, warum diese nicht etwas besser nutzen und das Nested Set Modell verwenden?
Das Nested Set Model
Arne Klempert hat das Thema Nested Set auf seiner Seite sehr schön beschrieben. Wer mehr dazu wissen will kann sich seinen Artikel gern durchlesen.
Nested Set verwendet die Mengenlehre um Hirarchische Strukturen abzubilden. Datenbanktechnisch ist dieses Model sehr leicht auszulesen und die Geschwindigkeit ist auch nicht zu verachten.
Hr. Klempert zeigt sehr schön wie in das Nested Set Elemente eingefügt oder auch gelöscht werden können. In seinem Artikel fehlte mir nur: Wie rücke ich ein Element ein oder aus?
Ein paar Excel Tests später war die Lösung eigentlich ganz einfach.
Aber der Reihe nach
Als erstes benötigen wir eine Tabelle in der Datenbank.
CREATE TABLE tree (
id INT(12) UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
lft INT(12) UNSIGNED NOT NULL,
rgt INT(12) UNSIGNED NOT NULL,
PRIMARY KEY (id)
);
Nachdem die Tabelle angelegt wurde können wir auch schon ein paar Daten einpflegen.
INSERT INTO `tree` (`id`, `name`, `lft`, `rgt`) VALUES
(1, 'Reptilien', 1, 6),
(2, 'Frosch', 2, 3),
(3, 'Krokodil', 4, 5),
(4, 'Primaten', 7, 14),
(5, 'Schimpanse', 8, 9),
(6, 'Orangutan', 10, 11),
(7, 'Bonobo', 12, 13),
(8, 'Vögel', 15, 24),
(9, 'Star', 16, 17),
(10, 'Rabe', 18, 19),
(11, 'Elster', 20, 21),
(12, 'Kanari', 22, 23);
Wie einzelne Nodes (Knoten) hinzugefügt werden, dazu kommen wir noch.
Nun schauen wir uns mal an wie der Baum aussehen soll.
Reptilien
ZFrosch
ZKrokodil
Primaten
ZSchimpanse
ZOrangutan
ZBonobo
Vögel
ZStar
ZRabe
ZElster
ZKanari
Dies ist ein einfacher Baum. Für dieses Beispiel aber ausreichend. Nun basteln wir uns ein Formular zum bearbeiten. Jede Zeile bekommt 5 Buttons. Einen zum Löschen, einen für nach oben und unten verschieben, einen zum einrücken und einen zum ausrücken.
Zum füllen des Formulars benötigen wir ersteinmal den Code zum auslesen. (Die Verbindung zur Datenbank via PDO setzte ich voraus)
PHP Code: auslesen.php
$sql = "SELECT n.id, n.name, n.lft, n.rgt, COUNT(*)-1 AS ebene
FROM tree AS n, tree AS p
WHERE n.lft BETWEEN p.lft AND p.rgt GROUP BY n.lft ORDER BY n.lft";
$rst = $pdo->prepare($sql);
$rst->execute();
$arr = array();
while($row = $rst->fetch(PDO::FETCH_ASSOC)){
array_push($arr,$row);
}
echo json_encode($arr);
Um die Daten die von dieser Datei zurückgegeben werden auch verarbeiten zu können und anschaulich auszugeben, benötigen wir noch eine HTML Datei. Um uns das Leben etwas einfacher zu machen benötigen wir zusätzlich JQuery.
HTML Code: index.php
JavaScript: JQuery, EasyUI
Font: IcoMoon (Download auf der IcoMoon Seite)
<!DOCTYPE HTML>
<html>
<head>
<title>Titel der in der Tableiste angezeigt wird</title>
<meta charset="utf-8">
<script src="js/jquery.min.js"></script>
<script src="js/jquery.easyui.min.js"></script>
<link rel="stylesheet" href="js/themes/metro/easyui.css"></link>
<link rel="stylesheet" href="js/themes/color.css"></link>
</head>
<body>
<table id="bearbtab">
<thead>
<th>DELETE</th>
<th>UP</th>
<th>DOWN</th>
<th>LEFT</th>
<th>RIGHT</th>
<th>name</th>
<th>id</th>
<th>lft</th>
<th>rgt</th>
<th>ebene</th>
</thead>
<tbody></tbody>
</table>
<label for="mname">Neuer Name:</label>
<input type="text" maxlength="100" id="mname" name="mname" class="easyui-textbox">
<label for="mid">Einfügen nach:</label>
<select id="mid" name="mid" data-options="valueField:'id',textField:'name',url:'php/auslesen.php'" class="easyui-combobox" style="width:100px;"></select>
<button onclick="EinfuegenNach()" class="easyui-linkbutton">Speichern</button>
</body>
</html>
Noch ein wenig Styling
#bearbtab td:nth-child(1) button { background-color: #f00; }
.mup { display: block; transform: rotate(270deg); }
.mdown{ display: block; transform: rotate(90deg); }
Das Grundgerüst für die Bearbeitung unsere Struktur steht schon mal. Nun muss noch der Inhalt geladen und eingefügt werden. Dazu benötigen wir dieses Script.
function laden(){
$.ajax({
url:"php/auslesen.php"
})
.done(function(data){
var obj = $.parseJSON(data);
var tstr = "";
var placeholder = "-";
var tempph = "";
var anzahl = Object.keys(obj).length;
var i = 1;
var tmplinks = 0;
var links=0;
$.each(obj,function(key,val){
links=parseInt(val.ebene);
tstr += "<tr>";
// Löschen
tstr += "<td><button onclick=\"openWindowLoeschen("+val.id+")\">X</button></td>";
// UP
if(i!=1){tstr += "<td><button onclick=\"nodeUP("+val.id+")\"><span class=\"mup\">></span></button></td>";}else{tstr += "<td></td>";}
// DOWN
if(i!=anzahl){tstr += "<td><button onclick=\"nodeDOWN("+val.id+")\"><span class=\"mdown\">></span></button></td>";}else{tstr += "<td></td>";}
// LEFT
if(links<tmplinks){
tstr += "<td> </td>";
}else{
tstr += "<td><button onclick=\"toLeft("+val.id+")\"><</button></td>";
}
// RIGHT
if(links>tmplinks){
tstr += "<td> </td>";
}else{
tstr += "<td><button onclick=\"toRight("+val.id+")\">></button></td>";
}
tmplinks=links;
// NAME + Einzug (-)
tempph = placeholder.repeat(val.ebene);
tstr += "<td><span class=\"im\">"+tempph+"</span>"+val.name+"</td>";
// ID
tstr += "<td>"+val.id+"</td>";
// LFT
tstr += "<td>"+val.lft+"</td>";
// RGT
tstr += "<td>"+val.rgt+"</td>";
// EBENE
tstr += "<td>"+val.ebene+"</td>";
tstr += "</tr>";
i++;
});
$("tbody").html(tstr);
});
}
Ergebnis:
So sollte die Seite nun aussehen. Natürlich könnte man das etwas hübscher gestalten, aber hier geht es um das Wesentliche, die Funktion. Gestalten könnt Ihr das nach eurem Geschmacksempfinden.
DELETE | UP | DOWN | LEFT | RIGHT | name | id | lft | rgt | ebene |
---|---|---|---|---|---|---|---|---|---|
Reptilien | 1 | 1 | 6 | 0 | |||||
-Frosch | 2 | 2 | 3 | 1 | |||||
-Krokodil | 3 | 4 | 5 | 1 | |||||
Primaten | 4 | 7 | 14 | 0 | |||||
-Schimpanse | 5 | 8 | 9 | 1 | |||||
-Orangutan | 6 | 10 | 11 | 1 | |||||
-Bonobo | 7 | 12 | 13 | 1 | |||||
Vögel | 8 | 15 | 24 | 0 | |||||
-Star | 9 | 16 | 17 | 1 | |||||
-Rabe | 10 | 18 | 19 | 1 | |||||
-Elster | 11 | 20 | 21 | 1 | |||||
-Kanari | 12 | 22 | 23 | 1 |
Nachdem dies erledigt ist können wir mit den einzelnen Funktionen der Buttons anfangen.