laravel-nestedset是一個關係型資料庫遍曆樹的larvel4-5的外掛程式包,本文主要和大家分享laravel-nestedset多級無限分類,希望能協助到大家。
目錄:
Nested Sets Model簡介
安裝要求
安裝
開始使用
遷移檔案
插入節點
擷取節點
刪除節點
一致性檢查和修複
範圍
Nested Sets Model簡介
Nested Set Model 是一種實現有序樹的高明的方法,它快速且不需要遞迴查詢,例如不管樹有多少層,你可以僅使用一條查詢來擷取某個節點下的所有的後代,缺點是它的插入、移動、刪除需要執行複雜的sql語句,但是這些都在這個外掛程式內處理了!
更多關於詳見維基百科!Nested set model 及它中文翻譯!嵌套集合模型
安裝要求
強烈建議使用支援事物功能的資料引擎(像MySql的innoDb)來防止可能的資料損毀。
安裝
在composer.json
檔案中加入下面代碼:
"kalnoy/nestedset": "^4.3",
運行composer install
來安裝它。
或者直接在命令列輸入
composer require kalnoy/nestedset
如需安裝曆史版本請點擊更多版本
開始使用
遷移檔案
你可以使用NestedSet
類的columns
方法來添加有預設名字的欄位:
...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;}
遷移其他地方已有的資料
從其他的nested set 模型庫遷移
public function getLftName(){ return 'left';}public function getRgtName(){ return 'right';}public function getParentIdName(){ return 'parent';}// Specify parent id attribute mutatorpublic function setParentAttribute($value){ $this->setParentIdAttribute($value);}
從其他的具有父子關係的模型庫遷移
如果你的資料庫結構樹包含 parent_id
欄位資訊,你需要添加下面兩欄欄位到你的藍圖檔案:
$table->unsignedInteger('_lft');$table->unsignedInteger('_rgt');
設定好你的模型後你只需要修複你的結構樹來填充_lft
和_rgt
欄位:
MyModel::fixTree();
關係
Node具有以下功能,他們功能完全且被預先載入:
假設我們有一個Category模型;變數$node是該模型的一個執行個體是我們操作的node(節點)。它可以為一個新建立的node或者是從資料庫中取出的node
插入節點(node)
每次插入或者移動一個節點都要執行好幾條資料庫操作,所有強烈推薦使用transaction.
注意! 對於v4.2.0版本不是自動開啟transaction的,另外node的結構化操作需要在模型上手動執行save,但是有些方法會隱性執行save並返回操作後的布爾類型的結果。
建立節點(node)
當你簡單的建立一個node,它會被添加到樹的末端。
Category::create($attributes); // 自動save為一個根節點(root)
或者
$node = new Category($attributes);$node->save(); // save為一個根節點(root)
在這裡node被設定為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();
擷取表的相關model
假設每一個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深度(depth)
如果你需要知道node的出入那一層級:
$result = Category::withDepth()->find($id);$depth = $result->depth;
根節點(root)是第0層(level 0),root的子節點是第一層(level 1),以此類推
你可以使用having
約束來獲得特定的層級的節點
$result = Category::withDepth()->having('depth', '=', 1)->get();
注意 這在資料庫strict 模式下無效
預設排序
所有的節點都是在內部嚴格組織的,預設情況下沒有順序,所以節點是隨機展現的,這部影響展現,你可以按字母和其他的順序對節點排序。
但是在一些情況下按層級展示是必要的,它對擷取祖先和用於菜單順序有用。
使用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();
之前的例子將向下面這樣輸出:
RootChild 1Sub child 1Child 2Another root
構建一個子樹
有時你並不需要載入整個樹而是只需要一些特定的子樹:
$root = Category::descendantsAndSelf($rootId)->toTree()->first();
通過一個簡單的查詢我們就可以獲得子樹的根節點和使用children關係擷取它所有的後代
如果你不需要$root節點本身,你可以這樣:
$tree = Category::descendantsOf($rootId)->toTree($rootId);
刪除節點
刪掉一個節點:
$node->delete();
注意!節點的所有後代將一併刪除
注意! 節點需要向模型一樣刪除,不能使用下面的語句來刪除節點:
Category::where('id', '=', $id)->delete();
這將破壞樹結構
支援SoftDeletes
trait,且在模型層
helper 方法
檢查節點是否為其他節點的子節點
$bool = $node->isDescendantOf($parent);
檢查是否為根節點
$bool = $node->isRoot();
其他的檢查
$node->isChildOf($other);
$node->isAncestorOf($other);
$node->isSiblingOf($other);
$node->isLeaf()
檢查一致性
你可以檢查樹是否被破環
$bool = Category::isBroken();
擷取錯誤統計:
$data = Category::countErrors();
它將返回含有一下鍵的數組
oddness -- lft 和 rgt 值錯誤的節點的數量
duplicates -- lft 或者 rgt 值重複的節點的數量
wrong_parent -- left 和 rgt 值 與parent_id 不對應的造成無效parent_id 的節點的數量
missing_parent -- 含有parent_id對應的父節點不存在的節點的數量
修複樹
從v3.1往後支援修複樹,通過parent_id欄位的繼承資訊,給每個node設定合適的lft 和 rgt值
Node::fixTree();
範圍(scope)
假設你有個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(); // OKMenuItem::descendantsOf($id)->get(); // WRONG: returns nodes from other scopeMenuItem::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, 但是多餘