- Added hierarchical comments pagination: - Created new GraphQL query `load_comments_branch` for efficient loading of hierarchical comments - Ability to load root comments with their first N replies - Added pagination for both root and child comments - Using existing `commented` field in `Stat` type to display number of replies - Added special `first_replies` field to store first replies to a comment - Optimized SQL queries for efficient loading of comment hierarchies - Implemented flexible comment sorting system (by time, rating)
This commit is contained in:
parent
615f1fe468
commit
369ff757b0
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -1,3 +1,13 @@
|
|||
#### [0.4.16] - 2025-03-22
|
||||
- Added hierarchical comments pagination:
|
||||
- Created new GraphQL query `load_comments_branch` for efficient loading of hierarchical comments
|
||||
- Ability to load root comments with their first N replies
|
||||
- Added pagination for both root and child comments
|
||||
- Using existing `commented` field in `Stat` type to display number of replies
|
||||
- Added special `first_replies` field to store first replies to a comment
|
||||
- Optimized SQL queries for efficient loading of comment hierarchies
|
||||
- Implemented flexible comment sorting system (by time, rating)
|
||||
|
||||
#### [0.4.15] - 2025-03-22
|
||||
- Upgraded caching system described `docs/caching.md`
|
||||
- Module `cache/memorycache.py` removed
|
||||
|
|
165
docs/comments-pagination.md
Normal file
165
docs/comments-pagination.md
Normal file
|
@ -0,0 +1,165 @@
|
|||
# Пагинация комментариев
|
||||
|
||||
## Обзор
|
||||
|
||||
Реализована система пагинации комментариев по веткам, которая позволяет эффективно загружать и отображать вложенные ветки обсуждений. Основные преимущества:
|
||||
|
||||
1. Загрузка только необходимых комментариев, а не всего дерева
|
||||
2. Снижение нагрузки на сервер и клиент
|
||||
3. Возможность эффективной навигации по большим обсуждениям
|
||||
4. Предзагрузка первых N ответов для улучшения UX
|
||||
|
||||
## API для иерархической загрузки комментариев
|
||||
|
||||
### GraphQL запрос `load_comments_branch`
|
||||
|
||||
```graphql
|
||||
query LoadCommentsBranch(
|
||||
$shout: Int!,
|
||||
$parentId: Int,
|
||||
$limit: Int,
|
||||
$offset: Int,
|
||||
$sort: ReactionSort,
|
||||
$childrenLimit: Int,
|
||||
$childrenOffset: Int
|
||||
) {
|
||||
load_comments_branch(
|
||||
shout: $shout,
|
||||
parent_id: $parentId,
|
||||
limit: $limit,
|
||||
offset: $offset,
|
||||
sort: $sort,
|
||||
children_limit: $childrenLimit,
|
||||
children_offset: $childrenOffset
|
||||
) {
|
||||
id
|
||||
body
|
||||
created_at
|
||||
created_by {
|
||||
id
|
||||
name
|
||||
slug
|
||||
pic
|
||||
}
|
||||
kind
|
||||
reply_to
|
||||
stat {
|
||||
rating
|
||||
commented
|
||||
}
|
||||
first_replies {
|
||||
id
|
||||
body
|
||||
created_at
|
||||
created_by {
|
||||
id
|
||||
name
|
||||
slug
|
||||
pic
|
||||
}
|
||||
kind
|
||||
reply_to
|
||||
stat {
|
||||
rating
|
||||
commented
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Параметры запроса
|
||||
|
||||
| Параметр | Тип | По умолчанию | Описание |
|
||||
|----------|-----|--------------|----------|
|
||||
| shout | Int! | - | ID статьи, к которой относятся комментарии |
|
||||
| parent_id | Int | null | ID родительского комментария. Если null, загружаются корневые комментарии |
|
||||
| limit | Int | 10 | Максимальное количество комментариев для загрузки |
|
||||
| offset | Int | 0 | Смещение для пагинации |
|
||||
| sort | ReactionSort | newest | Порядок сортировки: newest, oldest, like |
|
||||
| children_limit | Int | 3 | Максимальное количество дочерних комментариев для каждого родительского |
|
||||
| children_offset | Int | 0 | Смещение для пагинации дочерних комментариев |
|
||||
|
||||
### Поля в ответе
|
||||
|
||||
Каждый комментарий содержит следующие основные поля:
|
||||
|
||||
- `id`: ID комментария
|
||||
- `body`: Текст комментария
|
||||
- `created_at`: Время создания
|
||||
- `created_by`: Информация об авторе
|
||||
- `kind`: Тип реакции (COMMENT)
|
||||
- `reply_to`: ID родительского комментария (null для корневых)
|
||||
- `first_replies`: Первые N дочерних комментариев
|
||||
- `stat`: Статистика комментария, включающая:
|
||||
- `commented`: Количество ответов на комментарий
|
||||
- `rating`: Рейтинг комментария
|
||||
|
||||
## Примеры использования
|
||||
|
||||
### Загрузка корневых комментариев с первыми ответами
|
||||
|
||||
```javascript
|
||||
const { data } = await client.query({
|
||||
query: LOAD_COMMENTS_BRANCH,
|
||||
variables: {
|
||||
shout: 222,
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
sort: "newest",
|
||||
childrenLimit: 3
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Загрузка ответов на конкретный комментарий
|
||||
|
||||
```javascript
|
||||
const { data } = await client.query({
|
||||
query: LOAD_COMMENTS_BRANCH,
|
||||
variables: {
|
||||
shout: 222,
|
||||
parentId: 123, // ID комментария, для которого загружаем ответы
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
sort: "oldest" // Сортируем ответы от старых к новым
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Пагинация дочерних комментариев
|
||||
|
||||
Для загрузки дополнительных ответов на комментарий:
|
||||
|
||||
```javascript
|
||||
const { data } = await client.query({
|
||||
query: LOAD_COMMENTS_BRANCH,
|
||||
variables: {
|
||||
shout: 222,
|
||||
parentId: 123,
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
childrenLimit: 5,
|
||||
childrenOffset: 3 // Пропускаем первые 3 комментария (уже загруженные)
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Рекомендации по клиентской реализации
|
||||
|
||||
1. Для эффективной работы со сложными ветками обсуждений рекомендуется:
|
||||
|
||||
- Сначала загружать только корневые комментарии с первыми N ответами
|
||||
- При наличии дополнительных ответов (когда `stat.commented > first_replies.length`)
|
||||
добавить кнопку "Показать все ответы"
|
||||
- При нажатии на кнопку загружать дополнительные ответы с помощью запроса с указанным `parentId`
|
||||
|
||||
2. Для сортировки:
|
||||
- По умолчанию использовать `newest` для отображения свежих обсуждений
|
||||
- Предусмотреть переключатель сортировки для всего дерева комментариев
|
||||
- При изменении сортировки перезагружать данные с новым параметром `sort`
|
||||
|
||||
3. Для улучшения производительности:
|
||||
- Кешировать результаты запросов на клиенте
|
||||
- Использовать оптимистичные обновления при добавлении/редактировании комментариев
|
||||
- При необходимости загружать комментарии порциями (ленивая загрузка)
|
|
@ -34,4 +34,15 @@
|
|||
- Поддерживаемые методы: GET, POST, OPTIONS
|
||||
- Настроена поддержка credentials
|
||||
- Разрешенные заголовки: Authorization, Content-Type, X-Requested-With, DNT, Cache-Control
|
||||
- Настроено кэширование preflight-ответов на 20 дней (1728000 секунд)
|
||||
- Настроено кэширование preflight-ответов на 20 дней (1728000 секунд)
|
||||
|
||||
## Пагинация комментариев по веткам
|
||||
|
||||
- Эффективная загрузка комментариев с учетом их иерархической структуры
|
||||
- Отдельный запрос `load_comments_branch` для оптимизированной загрузки ветки комментариев
|
||||
- Возможность загрузки корневых комментариев статьи с первыми ответами на них
|
||||
- Гибкая пагинация как для корневых, так и для дочерних комментариев
|
||||
- Использование поля `stat.commented` для отображения количества ответов на комментарий
|
||||
- Добавление специального поля `first_replies` для хранения первых ответов на комментарий
|
||||
- Поддержка различных методов сортировки (новые, старые, популярные)
|
||||
- Оптимизированные SQL запросы для минимизации нагрузки на базу данных
|
|
@ -37,6 +37,7 @@ from resolvers.reaction import (
|
|||
create_reaction,
|
||||
delete_reaction,
|
||||
load_comment_ratings,
|
||||
load_comments_branch,
|
||||
load_reactions_by,
|
||||
load_shout_comments,
|
||||
load_shout_ratings,
|
||||
|
@ -107,6 +108,7 @@ __all__ = [
|
|||
"load_shout_comments",
|
||||
"load_shout_ratings",
|
||||
"load_comment_ratings",
|
||||
"load_comments_branch",
|
||||
# notifier
|
||||
"load_notifications",
|
||||
"notifications_seen_thread",
|
||||
|
|
|
@ -612,24 +612,22 @@ async def load_shout_comments(_, info, shout: int, limit=50, offset=0):
|
|||
@query.field("load_comment_ratings")
|
||||
async def load_comment_ratings(_, info, comment: int, limit=50, offset=0):
|
||||
"""
|
||||
Load ratings for a specified comment with pagination and statistics.
|
||||
Load ratings for a specified comment with pagination.
|
||||
|
||||
:param info: GraphQL context info.
|
||||
:param comment: Comment ID.
|
||||
:param limit: Number of ratings to load.
|
||||
:param offset: Pagination offset.
|
||||
:return: List of reactions.
|
||||
:return: List of ratings.
|
||||
"""
|
||||
q = query_reactions()
|
||||
|
||||
q = add_reaction_stat_columns(q)
|
||||
|
||||
# Filter, group, sort, limit, offset
|
||||
q = q.filter(
|
||||
and_(
|
||||
Reaction.deleted_at.is_(None),
|
||||
Reaction.reply_to == comment,
|
||||
Reaction.kind == ReactionKind.COMMENT.value,
|
||||
Reaction.kind.in_(RATING_REACTIONS),
|
||||
)
|
||||
)
|
||||
q = q.group_by(Reaction.id, Author.id, Shout.id)
|
||||
|
@ -637,3 +635,186 @@ async def load_comment_ratings(_, info, comment: int, limit=50, offset=0):
|
|||
|
||||
# Retrieve and return reactions
|
||||
return get_reactions_with_stat(q, limit, offset)
|
||||
|
||||
|
||||
@query.field("load_comments_branch")
|
||||
async def load_comments_branch(
|
||||
_,
|
||||
_info,
|
||||
shout: int,
|
||||
parent_id: int | None = None,
|
||||
limit=10,
|
||||
offset=0,
|
||||
sort="newest",
|
||||
children_limit=3,
|
||||
children_offset=0,
|
||||
):
|
||||
"""
|
||||
Загружает иерархические комментарии с возможностью пагинации корневых и дочерних.
|
||||
|
||||
:param info: GraphQL context info.
|
||||
:param shout: ID статьи.
|
||||
:param parent_id: ID родительского комментария (None для корневых).
|
||||
:param limit: Количество комментариев для загрузки.
|
||||
:param offset: Смещение для пагинации.
|
||||
:param sort: Порядок сортировки ('newest', 'oldest', 'like').
|
||||
:param children_limit: Максимальное количество дочерних комментариев.
|
||||
:param children_offset: Смещение для дочерних комментариев.
|
||||
:return: Список комментариев с дочерними.
|
||||
"""
|
||||
# Создаем базовый запрос
|
||||
q = query_reactions()
|
||||
q = add_reaction_stat_columns(q)
|
||||
|
||||
# Фильтруем по статье и типу (комментарии)
|
||||
q = q.filter(
|
||||
and_(
|
||||
Reaction.deleted_at.is_(None),
|
||||
Reaction.shout == shout,
|
||||
Reaction.kind == ReactionKind.COMMENT.value,
|
||||
)
|
||||
)
|
||||
|
||||
# Фильтруем по родительскому ID
|
||||
if parent_id is None:
|
||||
# Загружаем только корневые комментарии
|
||||
q = q.filter(Reaction.reply_to.is_(None))
|
||||
else:
|
||||
# Загружаем только прямые ответы на указанный комментарий
|
||||
q = q.filter(Reaction.reply_to == parent_id)
|
||||
|
||||
# Сортировка и группировка
|
||||
q = q.group_by(Reaction.id, Author.id, Shout.id)
|
||||
|
||||
# Определяем сортировку
|
||||
order_by_stmt = None
|
||||
if sort.lower() == "oldest":
|
||||
order_by_stmt = asc(Reaction.created_at)
|
||||
elif sort.lower() == "like":
|
||||
order_by_stmt = desc("rating_stat")
|
||||
else: # "newest" по умолчанию
|
||||
order_by_stmt = desc(Reaction.created_at)
|
||||
|
||||
q = q.order_by(order_by_stmt)
|
||||
|
||||
# Выполняем запрос для получения комментариев
|
||||
comments = get_reactions_with_stat(q, limit, offset)
|
||||
|
||||
# Если комментарии найдены, загружаем дочерние и количество ответов
|
||||
if comments:
|
||||
# Загружаем количество ответов для каждого комментария
|
||||
await load_replies_count(comments)
|
||||
|
||||
# Загружаем дочерние комментарии
|
||||
await load_first_replies(comments, children_limit, children_offset, sort)
|
||||
|
||||
return comments
|
||||
|
||||
|
||||
async def load_replies_count(comments):
|
||||
"""
|
||||
Загружает количество ответов для списка комментариев и обновляет поле stat.commented.
|
||||
|
||||
:param comments: Список комментариев, для которых нужно загрузить количество ответов.
|
||||
"""
|
||||
if not comments:
|
||||
return
|
||||
|
||||
comment_ids = [comment["id"] for comment in comments]
|
||||
|
||||
# Запрос для подсчета количества ответов
|
||||
q = (
|
||||
select(Reaction.reply_to.label("parent_id"), func.count().label("count"))
|
||||
.where(
|
||||
and_(
|
||||
Reaction.reply_to.in_(comment_ids),
|
||||
Reaction.deleted_at.is_(None),
|
||||
Reaction.kind == ReactionKind.COMMENT.value,
|
||||
)
|
||||
)
|
||||
.group_by(Reaction.reply_to)
|
||||
)
|
||||
|
||||
# Выполняем запрос
|
||||
with local_session() as session:
|
||||
result = session.execute(q).fetchall()
|
||||
|
||||
# Создаем словарь {parent_id: count}
|
||||
replies_count = {row[0]: row[1] for row in result}
|
||||
|
||||
# Добавляем значения в комментарии
|
||||
for comment in comments:
|
||||
if "stat" not in comment:
|
||||
comment["stat"] = {}
|
||||
|
||||
# Обновляем счетчик комментариев в stat
|
||||
comment["stat"]["commented"] = replies_count.get(comment["id"], 0)
|
||||
|
||||
|
||||
async def load_first_replies(comments, limit, offset, sort="newest"):
|
||||
"""
|
||||
Загружает первые N ответов для каждого комментария.
|
||||
|
||||
:param comments: Список комментариев, для которых нужно загрузить ответы.
|
||||
:param limit: Максимальное количество ответов для каждого комментария.
|
||||
:param offset: Смещение для пагинации дочерних комментариев.
|
||||
:param sort: Порядок сортировки ответов.
|
||||
"""
|
||||
if not comments or limit <= 0:
|
||||
return
|
||||
|
||||
# Собираем ID комментариев
|
||||
comment_ids = [comment["id"] for comment in comments]
|
||||
|
||||
# Базовый запрос для загрузки ответов
|
||||
q = query_reactions()
|
||||
q = add_reaction_stat_columns(q)
|
||||
|
||||
# Фильтрация: только ответы на указанные комментарии
|
||||
q = q.filter(
|
||||
and_(
|
||||
Reaction.reply_to.in_(comment_ids),
|
||||
Reaction.deleted_at.is_(None),
|
||||
Reaction.kind == ReactionKind.COMMENT.value,
|
||||
)
|
||||
)
|
||||
|
||||
# Группировка
|
||||
q = q.group_by(Reaction.id, Author.id, Shout.id)
|
||||
|
||||
# Определяем сортировку
|
||||
order_by_stmt = None
|
||||
if sort.lower() == "oldest":
|
||||
order_by_stmt = asc(Reaction.created_at)
|
||||
elif sort.lower() == "like":
|
||||
order_by_stmt = desc("rating_stat")
|
||||
else: # "newest" по умолчанию
|
||||
order_by_stmt = desc(Reaction.created_at)
|
||||
|
||||
q = q.order_by(order_by_stmt, Reaction.reply_to)
|
||||
|
||||
# Выполняем запрос
|
||||
replies = get_reactions_with_stat(q)
|
||||
|
||||
# Группируем ответы по родительским ID
|
||||
replies_by_parent = {}
|
||||
for reply in replies:
|
||||
parent_id = reply.get("reply_to")
|
||||
if parent_id not in replies_by_parent:
|
||||
replies_by_parent[parent_id] = []
|
||||
replies_by_parent[parent_id].append(reply)
|
||||
|
||||
# Добавляем ответы к соответствующим комментариям с учетом смещения и лимита
|
||||
for comment in comments:
|
||||
comment_id = comment["id"]
|
||||
if comment_id in replies_by_parent:
|
||||
parent_replies = replies_by_parent[comment_id]
|
||||
# Применяем смещение и лимит
|
||||
comment["first_replies"] = parent_replies[offset : offset + limit]
|
||||
else:
|
||||
comment["first_replies"] = []
|
||||
|
||||
# Загружаем количество ответов для дочерних комментариев
|
||||
all_replies = [reply for replies in replies_by_parent.values() for reply in replies]
|
||||
if all_replies:
|
||||
await load_replies_count(all_replies)
|
||||
|
|
|
@ -26,6 +26,9 @@ type Query {
|
|||
load_shout_ratings(shout: Int!, limit: Int, offset: Int): [Reaction]
|
||||
load_comment_ratings(comment: Int!, limit: Int, offset: Int): [Reaction]
|
||||
|
||||
# branched comments pagination
|
||||
load_comments_branch(shout: Int!, parent_id: Int, limit: Int, offset: Int, sort: ReactionSort, children_limit: Int, children_offset: Int): [Reaction]
|
||||
|
||||
# reader
|
||||
get_shout(slug: String, shout_id: Int): Shout
|
||||
load_shouts_by(options: LoadShoutsOptions): [Shout]
|
||||
|
|
Loading…
Reference in New Issue
Block a user