原文連接: http://www.pilishen.com/posts...; 歡迎做客咱們的php&Laravel學習羣:109256050
laravel-nestedset是一個關係型數據庫遍歷樹的larvel4-5的插件包php
目錄:前端
開始使用node
Nested Set Model 是一種實現有序樹的高明的方法,它快速且不須要遞歸查詢,例如無論樹有多少層,你能夠僅使用一條查詢來獲取某個節點下的全部的後代,缺點是它的插入、移動、刪除須要執行復雜的sql語句,可是這些都在這個插件內處理了!
更多關於詳見維基百科!Nested set model 及它中文翻譯!嵌套集合模型laravel
強烈建議使用支持事物功能的數據引擎(像MySql的innoDb)來防止可能的數據損壞。算法
在composer.json
文件中加入下面代碼:sql
"kalnoy/nestedset": "^4.3",
運行composer install
來安裝它。數據庫
或者直接在命令行輸入json
composer require kalnoy/nestedset
如需安裝歷史版本請點擊更多版本數組
你可使用NestedSet
類的columns
方法來添加有默認名字的字段:app
... use Kalnoy\Nestedset\NestedSet; Schema::create('table', function (Blueprint $table) { ... NestedSet::columns($table); });
刪除字段:
... use Kalnoy\Nestedset\NestedSet; Schema::table('table', function (Blueprint $table) { NestedSet::dropColumns($table); });
默認的字段名爲:_lft
、_rgt
、parent_id
,源碼以下:
public static function columns(Blueprint $table) { $table->unsignedInteger(self::LFT)->default(0); $table->unsignedInteger(self::RGT)->default(0); $table->unsignedInteger(self::PARENT_ID)->nullable(); $table->index(static::getDefaultColumns()); }
你的模型須要使用Kalnoy\Nestedset\NodeTrait
trait 來實現nested sets
use Kalnoy\Nestedset\NodeTrait; class Foo extends Model { use NodeTrait; }
public function getLftName() { return 'left'; } public function getRgtName() { return 'right'; } public function getParentIdName() { return 'parent'; } // Specify parent id attribute mutator public function setParentAttribute($value) { $this->setParentIdAttribute($value); }
若是你的數據庫結構樹包含 parent_id
字段信息,你須要添加下面兩欄字段到你的藍圖文件:
$table->unsignedInteger('_lft'); $table->unsignedInteger('_rgt');
設置好你的模型後你只須要修復你的結構樹來填充_lft
和_rgt
字段:
MyModel::fixTree();
Node具備如下功能,他們功能徹底且被預加載:
假設咱們有一個Category模型;變量$node是該模型的一個實例是咱們操做的node(節點)。它能夠爲一個新建立的node或者是從數據庫中取出的node
每次插入或者移動一個節點都要執行好幾條數據庫操做,全部強烈推薦使用transaction.
注意! 對於v4.2.0版本不是自動開啓transaction的,另外node的結構化操做須要在模型上手動執行save,可是有些方法會隱性執行save並返回操做後的布爾類型的結果。
當你簡單的建立一個node,它會被添加到樹的末端。
Category::create($attributes); // 自動save爲一個根節點(root)
或者
$node = new Category($attributes); $node->save(); // save爲一個根節點(root)
在這裏node被設置爲root,意味着它沒有父節點
// #1 隱性 save $node->saveAsRoot(); // #2 顯性 save $node->makeRoot()->save();
若是你想添加子節點,你能夠添加爲父節點的第一個子節點或者最後一個子節點。
*在下面的例子中, $parent
爲已存在的節點
添加到父節點的末端的方法包括:
// #1 使用延遲插入 $node->appendToNode($parent)->save(); // #2 使用父節點 $parent->appendNode($node); // #3 藉助父節點的children關係 $parent->children()->create($attributes); // #5 藉助子節點的parent關係 $node->parent()->associate($parent)->save(); // #6 藉助父節點屬性 $node->parent_id = $parent->id; $node->save(); // #7 使用靜態方法 Category::create($attributes, $parent);
添加到父節點的前端的方法
// #1 $node->prependToNode($parent)->save(); // #2 $parent->prependNode($node);
你可使用下面的方法來將$node
添加爲指定節點$neighbor
的相鄰節點
$neighbor
必須存在,$node
能夠爲新建立的節點,也能夠爲已存在的,若是$node
爲已存在的節點,它將移動到新的位置與$neighbor
相鄰,必要時它的父級將改變。
# 顯性save $node->afterNode($neighbor)->save(); $node->beforeNode($neighbor)->save(); # 隱性 save $node->insertAfterNode($neighbor); $node->insertBeforeNode($neighbor);
但使用create
靜態方法時,它將檢查數組是否包含children
鍵,若是有的話,將遞歸建立更多的節點。
$node = Category::create([ 'name' => 'Foo', 'children' => [ [ 'name' => 'Bar', 'children' => [ [ 'name' => 'Baz' ], ], ], ], ]);
如今$node->children
包含一組已建立的節點。
你能夠輕鬆的重建一個樹,這對於大量的修改的樹結構的保存很是有用。Category::rebuildTree($data, $delete);
$data
爲表明節點的數組
$data = [ [ 'id' => 1, 'name' => 'foo', 'children' => [ ... ] ], [ 'name' => 'bar' ], ];
上面有一個name
爲foo
的節點,它有指定的id
,表明這個已存在的節點將被填充,若是這個節點不存在,就好拋出一個ModelNotFoundException
,另外,這個節點還有children
數組,這個數組也會以相同的方式添加到foo
節點內。bar
節點沒有主鍵,就是不存在,它將會被建立。$delete
表明是否刪除數據庫中已存在的可是$data
中不存在的數據,默認爲不刪除。
重建子樹
對於4.3.8版本之後你能夠重建子樹
Category::rebuildSubtree($root, $data);
這將限制只重建$root子樹
在某些狀況下咱們須要使用變量$id表明目標節點的主鍵id
Ancestors 建立一個節點的父級鏈,這對於展現當前種類的麪包屑頗有幫助。
Descendants 是一個父節點的全部子節點。
Ancestors和Descendants均可以預加載。
// Accessing ancestors $node->ancestors; // Accessing descendants $node->descendants;
經過自定義的查詢加載ancestors和descendants:
$result = Category::ancestorsOf($id); $result = Category::ancestorsAndSelf($id); $result = Category::descendantsOf($id); $result = Category::descendantsAndSelf($id);
大多數狀況下,你須要按層級排序:
$result = Category::defaultOrder()->ancestorsOf($id);
祖先集合能夠被預加載:
$categories = Category::with('ancestors')->paginate(30); // 視圖模板中麪包屑: @foreach($categories as $i => $category) <small> $category->ancestors->count() ? implode(' > ', $category->ancestors->pluck('name')->toArray()) : 'Top Level' </small><br> $category->name @endforeach
將祖先的name
所有取出後轉換爲數組,在用>拼接爲字符串輸出。
有相同父節點的節點互稱爲兄弟節點
$result = $node->getSiblings(); $result = $node->siblings()->get();
獲取相鄰的後面兄弟節點:
// 獲取相鄰的下一個兄弟節點 $result = $node->getNextSibling(); // 獲取後面的全部兄弟節點 $result = $node->getNextSiblings(); // 使用查詢得到全部兄弟節點 $result = $node->nextSiblings()->get();
獲取相鄰的前面兄弟節點:
// 獲取相鄰的前一個兄弟節點 $result = $node->getPrevSibling(); // 獲取前面的全部兄弟節點 $result = $node->getPrevSiblings(); // 使用查詢得到全部兄弟節點 $result = $node->prevSiblings()->get();
假設每個category has many goods, 而且 hasMany 關係已經創建,怎麼樣簡單的獲取$category 和它全部後代下全部的goods?
// 獲取後代的id $categories = $category->descendants()->pluck('id'); // 包含Category自己的id $categories[] = $category->getKey(); // 得到goods $goods = Goods::whereIn('category_id', $categories)->get();
若是你須要知道node的出入那一層級:
$result = Category::withDepth()->find($id); $depth = $result->depth;
根節點(root)是第0層(level 0),root的子節點是第一層(level 1),以此類推
你可使用having
約束來得到特定的層級的節點
$result = Category::withDepth()->having('depth', '=', 1)->get();
注意 這在數據庫嚴格模式下無效
全部的節點都是在內部嚴格組織的,默認狀況下沒有順序,因此節點是隨機展示的,這部影響展示,你能夠按字母和其餘的順序對節點排序。
可是在一些狀況下按層級展現是必要的,它對獲取祖先和用於菜單順序有用。
使用deaultOrder運用樹的排序:$result = Category::defaultOrder()->get();
你也可使用倒序排序:$result = Category::reversed()->get();
讓節點在父級內部上下移動來改變默認排序:
$bool = $node->down(); $bool = $node->up(); // 向下移動3個兄弟節點 $bool = $node->down(3);
操做返回根據操做的節點的位置是否改變的布爾值
不少約束條件能夠被用到這些查詢構造器上:
祖先約束
$result = Category::whereAncestorOf($node)->get(); $result = Category::whereAncestorOrSelf($id)->get();
$node
能夠爲模型的主鍵或者模型實例
後代約束
$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(); //結果集合中包含目標node自身 $result = Category::whereDescendantOrSelf($node)->get();
在獲取了node的結果集合後,咱們就能夠將它轉化爲樹,例如:$tree = Category::get()->toTree();
這將在每一個node上添加parent 和 children 關係,且你可使用遞歸算法來渲染樹:
$nodes = Category::get()->toTree(); $traverse = function ($categories, $prefix = '-') use (&$traverse) { foreach ($categories as $category) { echo PHP_EOL.$prefix.' '.$category->name; $traverse($category->children, $prefix.'-'); } }; $traverse($nodes);
這將像下面相似的輸出:
- Root -- Child 1 --- Sub child 1 -- Child 2 - Another root
你也能夠構建一個扁平樹:將子節點直接放於父節點後面。當你獲取自定義排序的節點和不想使用遞歸來循環你的節點時頗有用。$nodes = Category::get()->toFlatTree();
以前的例子將向下面這樣輸出:
Root Child 1 Sub child 1 Child 2 Another root
有時你並不須要加載整個樹而是隻須要一些特定的子樹:$root = Category::descendantsAndSelf($rootId)->toTree()->first();
經過一個簡單的查詢咱們就能夠得到子樹的根節點和使用children關係獲取它全部的後代
若是你不須要$root節點自己,你能夠這樣:$tree = Category::descendantsOf($rootId)->toTree($rootId);
刪掉一個節點:
$node->delete();
注意!節點的全部後代將一併刪除
注意! 節點須要向模型同樣刪除,不能使用下面的語句來刪除節點:
Category::where('id', '=', $id)->delete();
這將破壞樹結構
支持SoftDeletes
trait,且在模型層
檢查節點是否爲其餘節點的子節點$bool = $node->isDescendantOf($parent);
檢查是否爲根節點$bool = $node->isRoot();
其餘的檢查
你能夠檢查樹是否被破環$bool = Category::isBroken();
獲取錯誤統計:$data = Category::countErrors();
它將返回含有一下鍵的數組
從v3.1日後支持修復樹,經過parent_id字段的繼承信息,給每一個node設置合適的lft 和 rgt值Node::fixTree();
假設你有個Memu模型和MenuItems.他們之間是one-to-many 關係。MenuItems有menu_id屬性並實現nested sets模型。顯然你想基於menu_id屬性來單獨處理每一個樹,爲了實現這樣的功能,咱們須要指定這個menu_id屬性爲scope屬性。
protected function getScopeAttributes() { return [ 'menu_id' ]; }
如今咱們爲了實現自定義的查詢,咱們須要提供須要限制做用域的屬性。
MenuItem::scoped([ 'menu_id' => 5 ])->withDepth()->get(); // OK MenuItem::descendantsOf($id)->get(); // WRONG: returns nodes from other scope MenuItem::scoped([ 'menu_id' => 5 ])->fixTree();
但使用model實例查詢node,scope自動基於設置的限制做用域屬性來刪選node。例如:
$node = MenuItem::findOrFail($id); $node->siblings()->withDepth()->get(); // OK
使用實例來獲取刪選的查詢:$node->newScopedQuery();
注意,當經過主鍵獲取模型時不須要使用scope
$node = MenuItem::findOrFail($id); // OK $node = MenuItem::scoped([ 'menu_id' => 5 ])->findOrFail(); // OK, 可是多餘