Đây là một gói thư viện làm việc với Laravel 4-5 để làm việc với cây trong cơ sở dữ liệu quan hệ
- Laravel 5.2 sử dụng v4
- Laravel 5.1 sử dụng v3
- Laravel 4 sử dụng v2
Nested sets hoặc MÔ HÌNH PHÂN CẤP một cách để lưu trữ hiệu quả phân cấp dữ liệu trong một bảng quan hệ.
NSM cho thấy hiệu suất khi cây được update ít. Nó được tạo ra làm nhanh chóng có được các giao điểm nút liên quan. Nó phù hợp xây dựng cho menu " building multi-depth menu " hoặc categories của shop.
Giả sử chúng ta có một mô hình model Category
; và một $node
biến là một một thể hiện của class mô hình model
và các giao điểm mà chúng ta tương tác . Nó có thể là một mô hình mới hoặc một bảng từ database.
Node giao điểm có mối quan hệ đẩy đủ chức năng với các function và và cấu trúc tự động nạp.
- Node belongs to
parent
=> nút giao điểm thuộc về một giao điểm cha - Node has many
children
=> nút giao điểm có nhiều giao điểm con - Node has many
descendants
=> giao điểm có nhiều giao điểm cháu
Khi bạn chỉ đơn giản là tạo ra một nút giao điểm mới, nó sẽ được thêm vào cuối cây của cấu trúc:
Category::create($attributes); // Saved as root
$node = new Category($attributes);
$node->save(); // Saved as root
Trong các trường hợp này các nút là một nút giao điểm root cấp cao nhât không có giao điểm cha.
// #1 Implicit save
$node->saveAsRoot();
// #2 Explicit save
$node->makeRoot()->save();
Các nút sẽ tạo ra cấu trúc cây mới được thêm vào cuối cây hiện tại cùng cấp các nút root đã có.
Giao dịch tự động khi các nút được save() thêm mới.
Di chuyển hay thêm mới một nút bao gồm truy vấn tới cơ sở dữ liệu. Thực thi được tự đông khi nào các nút giao điểm được lưu. Nó là an toàn sử dụng thực thi toàn cầu.
Nếu bạn làm việc với một số Models
Lưu ý quan trọng : Điều khiển cấu trúc cây bị trì hoãn cho đến khi bạn
save
trong model (một số phương pháp ngầm save
and trả về boolean result).
Nếu mô hình được saved nó không có nghĩa các nút giao điểm được di chuyển. Nếu ứng dụng của bạn
phụ thuộc vào việc các nút thực sự đã được thay đổi vị trí sắp xếp của nó , sử dụng hasMoved
method:
if ($node->save()) {
$moved = $node->hasMoved();
}
Nếu bạn muốn làm cho nút hiện tại là giao điểm nút con của một nút khác. Bạn có thể cho nó là cuối cùng hoặc đầu tiên. "last or first child".
Áp dụng trong ví dụ này, $parent
là một nút hiện có.
$parent = find($id) return object $nodes parent
Có vài cách để nối thêm một giao điểm vào cuối :
// #1 Sử dụng một định nghĩa insert
$node->appendToNode($parent)->save();
ví dụ :
$parent = Category::find(10);
$attributes = [
'name' => 'Màu Sắc',
'children' => [
[
'name' => 'Đỏ',
'children' => [
[ 'name' => 'Đỏ Đậm' ],
],
],
],
];
$node = Category::create($attributes);
$node->appendToNode($parent)->save();
// #2 Sử dụng giao điểm là object parent thêm mới một giao điểm con
$parent->appendNode($node);
ví dụ :
thay thế function $node->appendToNode($parent)->save() bằng $parent->appendNode($node);
// #3 Sử dụng một mối quan hệ giữa nút parent và các con sau đó tạo mới một giao điểm vào cuối cùng
$parent->children()->create($attributes);
// #5 Sử dụng một giao điểm parent mối quan hệ
$node->parent()->associate($parent)->save();
// #6 Using the parent attribute
$node->parent_id = $parent->id;
$node->save();
// #7 Using static method
Category::create($attributes, $parent);
Chỉ có một vài cách để thêm vào trước đầu tiên của các nút cùng cấp thay vì nối thêm vào ở cuối cùng:
// #1
$node->prependToNode($parent)->save();
// #2
$parent->prependNode($node);
Bạn có thể làm cho các giao điểm nút $node
là một giao điểm cùng cấp hàng xóm $neighbor
của nút giao điểm hiện có bằng các phương pháp:
$neighbor
phải tồn tại, nút giao điểm $node
phải là mới. nếu nút $node
tồn tại ,
nó sẽ được di chuyển đến vị trí mới và parent của nó sẽ được thay đổi nếu nó yêu cầu.
$neighbor = Category::find(13);
$attributes = [
'name' => 'Thit heo Tây',
'children' => [
[
'name' => 'Thị Heo Tây Nuôi Nhà',
],
],
];
// tạo nút mới
$node = Category::create($attributes);
// gọi function để di chuyển nút là hàng xóm hoặc thay đổi vị trí parent nếu được yêu cầu
# rõ ràng save
$node->afterNode($neighbor)->save();
$node->beforeNode($neighbor)->save();
# ngầm định save
$node->insertAfterNode($neighbor);
$node->insertBeforeNode($neighbor);
khi nào sử dụng các method static create
trong các giao điểm, nó sẽ kiểm tra xem các attributes chứa
children
key. Nếu nó có nó tạo ra nhiều hơn các nút đệ quy.
$node = Category::create(
['name' => 'Food',
'children' => [
[
'name' => 'Fruid',
'children' => [
[
'name' => 'Red',
'children' => [
['name'=>'Cherry'],
['name'=>'Torry'],
]
],
],
],
[
'name' => 'Meat',
'children' =>
[
[ 'name' => 'Beef',
'children' =>
[
['name'=>'Cherry'],
['name'=>'Torry'],
],
],
[ 'name' => 'Pork',
'children' =>
[
['name'=>'Niten'],
['name'=>'Forry']
]
],
],
],
],
]
);
$node->children
bây giờ chứa một danh sách các nút giao điểm con child đươc tạo ra.
Bạn có thể dễ dàng xây dựng lại một cây. Điều này rất hữu ích cho hàng loạt thay đổi cơ cấu
Category::rebuildTree($data, $delete);
$data
là một mảng của các nút :
$data = [
[ 'id' => 1, 'name' => 'foo', 'children' => [ ... ] ],
[ 'name' => 'bar' ],
];
Có một id được chỉ định cho nút với tên của foo
có nghĩa là hiện tại
nút sẽ được filled and saved. nếu nút không tồn tại một lỗi được trả về ModelNotFoundException
. Cũng như thế,
Nếu nút có children
quy định đó cũng là một mảng của các nút giao điểm;
họ sẽ được xử lý trong theo cách tương tự và Lưu giống như children của nút giao điểm foo
.
Nút giao điểm bar
không có khóa chính primary key ,
do đó, nó sẽ được tạo ra.
$delete
cho thấy cho dù để xóa các nút mà đã tồn tại nhưng không có mặt trong $data
.
Theo mặc định, nút không bị xóa.
Trong một số trường hợp, chúng tôi sẽ sử dụng một biến $id
mà là id của nút mục tiêu.
Một chuỗi từ tổ tiến đến parents đến nút hiện tại sẽ được trả về phù hợp để thực hiện hiển thị breadcrumbs category hiện tại.
// #1 Sử dụng truy cập
$result = $node->getAncestors();
// #2 Sử dụng một query
$result = $node->ancestors()->get();
// #3 Sử dụng truy cập từ 1 khóa chính
$result = Category::ancestorsOf($id);
Con cháu là tất cả các nút giao điểm trong một nhánh của cây có _lft và _rgt là một khoảng trống vị trí. Nghĩa là lấy ra các con của nút hoặc cháu hoặc tất cả ... v.v tùy vào khoảng trống cụ thể giữa column của nút giao điểm _lft và _rgt trong table
// #1 Using mối liên hệ
$result = $node->descendants;
// #2 Using một query
$result = $node->descendants()->get();
// #3 lấy ra con cháu descendants sử dụng khóa chính
$result = Category::descendantsOf($id);
Lấy ra con cháu hay hậu duệ của các giao điểm có id là một trong các số trong mảng id list nếu các id đó có cột _lft và _rgt có khoảng trống là hậu duệ sẽ được lấy ra :
$nodes = Category::with('descendants')->whereIn('id', $idList)->get();
Anh chị em là các giao điểm có cùng Parent.
ví dụ parent có _lft là 13 và _rgt là 18 vậy các khoảng trong 13 và 18 có 2 nút cùng cấp mang giá trị 2 column là 14-15 và 16-17 sẽ ;à ạ e cùng parent giao điểm cha nút.
$result = $node->getSiblings();
$result = $node->siblings()->get();
Để có được anh em phía sau cùng cấp của giao điểm hiện tại :
ví dụ _lft = 15 vậy sau 15 là 16 ...... với điều kiện cùng thuộc parent giao điểm.
$node = Model::find(15);
// lấy được một anh em được thêm vào sau node hiện có return 16
$result = $node->getNextSibling();
// lấy tất cả ae được thêm vào sau 16-> ... v.v.. <= nhỏ hơn parent _rgt
$result = $node->getNextSiblings();
// lấy tất cả thêm vào sau sử dụng query
$result = $node->nextSiblings()->get();
Để có được anh chị em giao điểm nút thêm vào trước :
// Lấy một người anh em của nút được thêm và trước giao điểm hiện tại
$result = $node->getPrevSibling();
// lấy tất cả các nút được thêm vào trước nút hiện tại
$result = $node->getPrevSiblings();
// Lấy tất cả nút cuối cùng được thêm vào trước object hiện tại bằng query xem right để biết giao điểm trước cùng cấp
$result = $node->prevSiblings()->get();
Hãy tưởng tượng một category has many
có nhiều goods. I.e. HasMany
mối quan hệ được thiết lập.
Làm thế nào bạn có thể nhận được tất cả get all goods của $category
và mỗi hậu duệ của nó ?
// Nhận id của con cháu
$categories = $category->descendants()->lists('id');
// Bao gồm các id của category chính no
$categories[] = $category->getKey();
// Nhận hàng goods
$goods = Goods::whereIn('category_id', $categories)->get();
Nếu bạn cần biết level các giao điểm nút với nút id là :
$result = Category::withDepth()->find($id);
$depth = $result->depth;
Root nút sẽ ở level 0. Children của nút root sẽ có level 1,
Để có được các name giao điểm nút trong cấp level cụ thể, bạn có thể áp dụng having
hạn chế :
$result = Category::withDepth()->having('depth', '=', 1)->get();
Mỗi nút giao điểm có giá trị cột _lft
là duy nhất để xác định vị trí của nó trong cây. Nếu bạn muốn nút giao điểm được sắp xếp theo các giá trị này, bạn cần sử dụng defaultOrder
phương pháp trong câu lệnh truy vấn query builder :
// Muốn lấy tất cả các nút giao điểm theo giá trị sắp xếp cột lft
$result = Category::defaultOrder()->get();
Bạn có thể nhận được các nút theo thứ tự đảo ngược lại :
$result = Category::reversed()->get();
Để thay đổi nút giao điểm lên hoặc xuống phía trong nhánh parent làm thay đổi thứ tự mặc định sử dụng :
$bool = $node->down();
$bool = $node->up();
// Di chuyển giao điểm xuống 3 đơn vị sắp xếp trong các giao điểm cùng cấp anh em.
$bool = $node->down(3);
Kết quả của hoạt động này là giá trị boolean của khi được thay đổi vị trí
những hạn chế khác nhau có thể được áp dụng cho truy vấn query builder:
- whereIsRoot() chỉ lấy duy nhất nút giao điểm gốc Root;
- whereIsAfter($id) Để có được tất cả các nút phía sau (Không chỉ là các nút đồng cấp anh em) với 1 id giao điểm chỉ định
- whereIsBefore($id) Để có được tất cả các nút phía trước của nút giao điểm với id chỉ định.
Hạn chế đối với con cháu :
// Lấy ra tất cả con cháu phía sau của nút
$result = Category::whereDescendantOf($node)->get();
// lấy ra tất cả không phải con cháu của nút
$result = Category::whereNotDescendantOf($node)->get();
// Hoặc sử dụng 2 phương pháp tương ứng
$result = Category::orWhereDescendantOf($node)->get();
$result = Category::orWhereNotDescendantOf($node)->get();
Tổ tiên cây cấu trúc hạn chế constraints:
// lấy ra tất cả các nút tổ tiên của nút
$result = Category::whereAncestorOf($node)->get();
$node
có thể là một a primary key khóa chính sử dụng trong bảng làm việc thông qua Model
Sau khi nhận được một bộ sưu tập các giao điểm bạn có thể chuyển nó sang cấu trúc dạng cây ví dụ
$tree = Category::get()->toTree();
Điều này sẽ điền các giao điểm cha parent
và con children
có mối quan hệ trong các giao điểm được thiết lập và bạn cần render hiển thị cây bằng thuật toán đệ qui phương pháp :
$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);
hoặc sắp xếp ul
$categories = Category::get()->toTree();
$traverse = function ($categories, $prefix = '<li>', $suffix = '</li>') use (&$traverse) {
foreach ($categories as $category) {
echo $prefix.$category->name.$suffix;
$hasChildren = (count($category->children) > 0);
if($hasChildren) {
echo('<ul>');
}
$traverse($category->children);
if($hasChildren) {
echo('</ul>');
}
}
};
$traverse($categories);
Kết quả khi output :
- Root
-- Child 1
--- Sub child 1
-- Child 2
- Another root
Ngoài ra bạn có thể xây dựng một cấu trúc cây phẳng thay vì nặp các nút theo đệ qui. Điều này thực sự có ích khi bạn muốn sắp xếp cây cấu trúc theo thứ tự abc....mà được một danh sách các nút mà nút con là ngay lập tức sau khi nút cha.
$nodes = Category::get()->toFlatTree();
Trong bảng dữ liệu của bạn có thể có nhiều cấu trúc cây, Đôi khi bạn không cần tải toàn bộ chúng bạn chỉ cần một cây của một giao điểm cụ thể
Đây là một ví dụ để hiển thị.
$root = Category::find($rootId);
$tree = $root->descendants->toTree($root);
Điều này $tree
sẽ trả về cấu trúc cây là các giao điểm con cháu của giao điểm $root
.
Nếu bạn không cần cấu trúc cây của nút $root
đó Bạn có thể phủ định nó để lấy ra các cây cấu trúc khác mà không phải là nó:
$tree = Category::descendantsOf($rootId)->toTree($rootId);
Để xóa một giao điểm nút
$node->delete();
Lưu Ý! bất kỳ các giao điểm con cháu cùng bị xóa
IMPORTANT! Các nút yêu cầu xóa phải được xóa như các model orm , don't không thực hiện xóa các giao điểm bằng các truy vấn tương tự như sau :
Category::where('id', '=', $id)->delete();
Điều này sẽ phá vỡ các cấu trúc của cây!
SoftDeletes
đặc điểm được hỗ trợ
Để kiểm tra nếu giao điểm hiện tại là con cháu của một giao điểm khác
$bool = $node->isDescendantOf($parent);
Để kiểm tra xem các giao điểm hiện tại có phải là nút Root :
$bool = $node->isRoot();
Kiểm tra khác :
// nếu đúng là con của nút khác
$node->isChildOf($other);
- // nếu đúng là tổ tiên của nút khác
$node->isAncestorOf($other);
- // nếu đúng là anh em của nút khác
$node->isSiblingOf($other);
Bạn có thể kiểm tra xem một cây bị phá vỡ cấu trúc của nó mà sinh lỗi cấu trúc
$bool = Category::isBroken();
Nó có thể cho phép hiển thị các lỗi
$data = Category::countErrors();
nó sẽ return ra một mảng các key sau kèm theo thông báo lỗi :
oddness
-- số giao điểm đã bị sai bộ thứ tự sắp xếplft
vàrgt
giá trịduplicates
-- các số của giao điểm này đã cólft
hoặcrgt
giá trị trong cộtwrong_parent
-- các số của thứ tự của giao điểm không hợp lệ trongparent_id
giá trị không tương ứng vớilft
vàrgt
giá trị cộtmissing_parent
-- number đã cóparent_id
chỉ đến một giao điểm không tồn tại
Kể từ khi cây v3.1 có thể được cố định. Sử dụng thông tin kế thừa từ parent_id
cột,
với đặc tính riêng _lft
và _rgt
giá trị được thiết lập là một cặp của giao điểm .
Node::fixTree();
Hãy thử nghĩ bạn có một bảng Menu
với model và một MenuItems
bảng với model. tức là mỗi quan hệ một nhiều one-to-many
giữa 2 mô hình này. MenuItem
có menu_id
thuộc tình sử dụng để nối 2 bảng qua model. MenuItem
sử dụng bộ cấu trúc cây lồng nhau. Rõ ràng rằng bạn sẽ muốn
xử lý từng cấu trúc dựa trên menu_id
thuộc tính. Để làm như vậy bạn cần xác định thuộc tính này như một phạm vi truy vấn sử dụng
ở mọi nơi.
protected function getScopeAttributes()
{
return [ 'menu_id' ];
}
Ngay bây giờ để thực hiện một tùy chỉnh query bạn cần cung cấp thuộc tính attributes được sử dụng của menu_id scoping:
MenuItem::scoped([ 'menu_id' => 5 ])->withDepth()->get(); // OK
MenuItem::descendantsOf($id)->get(); // WRONG: returns nodes from other scope
MenuItem::scoped([ 'menu_id' => 5 ])->fixTree();
When requesting nodes using model instance, scopes applied automatically based on the attributes of that model. See examples:
$node = MenuItem::findOrFail($id);
$node->siblings()->withDepth()->get(); // OK
To get scoped query builder using instance:
$node->newScopedQuery();
Note, that scoping is not required when retrieving model by primary key (since the key is unique):
$node = MenuItem::findOrFail($id); // OK
$node = MenuItem::scoped([ 'menu_id' => 5 ])->findOrFail(); // OK, but redundant
- PHP >= 5.4
- Laravel >= 4.1
It is highly suggested to use database that supports transactions (like MySql's InnoDb) to secure a tree from possible corruption.
To install the package, in terminal:
composer require kalnoy/nestedset
You can use a method to add needed columns with default names:
Schema::create('table', function (Blueprint $table) {
...
NestedSet::columns($table);
});
To drop columns:
Schema::table('table', function (Blueprint $table) {
NestedSet::dropColumns($table);
});
Your model should use Kalnoy\Nestedset\NodeTrait
trait to enable nested sets:
use Kalnoy\Nestedset\NodeTrait;
class Foo extends Model {
use NodeTrait;
}
If your previous extension used different set of columns, you just need to override following methods on your model class:
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);
}
If your tree contains parent_id
info, you need to add two columns to your schema:
$table->unsignedInteger('_lft');
$table->unsignedInteger('_rgt');
After setting up your model you only need to fix the tree to fill
_lft
and _rgt
columns:
MyModel::fixTree();
Copyright (c) 2016 Alexander Kalnoy
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.