Tutorial 教程 

The Problem 问题 

You’ve created a Django project, and you need to manage some hierarchical data. For instance you’ve got a bunch of hierarchical pages in a CMS, and sometimes pages are children of other pages
你已经创建了一个 Django 项目,你需要管理一些分层数据。例如,您在 CMS 中有一堆分层页面,有时页面是其他页面的子页面

Now suppose you want to show a breadcrumb on your site, like this:
现在,假设您想在网站上显示痕迹导航,如下所示:

Home > Products > Food > Meat > Spam > Spammy McDelicious

To get all those page titles you might do something like this:
要获取所有这些页面标题,您可以执行以下操作:

titles = []
while page:
    titles.append(page.title)
    page = page.parent

That’s one database query for each page in the breadcrumb, and database queries are slow. Let’s do this a better way.
这是针对痕迹导航中每个页面的一个数据库查询,并且数据库查询速度很慢。让我们用更好的方式做到这一点。

The Solution 解决方案 

Modified Preorder Tree Traversal can be a bit daunting at first, but it’s one of the best ways to solve this problem.
修改后的预序树遍历一开始可能有点令人生畏,但它是解决这个问题的最佳方法之一。

If you want to go into the details, there’s a good explanation here: Storing Hierarchical Data in a Database or Managing Hierarchical Data in Mysql
如果你想深入了解细节,这里有一个很好的解释:在数据库中存储分层数据或在Mysql中管理分层数据

tl;dr: MPTT makes most tree operations much cheaper in terms of queries. In fact all these operations take at most one query, and sometimes zero:
tl;dr:MPTT 使大多数树操作在查询方面成本大大降低。事实上,所有这些操作最多只接受一个查询,有时甚至为零:
  • get descendants of a node
    获取节点的后代

  • get ancestors of a node
    获取节点的祖先

  • get all nodes at a given level
    获取给定级别的所有节点

  • get leaf nodes 获取叶节点

And this one takes zero queries:
而这个不需要任何查询:
  • count the descendants of a given node
    计算给定节点的后代

Enough intro. Let’s get started.
足够的介绍。让我们开始吧。

Getting started 开始使用 

Add mptt To INSTALLED_APPS
添加到 mptt INSTALLED_APPS

As with most Django applications, you should add mptt to the INSTALLED_APPS in your settings.py file:
与大多数 Django 应用程序一样,您应该 INSTALLED_APPSsettings.py 文件中添加 mptt

INSTALLED_APPS = (
    'django.contrib.auth',
    # ...
    'mptt',
)

Set up your model
设置模型 

Start with a basic subclass of MPTTModel, something like this:
从 MPTTModel 的基本子类开始,如下所示:

from django.db import models
from mptt.models import MPTTModel, TreeForeignKey

class Genre(MPTTModel):
    name = models.CharField(max_length=50, unique=True)
    parent = TreeForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children')

    class MPTTMeta:
        order_insertion_by = ['name']

You must define a parent field which is a TreeForeignKey to 'self'. A TreeForeignKey is just a regular ForeignKey that renders form fields differently in the admin and a few other places.
您必须定义一个 TreeForeignKey 父字段,即 to 'self' 。A TreeForeignKey 只是一个常规 ForeignKey ,它在管理员和其他一些地方以不同的方式呈现表单字段。

Because you’re inheriting from MPTTModel, your model will also have a number of other fields: level, lft, rght, and tree_id. These fields are managed by the MPTT algorithm. Most of the time you won’t need to use these fields directly.
由于您继承自 MPTTModel,因此模型还将具有许多其他字段: level 、、 lft rghttree_id 。这些字段由 MPTT 算法管理。大多数情况下,您不需要直接使用这些字段。

That MPTTMeta class adds some tweaks to django-mptt - in this case, just order_insertion_by. This indicates the natural ordering of the data in the tree.
MPTTMeta 类添加了一些 django-mptt 调整 - 在本例中,只是 order_insertion_by .这表示树中数据的自然排序。

Now create and apply the migrations to create the table in the database:
现在创建并应用迁移以在数据库中创建表:

python manage.py makemigrations <your_app>
python manage.py migrate

