эффективная функция для извлечения набора запросов предков набора запросов 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) внутри дополнительного, но я думаю, что это может быть огромным/медленным.
2 ответа:
Как насчет:
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 содержит значительное количество объектов.
Вы должны будете проверить, если это yeilds увеличение скорости по сравнению с большим запросом DB, хотя.
Однажды мне пришлось написать подобный алгоритм. У меня было представление, отображающее дерево 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
, передаваемый в качестве аргумента, и конечный queryset, возвращаемый функцией.tree_list
- это словарь, в котором хранятся узлы, уже добавленные в queryset, это оптимизация, и она не нужна для работы алгоритма. Но так как я работал с относительным большим деревом, я должен был включить его.Я думаю, что вы могли бы превратить этот метод в менеджер и сделать его более универсальным, т. е. работа для любой модели MPTT, а не только
YourModel