هذه حزمة Laravel 4-10 للعمل مع الأشجار في قواعد البيانات العلائقية.
Laravel 11.0 مدعوم منذ الإصدار 6.0.4
Laravel 10.0 مدعوم منذ الإصدار 6.0.2
Laravel 9.0 مدعوم منذ الإصدار 6.0.1
Laravel 8.0 مدعوم منذ الإصدار 6.0.0
Laravel 5.7، 5.8، 6.0، 7.0 مدعوم منذ الإصدار 5
Laravel 5.5، 5.6 مدعوم منذ الإصدار 4.3
Laravel 5.2، 5.3، 5.4 مدعوم منذ الإصدار 4
Laravel 5.1 مدعوم في الإصدار 3
Laravel 4 مدعوم في الإصدار 2
محتويات:
نظرية
التوثيق
إدراج العقد
استرجاع العقد
حذف العقد
فحص الاتساق وتحديد
تحديد النطاق
متطلبات
تثبيت
تعد المجموعات المتداخلة أو Nested Set Model طريقة لتخزين البيانات الهرمية بشكل فعال في جدول علائقي. من ويكيبيديا:
نموذج المجموعة المتداخلة هو ترقيم العقد وفقًا لشجرة اجتياز، والتي تزور كل عقدة مرتين، وتعيين الأرقام بترتيب الزيارة، وفي كلتا الزيارتين. وهذا يترك رقمين لكل عقدة، ويتم تخزينهما كخاصيتين. يصبح الاستعلام غير مكلف: يمكن اختبار العضوية الهرمية من خلال مقارنة هذه الأرقام. يتطلب التحديث إعادة الترقيم، وبالتالي فهو مكلف.
يُظهر NSM أداءً جيدًا عندما يتم تحديث الشجرة نادرًا. تم ضبطه ليكون سريعًا للحصول على العقد ذات الصلة. إنها مناسبة بشكل مثالي لإنشاء قائمة أو فئات متعددة للمتاجر.
لنفترض أن لدينا Category
نموذجية؛ متغير $node
هو مثال لهذا النموذج والعقدة التي نتعامل معها. يمكن أن يكون نموذجًا جديدًا أو نموذجًا من قاعدة البيانات.
تحتوي العقدة على العلاقات التالية التي تعمل بكامل طاقتها ويمكن تحميلها بفارغ الصبر:
العقدة تنتمي إلى parent
العقدة لديها العديد children
العقدة لديها العديد ancestors
العقدة لديها العديد descendants
يتضمن نقل العقد وإدراجها العديد من استعلامات قاعدة البيانات، لذا يوصى بشدة باستخدام المعاملات.
مهم! اعتبارًا من الإصدار 4.2.0، لم يتم بدء المعاملة تلقائيًا
ملاحظة مهمة أخرى هي أن عمليات المعالجة الهيكلية يتم تأجيلها حتى تضغط على save
في النموذج (بعض الطرق تستدعي ضمنيًا save
وإرجاع النتيجة المنطقية للعملية).
إذا تم حفظ النموذج بنجاح، فهذا لا يعني أنه تم نقل العقدة. إذا كان تطبيقك يعتمد على ما إذا كانت العقدة قد غيرت موضعها بالفعل، فاستخدم التابع hasMoved
:
إذا ($node->save()) {$moved = $node->hasMoved(); }
عندما تقوم ببساطة بإنشاء عقدة، سيتم إلحاقها بنهاية الشجرة:
الفئة::إنشاء($attributes); // تم الحفظ كجذر
$node = فئة جديدة($attributes);$node->save(); // تم الحفظ كجذر
في هذه الحالة تعتبر العقدة جذرًا مما يعني أنها لا تحتوي على أصل.
// #1 ضمني save$node->saveAsRoot();// #2 صريح save$node->makeRoot()->save();
سيتم إلحاق العقدة بنهاية الشجرة.
إذا كنت تريد أن تجعل العقدة فرعًا لعقدة أخرى، فيمكنك جعلها العقدة الأخيرة أو الأولى.
في الأمثلة التالية، $parent
هو عقدة موجودة.
هناك طرق قليلة لإلحاق العقدة:
// #1 استخدام الإدراج المؤجل $node->appendToNode($parent)->save();// #2 استخدام العقدة الأصلية$parent->appendNode($node);// #3 استخدام علاقة أطفال الوالدين$parent- > children()-> create($attributes);// #5 استخدام العلاقة الأصلية للعقدة$node->parent()->associate($parent)->save();// #6 استخدام الأصل attribute$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->afterNode($neighbor)->save();$node->beforeNode($neighbor)->save();# حفظ ضمني$node->insertAfterNode($neighbor);$node-> InsertBeforeNode($neighbor);
عند استخدام طريقة ثابتة create
على العقدة، فإنها تتحقق مما إذا كانت السمات تحتوي على مفتاح children
. إذا حدث ذلك، فإنه يقوم بإنشاء المزيد من العقد بشكل متكرر.
$node = الفئة::إنشاء(['name' => 'Foo','children' => [ ['اسم' => 'شريط','أطفال' => [ [ 'اسم' => 'باز' ], ]، ]، ]، ]);
يحتوي $node->children
الآن على قائمة بالعقد الفرعية التي تم إنشاؤها.
يمكنك بسهولة إعادة بناء شجرة. وهذا مفيد للتغيير الشامل في بنية الشجرة.
الفئة::rebuildTree($data, $delete);
$data
عبارة عن مجموعة من العقد:
بيانات $ = [ [ 'id' => 1, 'name' => 'foo', 'children' => [... ] ], [ 'الاسم' => 'الشريط']، ];
يوجد معرف محدد للعقدة باسم foo
مما يعني أنه سيتم ملء العقدة الحالية وحفظها. إذا لم تكن العقدة موجودة، فسيتم طرح ModelNotFoundException
. أيضًا، تحتوي هذه العقدة على children
محددين وهو أيضًا مصفوفة من العقد؛ ستتم معالجتها بنفس الطريقة وحفظها كأبناء للعقدة foo
.
لا يحتوي bar
العقدة على مفتاح أساسي محدد، لذا سيتم إنشاؤه.
يُظهر $delete
ما إذا كان سيتم حذف العقد الموجودة بالفعل ولكنها غير موجودة في $data
. افتراضيًا، لا يتم حذف العقد.
اعتبارًا من 4.2.8 يمكنك إعادة بناء شجرة فرعية:
الفئة::rebuildSubtree($root, $data);
يؤدي هذا إلى تقييد إعادة بناء الشجرة لأحفاد العقدة $root
.
في بعض الحالات سوف نستخدم متغير $id
وهو معرف العقدة المستهدفة.
الأسلاف يصنعون سلسلة من الآباء إلى العقدة. مفيد لعرض فتات الخبز للفئة الحالية.
الأحفاد هم جميع العقد في شجرة فرعية، أي أبناء العقدة، أبناء الأطفال، وما إلى ذلك.
يمكن تحميل كل من الأجداد والأحفاد بفارغ الصبر.
// الوصول إلى الأجداد$node->ancestors;// الوصول إلى الأحفاد$node->descendants;
من الممكن تحميل الأسلاف والأحفاد باستخدام استعلام مخصص:
$result = الفئة::ancestorsOf($id);$result = الفئة::ancestorsAndSelf($id);$result = الفئة::descendantsOf($id);$result = الفئة::descendantsAndSelf($id);
في معظم الحالات، تحتاج إلى ترتيب أسلافك حسب المستوى:
$result = Category::defaultOrder()->ancestorsOf($id);
يمكن تحميل مجموعة من الأسلاف بفارغ الصبر:
$categories = Category::with('ancestors')->paginate(30);// في عرض فتات الخبز:@foreach($categories as $i => $category) <small>{{ $category->ancestors->count() ؟ Iplode(' > ', $category->ancestors->pluck('name')->toArray()) : 'المستوى الأعلى' }}</small><br> {{ $الفئة->الاسم }} @endforeach
الأشقاء هم العقد التي لها نفس الوالد.
$result = $node->getSiblings();$result = $node->siblings()->get();
للحصول على الأخوة التاليين فقط:
// احصل على الأخ الذي يقع مباشرة بعد العقدة$result = $node->getNextSibling();// احصل على جميع الأشقاء الذين هم بعد العقدة$result = $node->getNextSiblings();// احصل على جميع الأشقاء باستخدام query$result = $node->nextSiblings()->get();
للحصول على الأخوة السابقين:
// احصل على الأخ الموجود قبل العقدة مباشرة$result = $node->getPrevSibling();// احصل على جميع الأشقاء الموجودين قبل العقدة$result = $node->getPrevSiblings();// احصل على جميع الأشقاء باستخدام query$result = $node->prevSiblings()->get();
تخيل أن كل فئة has many
من السلع. أي تم تأسيس علاقة HasMany
. كيف يمكنك الحصول على جميع السلع من $category
وكل نسلها؟ سهل!
// احصل على معرفات الأحفاد$categories = $category->descendants()->pluck('id');// قم بتضمين معرف الفئة نفسها$categories[] = $category->getKey();// احصل على البضائع $goods = Goods::whereIn('category_id', $categories)->get();
إذا كنت تريد معرفة المستوى الذي توجد فيه العقدة:
$result = Category::withDepth()->find($id);$deep = $result->deep;
ستكون العقدة الجذرية عند المستوى 0. وسيكون لدى أبناء العقد الجذرية المستوى 1، وما إلى ذلك.
للحصول على العقد ذات المستوى المحدد، يمكنك تطبيق having
:
$result = Category::withDepth()->having('عمق', '=', 1)->get();
مهم! لن يعمل هذا في الوضع الصارم لقاعدة البيانات
يتم تنظيم جميع العقد بشكل صارم داخليًا. افتراضيًا، لا يتم تطبيق أي ترتيب، لذلك قد تظهر العقد بترتيب عشوائي ولا يؤثر هذا على عرض الشجرة. يمكنك ترتيب العقد حسب الأبجدية أو الفهرس الآخر.
لكن في بعض الحالات يكون الترتيب الهرمي ضروريًا. إنه مطلوب لاسترداد الأسلاف ويمكن استخدامه لطلب عناصر القائمة.
لتطبيق ترتيب الشجرة، يتم استخدام طريقة defaultOrder
:
$result = Category::defaultOrder()->get();
يمكنك الحصول على العقد بترتيب عكسي:
$result = Category::reversed()->get();
لتحويل العقدة لأعلى أو لأسفل داخل الأصل للتأثير على الترتيب الافتراضي:
$bool = $node->down();$bool = $node->up();// إزاحة العقدة بواسطة 3 أشقاء$bool = $node->down(3);
نتيجة العملية هي قيمة منطقية لمعرفة ما إذا كانت العقدة قد غيرت موضعها.
القيود المختلفة التي يمكن تطبيقها على منشئ الاستعلام:
WhereIsRoot() للحصول على العقد الجذرية فقط؛
hasParent() للحصول على العقد غير الجذرية؛
WhereIsLeaf() للحصول على الأوراق فقط؛
hasChildren() للحصول على العقد غير المغادرة؛
WhereIsAfter($id) للحصول على كل عقدة (ليس فقط الأشقاء) التي تلي العقدة ذات المعرف المحدد؛
WhereIsBefore($id) للحصول على كل عقدة موجودة قبل العقدة ذات المعرف المحدد.
قيود السلالة:
$result = الفئة::whereDescendantOf($node)->get();$result = الفئة::whereNotDescendantOf($node)->get();$result = الفئة::orWhereDescendantOf($node)->get() ;$result = الفئة::أوWhereNotDescendantOf($node)->get();$result = الفئة::whereDescendantAndSelf($id)->get();// تضمين العقدة المستهدفة في مجموعة النتائج$result = Category::whereDescendantOrSelf($node)->get();
قيود الأجداد:
$result = الفئة::whereAncestorOf($node)->get();$result = الفئة::whereAncestorOrSelf($id)->get();
يمكن أن يكون $node
إما مفتاحًا أساسيًا للنموذج أو لمثيل النموذج.
بعد الحصول على مجموعة العقد، يمكنك تحويلها إلى شجرة. على سبيل المثال:
$tree = الفئة::get()->toTree();
سيؤدي هذا إلى ملء علاقات parent
children
في كل عقدة في المجموعة ويمكنك تقديم شجرة باستخدام خوارزمية متكررة:
$nodes = Category::get()->toTree();$traverse = function ($categories, $prefix = '-') use (&$traverse) {foreach ($categories as $category) {echo PHP_EOL.$ بادئة.' '.$category->name;$traverse($category->children, $prefix.'-'); } };$اجتياز($nodes);
سيؤدي هذا إلى إخراج شيء مثل هذا:
- Root -- Child 1 --- Sub child 1 -- Child 2 - Another root
يمكنك أيضًا إنشاء شجرة مسطحة: قائمة بالعقد التي تقع فيها العقد الفرعية مباشرة بعد العقدة الأصلية. يعد هذا مفيدًا عندما تحصل على عقد بترتيب مخصص (أي أبجديًا) ولا تريد استخدام العودية للتكرار على العقد الخاصة بك.
$nodes = الفئة::get()->toFlatTree();
المثال السابق سوف يخرج:
Root Child 1 Sub child 1 Child 2 Another root
في بعض الأحيان لا تحتاج إلى تحميل الشجرة بأكملها وفقط بعض الشجرة الفرعية لعقدة محددة. ويظهر في المثال التالي:
$root = الفئة::descendantsAndSelf($rootId)->toTree()->first();
في استعلام واحد نحصل على جذر شجرة فرعية وجميع أحفادها التي يمكن الوصول إليها عبر علاقة children
.
إذا لم تكن بحاجة إلى عقدة $root
نفسها، فقم بما يلي بدلاً من ذلك:
$tree = الفئة::descendantsOf($rootId)->toTree($rootId);
لحذف عقدة:
$node->delete();
مهم! سيتم أيضًا حذف أي سليل لهذه العقدة!
مهم! العقد مطلوبة ليتم حذفها كنماذج، لا تحاول حذفها باستخدام استعلام مثل هذا:
الفئة::حيث('id', '=', $id)->delete();
هذا سوف يكسر الشجرة!
سمة SoftDeletes
مدعومة أيضًا على مستوى النموذج.
للتحقق مما إذا كانت العقدة سليلًا لعقدة أخرى:
$bool = $node->isDescendantOf($parent);
للتحقق مما إذا كانت العقدة جذرًا:
$bool = $node->isRoot();
الشيكات الأخرى:
$node->isChildOf($other);
$node->isAncestorOf($other);
$node->isSiblingOf($other);
$node->isLeaf()
يمكنك التحقق مما إذا كانت الشجرة مكسورة (أي بها بعض الأخطاء الهيكلية):
$bool = الفئة::isBroken();
من الممكن الحصول على إحصائيات الخطأ:
$data = الفئة::countErrors();
سيُرجع مصفوفة بالمفاتيح التالية:
oddness
- عدد العقد التي تحتوي على مجموعة خاطئة من قيم lft
و rgt
duplicates
- عدد العقد التي لها نفس قيم lft
أو rgt
wrong_parent
-- عدد العقد التي تحتوي على قيمة parent_id
غير صالحة والتي لا تتوافق مع قيمتي lft
و rgt
missing_parent
- عدد العقد التي تحتوي على parent_id
الذي يشير إلى عقدة غير موجودة
منذ v3.1 يمكن الآن إصلاح الشجرة. باستخدام معلومات الوراثة من عمود parent_id
، يتم تعيين قيم _lft
و_ _rgt
المناسبة لكل عقدة.
Node::fixTree();
تخيل أن لديك نموذج Menu
وعناصر MenuItems
. توجد علاقة رأس بأطراف بين هذه النماذج. يحتوي MenuItem
على سمة menu_id
لربط النماذج معًا. يتضمن MenuItem
مجموعات متداخلة. من الواضح أنك تريد معالجة كل شجرة على حدة بناءً على سمة menu_id
. للقيام بذلك، تحتاج إلى تحديد هذه السمة كسمة النطاق:
الدالة المحمية getScopeAttributes() {إرجاع ['menu_id']؛ }
ولكن الآن، من أجل تنفيذ بعض الاستعلامات المخصصة، تحتاج إلى توفير السمات المستخدمة لتحديد النطاق:
MenuItem::scoped([ 'menu_id' => 5 ])->withDepth()->get(); // OKMenuItem::descendantsOf($id)->get(); // خطأ: إرجاع العقد من نطاق آخر::scoped([ 'menu_id' => 5 ])->fixTree(); // نعم
عند طلب العقد باستخدام مثيل النموذج، يتم تطبيق النطاقات تلقائيًا بناءً على سمات هذا النموذج:
$node = MenuItem::findOrFail($id);$node->siblings()->withDepth()->get(); // نعم
للحصول على منشئ استعلام محدد النطاق باستخدام المثيل:
$node->newScopedQuery();
استخدم دائمًا استعلامًا محددًا عند التحميل المتحمس:
MenuItem::scoped([ 'menu_id' => 5])->with('أحفاد')->findOrFail($id); // OKMenuItem::with('descendants')->findOrFail($id); // خطأ
بي إتش بي >= 5.4
لارافيل >= 4.1
يُقترح بشدة استخدام قاعدة البيانات التي تدعم المعاملات (مثل MySql's InnoDb) لتأمين الشجرة من الفساد المحتمل.
لتثبيت الحزمة، في المحطة:
composer require kalnoy/nestedset
لمستخدمي Laravel 5.5 وما فوق:
Schema::create('table', function (Blueprint $table) {...$table->nestedSet(); });// لإسقاط columnsSchema::table('table', function (Blueprint $table) {$table->dropNestedSet(); });
لإصدارات Laravel السابقة:
...use KalnoyNestedsetNestedSet; مخطط::إنشاء('جدول', وظيفة (مخطط $table) {...NestedSet::columns($table); });
لإسقاط الأعمدة:
...use KalnoyNestedsetNestedSet; المخطط::الجدول('الجدول'، الوظيفة (مخطط $table) { NestedSet::dropColumns($table); });
يجب أن يستخدم النموذج الخاص بك سمة KalnoyNestedsetNodeTrait
لتمكين المجموعات المتداخلة:
استخدم KalnoyNestedsetNodeTrait;class Foo يمتد Model {use NodeTrait; }
إذا كان ملحقك السابق يستخدم مجموعة مختلفة من الأعمدة، فستحتاج فقط إلى تجاوز الطرق التالية في فئة النموذج الخاص بك:
الوظيفة العامة getLftName() {عودة "يسار"؛ }الوظيفة العامة getRgtName() {عودة "يمين"؛ }الوظيفة العامة getParentIdName() {إرجاع "الوالد"؛ }// حدد سمة معرف الأصل mutatorpublic function setParentAttribute($value) {$this->setParentIdAttribute($value); }
إذا كانت شجرتك تحتوي على معلومات parent_id
، فستحتاج إلى إضافة عمودين إلى مخططك:
$table->unsignedInteger('_lft');$table->unsignedInteger('_rgt');
بعد إعداد النموذج الخاص بك، ما عليك سوى إصلاح الشجرة لملء أعمدة _lft
و_ _rgt
:
MyModel::fixTree();
حقوق الطبع والنشر (ج) 2017 ألكسندر كالنوي
يُمنح الإذن مجانًا لأي شخص يحصل على نسخة من هذا البرنامج وملفات الوثائق المرتبطة به ("البرنامج")، للتعامل في البرنامج دون قيود، بما في ذلك، على سبيل المثال لا الحصر، حقوق الاستخدام والنسخ والتعديل والدمج. ونشر و/أو توزيع وترخيص من الباطن و/أو بيع نسخ من البرنامج، والسماح للأشخاص الذين تم توفير البرنامج لهم بالقيام بذلك، وفقًا للشروط التالية:
يجب تضمين إشعار حقوق الطبع والنشر أعلاه وإشعار الإذن هذا في جميع النسخ أو الأجزاء الكبيرة من البرنامج.
يتم توفير البرنامج "كما هو"، دون أي ضمان من أي نوع، صريحًا أو ضمنيًا، بما في ذلك، على سبيل المثال لا الحصر، ضمانات القابلية للتسويق والملاءمة لغرض معين وعدم الانتهاك. لا يتحمل المؤلفون أو أصحاب حقوق الطبع والنشر بأي حال من الأحوال المسؤولية عن أي مطالبة أو أضرار أو مسؤولية أخرى، سواء في إجراء العقد أو الضرر أو غير ذلك، الناشئة عن أو خارج أو فيما يتعلق بالبرنامج أو الاستخدام أو المعاملات الأخرى في برمجة.