Dies ist ein Laravel 4-10-Paket für die Arbeit mit Bäumen in relationalen Datenbanken.
Laravel 11.0 wird seit v6.0.4 unterstützt
Laravel 10.0 wird seit v6.0.2 unterstützt
Laravel 9.0 wird seit v6.0.1 unterstützt
Laravel 8.0 wird seit v6.0.0 unterstützt
Laravel 5.7, 5.8, 6.0, 7.0 wird seit v5 unterstützt
Laravel 5.5, 5.6 wird seit v4.3 unterstützt
Laravel 5.2, 5.3, 5.4 wird seit v4 unterstützt
Laravel 5.1 wird in v3 unterstützt
Laravel 4 wird in v2 unterstützt
Inhalt:
Theorie
Dokumentation
Knoten einfügen
Knoten abrufen
Knoten löschen
Konsistenzprüfung und -korrektur
Scoping
Anforderungen
Installation
Verschachtelte Mengen oder das Nested-Set-Modell sind eine Möglichkeit, hierarchische Daten effektiv in einer relationalen Tabelle zu speichern. Aus Wikipedia:
Das Nested-Set-Modell besteht darin, die Knoten gemäß einer Baumdurchquerung zu nummerieren, bei der jeder Knoten zweimal besucht wird und Nummern in der Reihenfolge des Besuchs und bei beiden Besuchen zugewiesen werden. Damit bleiben für jeden Knoten zwei Zahlen übrig, die als zwei Attribute gespeichert werden. Die Abfrage wird kostengünstig: Durch den Vergleich dieser Zahlen kann die Hierarchiezugehörigkeit getestet werden. Eine Aktualisierung erfordert eine Neunummerierung und ist daher teuer.
NSM zeigt eine gute Leistung, wenn der Baum selten aktualisiert wird. Es ist darauf abgestimmt, schnell verwandte Knoten abzurufen. Es eignet sich ideal für die Erstellung von Menüs oder Kategorien mit mehreren Tiefen für den Shop.
Angenommen, wir haben ein Modell Category
; Eine $node
Variable ist eine Instanz dieses Modells und des Knotens, den wir manipulieren. Es kann sich um ein neues Modell oder eines aus der Datenbank handeln.
Der Knoten verfügt über die folgenden Beziehungen, die voll funktionsfähig sind und problemlos geladen werden können:
Der Knoten gehört zum parent
Knoten hat viele children
Knoten hat viele ancestors
Knoten hat viele descendants
Das Verschieben und Einfügen von Knoten umfasst mehrere Datenbankabfragen, daher wird dringend empfohlen, Transaktionen zu verwenden.
WICHTIG! Ab v4.2.0 wird die Transaktion nicht automatisch gestartet
Ein weiterer wichtiger Hinweis ist, dass Strukturmanipulationen verzögert werden, bis Sie auf save
im Modell“ klicken (einige Methoden rufen implizit save
auf und geben ein boolesches Ergebnis der Operation zurück).
Wenn das Modell erfolgreich gespeichert wurde, bedeutet das nicht, dass der Knoten verschoben wurde. Wenn Ihre Anwendung davon abhängt, ob der Knoten tatsächlich seine Position geändert hat, verwenden Sie die Methode hasMoved
:
if ($node->save()) {$moved = $node->hasMoved(); }
Wenn Sie einfach einen Knoten erstellen, wird dieser an das Ende des Baums angehängt:
Kategorie::create($attributes); // Als Root gespeichert
$node = new Category($attributes);$node->save(); // Als Root gespeichert
In diesem Fall wird der Knoten als Wurzel betrachtet, was bedeutet, dass er keinen übergeordneten Knoten hat.
// #1 Implizites save$node->saveAsRoot();// #2 Explizites save$node->makeRoot()->save();
Der Knoten wird an das Ende des Baums angehängt.
Wenn Sie einen Knoten zu einem untergeordneten Knoten eines anderen Knotens machen möchten, können Sie ihn zum letzten oder ersten untergeordneten Knoten machen.
In den folgenden Beispielen ist $parent
ein vorhandener Knoten.
Es gibt mehrere Möglichkeiten, einen Knoten anzuhängen:
// #1 Verwendung des verzögerten insert$node->appendToNode($parent)->save();// #2 Verwendung des übergeordneten Knotens$parent->appendNode($node);// #3 Verwendung der Kinderbeziehung des Elternteils$parent- >children()->create($attributes);// #5 Verwendung der übergeordneten Beziehung des Knotens$node->parent()->associate($parent)->save();// #6 Verwendung des übergeordneten Elements attribute$node->parent_id = $parent->id;$node->save();// #7 Statische Methode verwendenCategory::create($attributes, $parent);
Und nur ein paar Möglichkeiten, es voranzustellen:
// #1$node->prependToNode($parent)->save();// #2$parent->prependNode($node);
Mit den folgenden Methoden können Sie $node
zum Nachbarn des Knotens $neighbor
machen:
$neighbor
muss vorhanden sein, der Zielknoten kann frisch sein. Wenn ein Zielknoten vorhanden ist, wird er an die neue Position verschoben und der übergeordnete Knoten wird bei Bedarf geändert.
# Explizites save$node->afterNode($neighbor)->save();$node->beforeNode($neighbor)->save();# Implizites save$node->insertAfterNode($neighbor);$node-> insertBeforeNode($neighbor);
Bei Verwendung der statischen Methode create
on node“ wird geprüft, ob Attribute children
Schlüssel enthalten. Wenn dies der Fall ist, werden rekursiv weitere Knoten erstellt.
$node = Category::create(['name' => 'Foo','children' => [ ['name' => 'Bar','children' => [ [ 'name' => 'Baz' ], ], ], ], ]);
$node->children
enthält jetzt eine Liste der erstellten untergeordneten Knoten.
Sie können einen Baum leicht wieder aufbauen. Dies ist nützlich, um die Struktur des Baums massenhaft zu ändern.
Category::rebuildTree($data, $delete);
$data
ist ein Array von Knoten:
$data = [ [ 'id' => 1, 'name' => 'foo', 'children' => [ ... ] ], [ 'name' => 'bar' ], ];
Für den Knoten mit dem Namen foo
ist eine ID angegeben, was bedeutet, dass der vorhandene Knoten gefüllt und gespeichert wird. Wenn der Knoten nicht vorhanden ist, wird ModelNotFoundException
ausgelöst. Außerdem sind für diesen Knoten untergeordnete children
angegeben, bei denen es sich ebenfalls um ein Array von Knoten handelt. Sie werden auf die gleiche Weise verarbeitet und als untergeordnete Elemente des Knotens foo
gespeichert.
Für die bar
ist kein Primärschlüssel angegeben, daher wird er erstellt.
$delete
zeigt an, ob Knoten gelöscht werden sollen, die bereits vorhanden, aber nicht in $data
vorhanden sind. Standardmäßig werden Knoten nicht gelöscht.
Ab 4.2.8 können Sie einen Teilbaum neu erstellen:
Category::rebuildSubtree($root, $data);
Dies schränkt die Baumneubildung auf Nachkommen des $root
Knotens ein.
In einigen Fällen verwenden wir eine $id
Variable, die eine ID des Zielknotens ist.
Vorfahren bilden eine Kette von Eltern zum Knoten. Hilfreich für die Anzeige von Breadcrumbs zur aktuellen Kategorie.
Nachkommen sind alle Knoten in einem Unterbaum, d. h. Kinder von Knoten, Kinder von Kindern usw.
Sowohl Vorfahren als auch Nachkommen können eifrig geladen werden.
// Zugriff auf Vorfahren$node->ancestors;// Zugriff auf Nachkommen$node->descendants;
Es ist möglich, Vorfahren und Nachkommen mithilfe einer benutzerdefinierten Abfrage zu laden:
$result = Category::ancestorsOf($id);$result = Category::ancestorsAndSelf($id);$result = Category::descendantsOf($id);$result = Category::descendantsAndSelf($id);
In den meisten Fällen müssen Sie Ihre Vorfahren nach der Ebene ordnen:
$result = Category::defaultOrder()->ancestorsOf($id);
Eine Sammlung von Vorfahren kann eifrig geladen werden:
$categories = Category::with('ancestors')->paginate(30);// in view for breadcrumbs:@foreach($categories as $i => $category) <small>{{ $category->ancestors->count() ? implode(' > ', $category->ancestors->pluck('name')->toArray()) : 'Top Level' }}</small><br> {{ $category->name }} @endforeach
Geschwister sind Knoten, die denselben übergeordneten Knoten haben.
$result = $node->getSiblings();$result = $node->siblings()->get();
Um nur nächste Geschwister zu bekommen:
// Holen Sie sich ein Geschwister, das unmittelbar nach dem Knoten liegt. $result = $node->getNextSibling();// Holen Sie sich alle Geschwister, die nach dem Knoten liegen. $result = $node->getNextSiblings();// Holen Sie sich alle Geschwister mit a query$result = $node->nextSiblings()->get();
So erhalten Sie frühere Geschwister:
// Holen Sie sich ein Geschwister, das unmittelbar vor dem Knoten liegt$result = $node->getPrevSibling();// Holen Sie sich alle Geschwister, die vor dem Knoten liegen$result = $node->getPrevSiblings();// Holen Sie sich alle Geschwister mit a query$result = $node->prevSiblings()->get();
Stellen Sie sich vor, dass jede Kategorie has many
. Das heißt, HasMany
-Beziehung wird hergestellt. Wie können Sie alle Waren der $category
und alle ihre Nachkommen erhalten? Einfach!
// IDs der Nachkommen abrufen$categories = $category->descendants()->pluck('id');// Die ID der Kategorie selbst einschließen$categories[] = $category->getKey();// Waren abrufen $goods = Goods::whereIn('category_id', $categories)->get();
Wenn Sie wissen möchten, auf welcher Ebene sich der Knoten befindet:
$result = Category::withDepth()->find($id);$ Depth = $result-> Depth;
Der Wurzelknoten hat die Ebene 0. Die untergeordneten Knoten der Wurzelknoten haben die Ebene 1 usw.
Um Knoten einer bestimmten Ebene zu erhalten, können Sie having
Einschränkungen anwenden:
$result = Category::withDepth()->having(' Depth', '=', 1)->get();
WICHTIG! Dies funktioniert nicht im datenbankstrikten Modus
Alle Knoten sind intern streng organisiert. Standardmäßig wird keine Reihenfolge angewendet, sodass Knoten möglicherweise in zufälliger Reihenfolge angezeigt werden. Dies hat keinen Einfluss auf die Anzeige eines Baums. Sie können Knoten nach Alphabet oder einem anderen Index ordnen.
Aber in manchen Fällen ist eine hierarchische Ordnung unerlässlich. Es wird zum Abrufen von Vorfahren benötigt und kann zum Bestellen von Menüpunkten verwendet werden.
Um die Baumreihenfolge anzuwenden, wird die Methode defaultOrder
verwendet:
$result = Category::defaultOrder()->get();
Sie können Knoten in umgekehrter Reihenfolge erhalten:
$result = Category::reversed()->get();
So verschieben Sie den Knoten innerhalb des übergeordneten Knotens nach oben oder unten, um die Standardreihenfolge zu beeinflussen:
$bool = $node->down();$bool = $node->up();// Knoten um 3 Geschwister verschieben$bool = $node->down(3);
Das Ergebnis der Operation ist ein boolescher Wert, der angibt, ob der Knoten seine Position geändert hat.
Verschiedene Einschränkungen, die auf den Abfrage-Generator angewendet werden können:
whereIsRoot(), um nur Wurzelknoten abzurufen;
hasParent(), um Nicht-Root-Knoten abzurufen;
whereIsLeaf() um nur Blätter zu bekommen;
hasChildren(), um Nicht-Leave-Knoten abzurufen;
whereIsAfter($id), um jeden Knoten (nicht nur Geschwister) abzurufen, der nach einem Knoten mit der angegebenen ID liegt;
whereIsBefore($id), um jeden Knoten abzurufen, der vor einem Knoten mit der angegebenen ID liegt.
Einschränkungen für Nachkommen:
$result = Category::whereDescendantOf($node)->get();$result = Category::whereNotDescendantOf($node)->get();$result = Category::orWhereDescendantOf($node)->get() ;$result = Category::orWhereNotDescendantOf($node)->get();$result = Category::whereDescendantAndSelf($id)->get();// Zielknoten in Ergebnismenge einbeziehen$result = Category::whereDescendantOrSelf($node)->get();
Einschränkungen für Vorfahren:
$result = Category::whereAncestorOf($node)->get();$result = Category::whereAncestorOrSelf($id)->get();
$node
kann entweder ein Primärschlüssel des Modells oder eine Modellinstanz sein.
Nachdem Sie einen Satz Knoten erhalten haben, können Sie ihn in einen Baum umwandeln. Zum Beispiel:
$tree = Category::get()->toTree();
Dadurch werden parent
und children
Beziehungen auf jedem Knoten im Satz ausgefüllt und Sie können einen Baum mithilfe eines rekursiven Algorithmus rendern:
$nodes = Category::get()->toTree();$traverse = function ($categories, $prefix = '-') use (&$traverse) {foreach ($categories as $category) {echo PHP_EOL.$ Präfix.' '.$category->name;$traverse($category->children, $prefix.'-'); } };$traverse($nodes);
Dies wird etwa Folgendes ausgeben:
- Root -- Child 1 --- Sub child 1 -- Child 2 - Another root
Sie können auch einen flachen Baum erstellen: eine Liste von Knoten, bei denen sich untergeordnete Knoten unmittelbar nach dem übergeordneten Knoten befinden. Dies ist hilfreich, wenn Sie Knoten mit benutzerdefinierter Reihenfolge (z. B. alphabetisch) erhalten und keine Rekursion verwenden möchten, um über Ihre Knoten zu iterieren.
$nodes = Category::get()->toFlatTree();
Das vorherige Beispiel gibt Folgendes aus:
Root Child 1 Sub child 1 Child 2 Another root
Manchmal muss nicht der gesamte Baum geladen werden, sondern nur ein Teilbaum eines bestimmten Knotens. Dies wird im folgenden Beispiel gezeigt:
$root = Category::descendantsAndSelf($rootId)->toTree()->first();
In einer einzigen Abfrage erhalten wir eine Wurzel eines Unterbaums und aller seiner Nachkommen, auf die über children
zugegriffen werden kann.
Wenn Sie $root
Knoten selbst nicht benötigen, gehen Sie stattdessen wie folgt vor:
$tree = Category::descendantsOf($rootId)->toTree($rootId);
So löschen Sie einen Knoten:
$node->delete();
WICHTIG! Alle Nachkommen dieses Knotens werden ebenfalls gelöscht!
WICHTIG! Knoten müssen als Modelle gelöscht werden. Versuchen Sie nicht, sie mit einer Abfrage wie dieser zu löschen:
Category::where('id', '=', $id)->delete();
Das wird den Baum zerstören!
SoftDeletes
-Merkmal wird unterstützt, auch auf Modellebene.
So überprüfen Sie, ob der Knoten ein Nachkomme eines anderen Knotens ist:
$bool = $node->isDescendantOf($parent);
So überprüfen Sie, ob der Knoten ein Root-Knoten ist:
$bool = $node->isRoot();
Weitere Kontrollen:
$node->isChildOf($other);
$node->isAncestorOf($other);
$node->isSiblingOf($other);
$node->isLeaf()
Sie können überprüfen, ob ein Baum kaputt ist (d. h. strukturelle Fehler aufweist):
$bool = Category::isBroken();
Es ist möglich, Fehlerstatistiken abzurufen:
$data = Category::countErrors();
Es wird ein Array mit den folgenden Schlüsseln zurückgegeben:
oddness
– die Anzahl der Knoten mit falschen lft
und rgt
-Werten
duplicates
– die Anzahl der Knoten, die dieselben lft
oder rgt
-Werte haben
wrong_parent
– die Anzahl der Knoten mit einem ungültigen parent_id
-Wert, der nicht den lft
und rgt
-Werten entspricht
missing_parent
– die Anzahl der Knoten, deren parent_id
auf einen nicht vorhandenen Knoten verweist
Seit Version 3.1 kann der Baum jetzt repariert werden. Mithilfe der Vererbungsinformationen aus der Spalte parent_id
werden für jeden Knoten die richtigen _lft
und _rgt
Werte festgelegt.
Node::fixTree();
Stellen Sie sich vor, Sie haben Menu
Modell und MenuItems
. Zwischen diesen Modellen besteht eine Eins-zu-viele-Beziehung. MenuItem
verfügt über das Attribut menu_id
zum Zusammenfügen von Modellen. MenuItem
enthält verschachtelte Mengen. Es ist offensichtlich, dass Sie jeden Baum basierend auf dem Attribut menu_id
separat verarbeiten möchten. Dazu müssen Sie dieses Attribut als Scope-Attribut angeben:
geschützte Funktion getScopeAttributes() {return [ 'menu_id' ]; }
Um nun jedoch eine benutzerdefinierte Abfrage auszuführen, müssen Sie Attribute bereitstellen, die für die Bereichsbestimmung verwendet werden:
MenuItem::scoped([ 'menu_id' => 5 ])->withDepth()->get(); // OKMenuItem::descendantsOf($id)->get(); // FALSCH: gibt Knoten von anderen zurückscopeMenuItem::scoped([ 'menu_id' => 5 ])->fixTree(); // OK
Beim Anfordern von Knoten mithilfe einer Modellinstanz werden Bereiche automatisch basierend auf den Attributen dieses Modells angewendet:
$node = MenuItem::findOrFail($id);$node->siblings()->withDepth()->get(); // OK
So rufen Sie den bereichsbezogenen Abfrage-Generator mithilfe einer Instanz ab:
$node->newScopedQuery();
Verwenden Sie beim Eager Loading immer eine bereichsbezogene Abfrage:
MenuItem::scoped([ 'menu_id' => 5])->with('descendants')->findOrFail($id); // OKMenuItem::with('descendants')->findOrFail($id); // FALSCH
PHP >= 5.4
Laravel >= 4.1
Es wird dringend empfohlen, eine Datenbank zu verwenden, die Transaktionen unterstützt (wie InnoDb von MySql), um einen Baum vor möglicher Beschädigung zu schützen.
Um das Paket zu installieren, gehen Sie im Terminal wie folgt vor:
composer require kalnoy/nestedset
Für Benutzer von Laravel 5.5 und höher:
Schema::create('table', function (Blueprint $table) {...$table->nestedSet(); });// Um Spalten zu löschenSchema::table('table', function (Blueprint $table) {$table->dropNestedSet(); });
Für frühere Laravel-Versionen:
...verwende KalnoyNestedsetNestedSet; Schema::create('table', function (Blueprint $table) {...NestedSet::columns($table); });
So löschen Sie Spalten:
...verwende KalnoyNestedsetNestedSet; Schema::table('table', function (Blueprint $table) { NestedSet::dropColumns($table); });
Ihr Modell sollte das Merkmal KalnoyNestedsetNodeTrait
verwenden, um verschachtelte Mengen zu ermöglichen:
use KalnoyNestedsetNodeTrait;class Foo erweitert Model {use NodeTrait; }
Wenn Ihre vorherige Erweiterung einen anderen Spaltensatz verwendet hat, müssen Sie nur die folgenden Methoden in Ihrer Modellklasse überschreiben:
öffentliche Funktion getLftName() {return 'left'; }öffentliche Funktion getRgtName() {return 'right'; }öffentliche Funktion getParentIdName() {return 'parent'; }// Eltern-ID-Attribut angeben mutatorpublic function setParentAttribute($value) {$this->setParentIdAttribute($value); }
Wenn Ihr Baum parent_id
Informationen enthält, müssen Sie Ihrem Schema zwei Spalten hinzufügen:
$table->unsignedInteger('_lft');$table->unsignedInteger('_rgt');
Nachdem Sie Ihr Modell eingerichtet haben, müssen Sie nur noch den Baum reparieren, um die Spalten _lft
und _rgt
zu füllen:
MyModel::fixTree();
Copyright (c) 2017 Alexander Kalnoy
Hiermit wird jeder Person, die eine Kopie dieser Software und der zugehörigen Dokumentationsdateien (die „Software“) erhält, kostenlos die Erlaubnis erteilt, mit der Software ohne Einschränkung zu handeln, einschließlich und ohne Einschränkung der Rechte zur Nutzung, zum Kopieren, Ändern und Zusammenführen , Kopien der Software zu veröffentlichen, zu verteilen, unterzulizenzieren und/oder zu verkaufen und Personen, denen die Software zur Verfügung gestellt wird, dies zu gestatten, vorbehaltlich der folgenden Bedingungen:
Der obige Urheberrechtshinweis und dieser Genehmigungshinweis müssen in allen Kopien oder wesentlichen Teilen der Software enthalten sein.
DIE SOFTWARE WIRD „WIE BESEHEN“ ZUR VERFÜGUNG GESTELLT, OHNE JEGLICHE AUSDRÜCKLICHE ODER STILLSCHWEIGENDE GEWÄHRLEISTUNG, EINSCHLIESSLICH, ABER NICHT BESCHRÄNKT AUF DIE GEWÄHRLEISTUNG DER MARKTGÄNGIGKEIT, EIGNUNG FÜR EINEN BESTIMMTEN ZWECK UND NICHTVERLETZUNG. IN KEINEM FALL SIND DIE AUTOREN ODER URHEBERRECHTSINHABER HAFTBAR FÜR JEGLICHE ANSPRÜCHE, SCHÄDEN ODER ANDERE HAFTUNG, WEDER AUS EINER VERTRAGLICHEN HANDLUNG, AUS HANDLUNG ODER ANDERWEITIG, DIE SICH AUS, AUS ODER IN ZUSAMMENHANG MIT DER SOFTWARE ODER DER NUTZUNG ODER ANDEREN HANDELN IN DER SOFTWARE ERGEBEN SOFTWARE.