Эффективная функция для извлечения набора запросов предков из набора запросов mptt

Есть ли у кого-нибудь эффективный алгоритм для извлечения всех предков из набора запросов mptt? Самое лучшее, что я мог додумать до сих пор, это что-то вроде этого:

def qs_ancestors(queryset):
    if isinstance(queryset, EmptyQuerySet):
        return queryset
    queryset_aggs = queryset.values_list('tree_id', 'level').annotate(max_lft=Max('lft'), min_rght=Min('rght'))
    new_queryset = queryset.none()
    for tree_id, level, max_lft, min_rght in queryset_aggs:
        ancestors = MyModel.objects.filter(
           tree_id=tree_id,
           level__lt=level, 
           lft__lte=max_lft,
           rght__gte=min_rght,
        )
        new_queryset = ancestors | new_queryset
    return new_queryset

Существуют две проблемы с этим подходом:

  • Это не удается, если есть ветки, которые не находятся рядом друг с другом (т.е. на самом деле это не работает)
  • Это очень неэффективно, потому что в конечном итоге у него есть предложения number_of_trees*number_of_levels в конечном запросе, который может очень быстро стать очень быстрым.

Я открыт для кэширования предков где-то в другом месте, но я не могу придумать способ сделать это эффективно. Я подумал о добавлении поля с разделенным запятыми списком идентификаторов предков, а затем сделать GROUP_CONCAT (я в MySQL) в дополнение, но я думаю, что это может стать огромным/медленным.

Ответы

Ответ 1

Как насчет:

def qs_ancestors(queryset):
    if isinstance(queryset, EmptyQuerySet):
        return queryset
    new_queryset = queryset.none()
    for obj in queryset:
        new_queryset = new_queryset | obj.get_ancestors()
return new_queryset

Это все еще len (queryset). Вы могли бы немного уменьшить количество предложений, выполнив препроцессный проход, который удаляет объекты, которые являются предками других объектов в наборе запросов, например:

min_obj_set = []
for obj in queryset.order_by('tree_id', '-level'):
    for obj2 in min_obj_set:
        if obj.is_ancestor_of(obj2):
            break
    else:
        min_obj_set.append(obj)

Хотя приведенный выше фрагмент является лишь примером, вы, вероятно, захотите использовать BST, если ваш querset содержит значительное количество объектов.

Вам нужно будет проверить, увеличивает ли это увеличение скорости по сравнению с более крупным запросом БД.

Ответ 2

Мне пришлось написать аналогичный алгоритм один раз. У меня было представление, показывающее дерево MPTT, это было очень большое дерево, поэтому я не мог загрузить все его данные в HTML-шаблоне. Поэтому я отобразил только корневые узлы начальной загрузки и использовал Ajax для загрузки других узлов.

Он работал хорошо, пока мой босс не попросил меня реализовать опцию "поиск". Поиск должен был выглядеть во всех узлах и взорвать дерево, если бы он нашел совпадение. Мне потребовалось некоторое время, чтобы понять это, но я, наконец, понял. Вот решение, которое придумало:

from django.db.models import Q

def get_parents(self, qs):
    tree_list = {}
    query = Q()
    for node in qs:
        if node.tree_id not in tree_list:
            tree_list[node.tree_id] = []

        parent =  node.parent.pk if node.parent is not None else None,

        if parent not in tree_list[node.tree_id]:
            tree_list[node.tree_id].append(parent)

            query |= Q(lft__lt=node.lft, rght__gt=node.rght, tree_id=node.tree_id)

    return YourModel.objects.filter(query)

Ему нужно всего два запроса для запуска, исходный qs передан как аргумент и окончательный запрос, возвращаемый функцией. tree_list - это словарь, в котором хранятся узлы, которые уже были добавлены в набор запросов, это оптимизация и не требуется, чтобы алгоритм работал. Но поскольку я работал с относительным большим деревом, я должен был включить его.

Я думаю, вы можете превратить этот метод в диспетчер и сделать его более универсальным, т.е. заставить его работать для любой модели MPTT, а не только YourModel