laravel-nestedset:多級無限分類正確姿式

本文來自pilishen.com---原文連接; 歡迎做客咱們的php&Laravel學習羣:109256050php

此文檔是 nestedset-無限分類正確姿式的擴展閱讀前端

laravel-nestedset是一個關係型數據庫遍歷樹的larvel4-5的插件包node

目錄:laravel

  • Nested Sets Model簡介
  • 安裝要求
  • 安裝
  • 開始使用
    • 遷移文件
    • 插入節點
    • 獲取節點
    • 刪除節點
    • 一致性檢查和修復
    • 做用域

Nested Sets Model簡介

Nested Set Model 是一種實現有序樹的高明的方法,它快速且不須要遞歸查詢,例如無論樹有多少層,你能夠僅使用一條查詢來獲取某個節點下的全部的後代,缺點是它的插入、移動、刪除須要執行復雜的sql語句,可是這些都在這個插件內處理了! 更多關於詳見維基百科!Nested set model 及它中文翻譯!嵌套集合模型算法

安裝要求

  • PHP>=5.4
  • laravel>=4.1
  • v4.3版本之後支持Laravel-5.5
  • v4版本支持Laravel-5.二、5.三、5.4
  • v3版本支持Laravel-5.1
  • v2版本支持Laravel-4 強烈建議使用支持事物功能的數據引擎(像MySql的innoDb)來防止可能的數據損壞。

安裝

composer.json文件中加入下面代碼:sql

"kalnoy/nestedset": "^4.3",
複製代碼

運行composer install 來安裝它。數據庫

或者直接在命令行輸入json

composer require kalnoy/nestedset
複製代碼

如需安裝歷史版本請點擊更多版本數組

開始使用

遷移文件

你可使用NestedSet類的columns方法來添加有默認名字的字段:bash

...
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_rgtparent_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\NodeTraittrait 來實現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 mutator
public function setParentAttribute($value)
{
    $this->setParentIdAttribute($value);
}
複製代碼

從其餘的具備父子關係的模型庫遷移

若是你的數據庫結構樹包含 parent_id 字段信息,你須要添加下面兩欄字段到你的藍圖文件:

$table->unsignedInteger('_lft');
$table->unsignedInteger('_rgt');
複製代碼

設置好你的模型後你只須要修復你的結構樹來填充_lft_rgt字段:

MyModel::fixTree();
複製代碼

關係

Node具備如下功能,他們功能徹底且被預加載:

  • Node belongs to parent
  • Node has many children
  • Node has many ancestors
  • Node has many descendants

假設咱們有一個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' ],
];
複製代碼

上面有一個namefoo的節點,它有指定的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();

注意 這在數據庫嚴格模式下無效

默認排序

全部的節點都是在內部嚴格組織的,默認狀況下沒有順序,因此節點是隨機展示的,這部影響展示,你能夠按字母和其餘的順序對節點排序。

可是在一些狀況下按層級展現是必要的,它對獲取祖先和用於菜單順序有用。

使用deaultOrder運用樹的排序: $result = Category::defaultOrder()->get();

你也可使用倒序排序: $result = Category::reversed()->get();

讓節點在父級內部上下移動來改變默認排序:

$bool = $node->down();
$bool = $node->up();

// 向下移動3個兄弟節點
$bool = $node->down(3);
複製代碼

操做返回根據操做的節點的位置是否改變的布爾值

約束

不少約束條件能夠被用到這些查詢構造器上:

  • whereIsRoot() 僅獲取根節點;
  • whereIsAfter($id) 獲取特定id的節點後面的全部節點(不只是兄弟節點)。
  • whereIsBefore($id) 獲取特定id的節點前面的全部節點(不只是兄弟節點)。

祖先約束

$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();

這將破壞樹結構 支持SoftDeletestrait,且在模型層

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(); // 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, 可是多餘
複製代碼
相關文章
相關標籤/搜索