From 558f13ef7dbb963700d419c167ff3f3d697bc6fe Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:43:34 +0300 Subject: [PATCH] feat: implement optimized method for retrieving template tasks with subtasks using SQL JOIN --- .../models/template.task.model.js | 48 +++++++++++ .../template.task/template.task.controller.js | 2 +- .../template.task/template.task.service.js | 80 ++++++++++++++++++- 3 files changed, 125 insertions(+), 5 deletions(-) diff --git a/src/entities/template.task/models/template.task.model.js b/src/entities/template.task/models/template.task.model.js index 28d4ad7..9d0c58b 100644 --- a/src/entities/template.task/models/template.task.model.js +++ b/src/entities/template.task/models/template.task.model.js @@ -4,10 +4,57 @@ function getTemplateTaskById(id) { return db.query('SELECT * FROM template_tasks WHERE id = ?', [id]); } +/** + * Get all template tasks for a specific user + * @deprecated Use getAllTemplateTasksWithSubtasksByUser for better performance when subtasks are needed + * @param {number} user_id - User ID + */ function getAllTemplateTasksByUser(user_id) { return db.query('SELECT * FROM template_tasks WHERE user_id = ?', [user_id]); } +/** + * Get all template tasks with their subtasks for a user using SQL JOIN (optimized) + * @param {number} user_id - User ID + * @returns {Promise} Promise resolving to template tasks with subtasks in a flat structure + */ +function getAllTemplateTasksWithSubtasksByUser(user_id) { + return db + .query( + `SELECT + t.id, + t.title, + t.description, + t.category, + t.priority, + t.exp, + t.start_time, + t.end_time, + t.user_id, + t.created_at, + t.special_id, + t.is_active, + t.repeat_rule, + t.start_active_date, + t.end_active_date, + s.id as subtask_id, + s.title as subtask_title, + s.position as subtask_position, + s.special_id as subtask_special_id, + s.created_at as subtask_created_at + FROM template_tasks t + LEFT JOIN template_subtasks s ON t.id = s.template_task_id + WHERE t.user_id = ? + ORDER BY t.id, s.position`, + [user_id] + ) + .then(([rows]) => [rows]) + .catch((error) => { + console.error('Ошибка при получении шаблонов задач с подзадачами:', error); + throw error; + }); +} + function createTemplateTask(templateTask) { const { title, @@ -139,6 +186,7 @@ function getTemplatesForDay(user_id, dayOfWeek) { module.exports = { getTemplateTaskById, getAllTemplateTasksByUser, + getAllTemplateTasksWithSubtasksByUser, createTemplateTask, deleteTemplateTaskById, updateTemplateTaskById, diff --git a/src/entities/template.task/template.task.controller.js b/src/entities/template.task/template.task.controller.js index 1695285..3479648 100644 --- a/src/entities/template.task/template.task.controller.js +++ b/src/entities/template.task/template.task.controller.js @@ -7,7 +7,7 @@ class TemplateTaskController { return res.status(400).json({ error: 'Не передан user-id в заголовке' }); } - templateTaskService.getTemplateTasksWithSubTasks(userId).then((result) => { + templateTaskService.getTemplateTasksWithSubTasksOptimized(userId).then((result) => { res.status(result.status).json(result.data); }); } diff --git a/src/entities/template.task/template.task.service.js b/src/entities/template.task/template.task.service.js index f0b7a14..570deee 100644 --- a/src/entities/template.task/template.task.service.js +++ b/src/entities/template.task/template.task.service.js @@ -26,6 +26,10 @@ class TemplateTaskService { } } + /** + * Get all template tasks with subtasks for a user (deprecated - uses N+1 queries) + * @deprecated Use getTemplateTasksWithSubTasksOptimized instead - it uses optimized SQL JOIN + */ async getTemplateTasksWithSubTasks(userId) { try { const [rows] = await templateTaskModel.getAllTemplateTasksByUser(userId); @@ -47,10 +51,78 @@ class TemplateTaskService { return { status: 200, data: fullTasks }; } catch (err) { - console.error( - '❌ Ошибка при получении шаблонов задач с подзадачами:', - err, - ); + console.error('❌ Ошибка при получении шаблонов задач с подзадачами:', err); + return { status: 500, data: { error: err.message } }; + } + } + + /** + * Get all template tasks with subtasks for a user (optimized with SQL JOIN) + */ + async getTemplateTasksWithSubTasksOptimized(userId) { + try { + const [rows] = await templateTaskModel.getAllTemplateTasksWithSubtasksByUser(userId); + + if (!rows.length) { + return { status: 404, data: { error: 'Шаблоны задач не найдены' } }; + } + + // Группируем результаты по задачам + const tasksMap = new Map(); + + for (const row of rows) { + const taskId = row.id; + + // Создаем задачу, если её еще нет + if (!tasksMap.has(taskId)) { + tasksMap.set(taskId, { + id: row.id, + title: row.title, + description: row.description, + category: row.category, + priority: row.priority, + exp: row.exp, + start_time: row.start_time, + end_time: row.end_time, + user_id: row.user_id, + created_at: row.created_at, + special_id: row.special_id, + is_active: row.is_active, + repeat_rule: (() => { + if (typeof row.repeat_rule !== 'string') { + return row.repeat_rule; + } + // Пытаемся распарсить JSON, если не получается - оставляем строку как есть + try { + return JSON.parse(row.repeat_rule); + } catch { + // Если это не JSON (например, "weekly" или "quests"), возвращаем как есть + return row.repeat_rule; + } + })(), + start_active_date: row.start_active_date, + end_active_date: row.end_active_date, + subtasks: [], + }); + } + + // Добавляем подзадачу, если она есть + if (row.subtask_id) { + tasksMap.get(taskId).subtasks.push({ + id: row.subtask_id, + title: row.subtask_title, + position: row.subtask_position, + special_id: row.subtask_special_id, + created_at: row.subtask_created_at, + template_task_id: taskId, + }); + } + } + + const fullTasks = Array.from(tasksMap.values()); + return { status: 200, data: fullTasks }; + } catch (err) { + console.error('❌ Ошибка при получении шаблонов задач с подзадачами:', err); return { status: 500, data: { error: err.message } }; } }