Django рекурсивная аннотация - PullRequest
1 голос
/ 05 марта 2020

Я создаю приложение Django с рекурсивной структурой комментариев.

Проблема: Рекурсивный характер структуры данных моих комментариев означает, что я изо всех сил пытаюсь написать запрос, чтобы аннотировать каждый пост числом ответов, а затем перебирать эти сообщения / ответы в моем шаблоне. .

Модель комментариев, которую я построил, различает ответы на сообщения (которые являются комментариями верхнего уровня) и комментарии (которые являются ответами на другие комментарии).

(Post)
3 Total Comments
-----------------
one (post reply)
└── two (comment reply)
    └── three (comment reply)    
(more)

Я представил следующий комментарий:

class Comment(TimeStamp):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    content = models.TextField(max_length=2000)
    post = models.ForeignKey("Post", on_delete=models.CASCADE, related_name="comments")
    # Top level comments are those that aren't replies to other comments
    reply = models.ForeignKey(
        "self", on_delete=models.PROTECT, null=True, blank=True, related_name="replies"
    )

Это работает очень хорошо, пи c связано

enter image description here

Что работает

Я могу предварительно выбрать все ответы на комментарии для сообщения следующим образом:

comment_query = Comment.objects.annotate(num_replies=Count("replies"))
post = Post.objects.prefetch_related(Prefetch("comments", comment_query)).get(id="1")

, который правильно отображает количество ответов для каждого комментарий:

>>> post.comments.values_list('num_replies')                                                                                                                                                 
<QuerySet [(1,), (1,), (0,)]>

Что не работает

Этот запрос аннотирует только верхний уровень post.comments

>>> post.comments.first().replies.all()                                                                                                                                                      
<QuerySet [<Comment: two>]>

>>> post.comments.first().replies.first().num_replies                                                                                                                                       
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-132-8151a7d13021> in <module>
----> 1 post.comments.first().replies.first().num_replies

AttributeError: 'Comment' object has no attribute 'num_replies'                                                                                                                              

Для правильной визуализации по шаблону мне нужно перебирать comment.replies для каждого ответа верхнего уровня. Поэтому во всех вложенных ответах на комментарии отсутствует исходная аннотация num_replies.

В моей логике шаблона / представления c Я отображаю деревья комментариев примерно со следующими логиками c:

{% for comment in post.comments.all %}
{% if not comment.reply %}
  {% include "posts/comment_tree.html" %}
{% endif %}
{% endfor %}

Где post/comments_tree.html содержит:

{{ post.content }}
{% for reply in comment.replies.all %}
   {% include "posts/comment_tree.html" with comment=reply %}
{% endfor %}

Что я уже пробовал

Я могу обойти эту проблему в некоторой степени, выполнив следующие действия, которые аннотируют первый уровень ответов :

comment_query = Comment.objects.prefetch_related(
    Prefetch("replies", Comment.objects.annotate(num_replies=Count("replies")))
).annotate(num_replies=Count("replies"))

Это успешно аннотирует второй комментарий, который является вложенным ответом

>>> post.comments.first().replies.first().num_replies                                                                                                                                       
1

Но он не будет работать для дальнейших вложенных комментариев (т. Е. Третьего)

>>> post.comments.first().replies.first().replies.first().num_replies                                                                                                                       
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-127-7d5b9798b7b1> in <module>
----> 1 post.comments.first().replies.first().replies.first().num_replies

AttributeError: 'Comment' object has no attribute 'num_replies'

Очевидно, что этот подход полностью ошибочен, так как я буду вынужден добавить вложенный оператор Prefetch для общего количества вложенных комментариев, которые я хочу поддержать. В идеале мне бы хотелось решение, которое позволило бы мне аннотировать вложенную (самореференциальную) структуру данных.

TLDR: возможен ли этот тип запроса даже в ORM Django, или мне придется достичь SQL?

1 Ответ

1 голос
/ 05 марта 2020

Взгляните на django -cte . Вы хотите определить CTE (общее табличное выражение), которое содержит аннотацию. Затем используйте этот CTE в запросе для получения комментариев к сообщению.

Из документов django -cte:

class Region(Model):
    objects = CTEManager()
    name = TextField(primary_key=True)
    parent = ForeignKey("self", null=True, on_delete=CASCADE)

def make_regions_cte(cte):
    return Region.objects.filter(
        # start with root nodes
        parent__isnull=True
    ).values(
        "name",
        path=F("name"),
        depth=Value(0, output_field=IntegerField()),
    ).union(
        # recursive union: get descendants
        cte.join(Region, parent=cte.col.name).values(
            "name",
            path=Concat(
                cte.col.path, Value("\x01"), F("name"),
                output_field=TextField(),
            ),
            depth=cte.col.depth + Value(1, output_field=IntegerField()),
        ),
        all=True,
    )

cte = With.recursive(make_regions_cte)

regions = (
    cte.join(Region, name=cte.col.name)
    .with_cte(cte)
    .annotate(
        path=cte.col.path,
        depth=cte.col.depth,
    )
    .order_by("path")
)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...