Create some data 创建一些数据 

Fire up a django shell:
启动 django shell:

python manage.py shell

Now create some data to test:
现在创建一些数据进行测试:

from myapp.models import Genre
rock = Genre.objects.create(name="Rock")
blues = Genre.objects.create(name="Blues")
Genre.objects.create(name="Hard Rock", parent=rock)
Genre.objects.create(name="Pop Rock", parent=rock)

Make a view 制作视图 

This one’s pretty simple for now. Add this lightweight view to your views.py:
这个现在很简单。将此轻量级视图添加到您的 views.py

def show_genres(request):
    return render(request, "genres.html", {'genres': Genre.objects.all()})

And add a URL for it in urls.py:
并在以下位置添加 urls.py URL:

(r'^genres/$', show_genres),

Template 模板 

django-mptt includes some template tags for making this bit easy too. Create a template called genres.html in your template directory and put this in it:
django-mptt 包括一些模板标签,使这一点也变得简单。创建一个在模板目录中调用 genres.html 的模板,并将其放入其中:

{% load mptt_tags %}
<ul>
    {% recursetree genres %}
        <li>
            {{ node.name }}
            {% if not node.is_leaf_node %}
                <ul class="children">
                    {{ children }}
                </ul>
            {% endif %}
        </li>
    {% endrecursetree %}
</ul>

That recursetree tag will recursively render that template fragment for all the nodes. Try it out by going to /genres/.
该递归树标记将递归呈现所有节点的模板片段。请转到 /genres/ 尝试一下。

There’s more; check out the docs for custom admin-site stuff, more template tags, tree rebuild functions etc.
还有更多;查看文档,了解自定义管理站点内容、更多模板标签、树重建功能等。

Now you can stop thinking about how to do trees, and start making a great django app!
现在你可以停止考虑如何做树,开始制作一个伟大的 django 应用程序!

order_insertion_by gotcha  order_insertion_by 陷阱 

In the example above, we used order_insertion_by option, which makes django-mptt order items in the tree automatically, using name key. What does this mean, technically? Well, in case you add items in an unordered manner, django-mptt will update the database, so they will be ordered in the tree.
在上面的例子中,我们使用了 order_insertion_by option,它使用 name key 自动在树中生成 django-mptt 订单项。从技术上讲,这意味着什么?好吧,如果您以无序方式添加项目, django-mptt 将更新数据库,因此它们将在树中排序。

So why this is exactly a gotcha?
那么为什么这恰恰是一个陷阱呢?

Well, it is not. As long as you don’t keep instances with references to old data. But chances are you do keep them and you don’t even know about this.
好吧,事实并非如此。只要您不保留引用旧数据的实例。但很有可能你确实保留了它们,你甚至不知道这一点。

In case you do, you will need to reload your items from the database, or else you will be left with strange bugs, looking like data inconsistency.
如果这样做,您将需要从数据库中重新加载您的项目,否则您将留下奇怪的错误,看起来像数据不一致。

The sole reason behind that is we can’t actually tell Django to reload every single instance of django.db.models.Model based on the table row. You will need to reload manually, by calling Model.refresh_from_db().
这背后的唯一原因是我们实际上不能告诉 Django 根据表行重新加载每个 django.db.models.Model 实例。您需要通过调用 Model.refresh_from_db() 手动重新加载。

For example, using that model from the previous code snippet:
例如,使用上一个代码片段中的该模型:

>>> root = Genre.objects.create(name="")
#
# Bear in mind, that we're going to add children in an unordered
# manner:
#
>>> b = OrderedInsertion.objects.create(name="b", parent=root)
>>> a = OrderedInsertion.objects.create(name="a", parent=root)
#
# At this point, the tree will be reorganized in the database
# and unless you will refresh the 'b' instance, it will be left
# containing old data, which in turn will lead to bugs like:
#
>>> a in a.get_ancestors(include_self=True)
True
>>> b in b.get_ancestors(include_self=True)
False
#
# What? That's wrong! Let's reload
#
>>> b.refresh_from_db()
>>> b in b.get_ancestors(include_self=True)
True
# Everything's back to normal.