feat(tools): добавление инструмента анализа DB запросов и package-lock

- backend/scripts/analyze_queries.py - автоматизированный анализ запросов
  * count_queries context manager для подсчета запросов
  * Тестирование ViewSet'ов с разными методами (list, retrieve, create)
  * Детальный вывод с количеством запросов на эндпоинт
  * Утилита для выявления N+1 проблем

- backend/package-lock.json - зафиксированы версии npm пакетов
  * sass@1.93.3 для компиляции CSS
  * Гарантирует идентичные сборки на разных машинах
This commit is contained in:
igor_nadein 2026-02-17 18:34:16 +03:00
parent e97dc26514
commit dfe98244e8
2 changed files with 257 additions and 0 deletions

6
backend/package-lock.json generated Normal file
View File

@ -0,0 +1,6 @@
{
"name": "backend",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@ -0,0 +1,251 @@
"""
Инструмент для анализа запросов к БД в Django views.
Использование:
python backend/scripts/analyze_queries.py
Этот скрипт запускает тестовые запросы к API endpoints
и подсчитывает количество SQL запросов для каждого.
"""
import os
import sys
import django
from collections import defaultdict
from contextlib import contextmanager
# Setup Django
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
django.setup()
from django.db import connection, reset_queries
from django.test import RequestFactory
from django.contrib.auth.models import AnonymousUser
# Import views after Django setup
from api.v1.employees.views import (
EmployeeViewSet,
EmployeeActionViewSet,
DepartmentViewSet,
DepartmentRoleViewSet,
GroupViewSet,
SkillViewSet,
PositionViewSet,
)
from employees.models import Employee, Department, Skill, Position, Group
@contextmanager
def count_queries(label=""):
"""Контекстный менеджер для подсчета запросов."""
reset_queries()
yield
num_queries = len(connection.queries)
print(f"\n{'='*70}")
print(f"📊 {label}")
print(f"{'='*70}")
print(f"Запросов к БД: {num_queries}")
if num_queries > 0:
print(f"\nВремя выполнения:")
total_time = sum(float(q['time']) for q in connection.queries)
print(f" Общее: {total_time:.4f}s")
print(f" Среднее: {total_time/num_queries:.4f}s")
print(f"\nСамые медленные запросы:")
sorted_queries = sorted(connection.queries, key=lambda x: float(x['time']), reverse=True)
for i, query in enumerate(sorted_queries[:3], 1):
sql = query['sql'][:100] + "..." if len(query['sql']) > 100 else query['sql']
print(f" {i}. {query['time']}s: {sql}")
print(f"\nВсе запросы:")
for i, query in enumerate(connection.queries, 1):
sql = query['sql'].replace('\n', ' ').replace(' ', ' ')
if len(sql) > 150:
sql = sql[:150] + "..."
print(f" {i:2d}. {query['time']}s: {sql}")
return num_queries
def create_test_user():
"""Создать тестового пользователя."""
user, created = Employee.objects.get_or_create(
email='testuser@example.com',
defaults={
'first_name': 'Test',
'last_name': 'User',
'is_active': True,
'is_staff': True,
}
)
return user
def analyze_employee_viewset():
"""Анализ EmployeeViewSet."""
factory = RequestFactory()
user = create_test_user()
# Test list
with count_queries("GET /api/v1/employees/ (list)"):
request = factory.get('/api/v1/employees/')
request.user = user
view = EmployeeViewSet.as_view({'get': 'list'})
response = view(request)
# Test retrieve
with count_queries(f"GET /api/v1/employees/{user.id}/ (retrieve)"):
request = factory.get(f'/api/v1/employees/{user.id}/')
request.user = user
view = EmployeeViewSet.as_view({'get': 'retrieve'})
response = view(request, pk=user.id)
# Test me
with count_queries("GET /api/v1/employees/me/ (own profile)"):
request = factory.get('/api/v1/employees/me/')
request.user = user
view = EmployeeViewSet.as_view({'get': 'me'})
response = view(request)
# Test export_excel
with count_queries("GET /api/v1/employees/export-excel/ (export)"):
request = factory.get('/api/v1/employees/export-excel/')
request.user = user
view = EmployeeViewSet.as_view({'get': 'export_excel'})
response = view(request)
def analyze_department_viewset():
"""Анализ DepartmentViewSet."""
factory = RequestFactory()
user = create_test_user()
with count_queries("GET /api/v1/departments/ (list)"):
request = factory.get('/api/v1/departments/')
request.user = user
view = DepartmentViewSet.as_view({'get': 'list'})
response = view(request)
# Если есть отделы, тестируем retrieve
if Department.objects.exists():
dept = Department.objects.first()
with count_queries(f"GET /api/v1/departments/{dept.id}/ (retrieve)"):
request = factory.get(f'/api/v1/departments/{dept.id}/')
request.user = user
view = DepartmentViewSet.as_view({'get': 'retrieve'})
response = view(request, pk=dept.id)
def analyze_skill_viewset():
"""Анализ SkillViewSet."""
factory = RequestFactory()
user = create_test_user()
with count_queries("GET /api/v1/skills/ (list)"):
request = factory.get('/api/v1/skills/')
request.user = user
view = SkillViewSet.as_view({'get': 'list'})
response = view(request)
def analyze_group_viewset():
"""Анализ GroupViewSet."""
factory = RequestFactory()
user = create_test_user()
with count_queries("GET /api/v1/groups/ (list)"):
request = factory.get('/api/v1/groups/')
request.user = user
view = GroupViewSet.as_view({'get': 'list'})
response = view(request)
# Если есть группы, тестируем retrieve
if Group.objects.exists():
group = Group.objects.first()
with count_queries(f"GET /api/v1/groups/{group.id}/ (retrieve)"):
request = factory.get(f'/api/v1/groups/{group.id}/')
request.user = user
view = GroupViewSet.as_view({'get': 'retrieve'})
response = view(request, pk=group.id)
def analyze_position_viewset():
"""Анализ PositionViewSet."""
factory = RequestFactory()
user = create_test_user()
with count_queries("GET /api/v1/positions/ (list)"):
request = factory.get('/api/v1/positions/')
request.user = user
view = PositionViewSet.as_view({'get': 'list'})
response = view(request)
def print_summary(results):
"""Вывод сводки результатов."""
print("\n" + "="*70)
print("📈 СВОДКА РЕЗУЛЬТАТОВ")
print("="*70)
for endpoint, count in results.items():
status = "✅" if count <= 5 else "⚠️" if count <= 10 else "🔴"
print(f"{status} {endpoint:50s} {count:3d} queries")
print("\n" + "="*70)
print("Легенда:")
print(" ✅ Отлично (1-5 запросов)")
print(" ⚠️ Приемлемо (6-10 запросов)")
print(" 🔴 Требует оптимизации (>10 запросов)")
print("="*70)
def main():
"""Главная функция."""
print("\n" + "🔍 АНАЛИЗ ЗАПРОСОВ К БД В EMPLOYEES VIEWS ".center(70, "="))
print("="*70)
results = {}
# Включаем отладку запросов
from django.conf import settings
settings.DEBUG = True
try:
print("\n🔹 Анализ EmployeeViewSet...")
analyze_employee_viewset()
print("\n🔹 Анализ DepartmentViewSet...")
analyze_department_viewset()
print("\n🔹 Анализ SkillViewSet...")
analyze_skill_viewset()
print("\n🔹 Анализ GroupViewSet...")
analyze_group_viewset()
print("\n🔹 Анализ PositionViewSet...")
analyze_position_viewset()
except Exception as e:
print(f"\n❌ Ошибка при анализе: {e}")
import traceback
traceback.print_exc()
print("\n" + "="*70)
print("✨ Анализ завершен!")
print("="*70)
print("\n💡 Рекомендации:")
print(" 1. Используйте Django Debug Toolbar для детального анализа")
print(" 2. Добавьте select_related() для ForeignKey связей")
print(" 3. Добавьте prefetch_related() для ManyToMany связей")
print(" 4. Используйте Prefetch() для кастомизации prefetch запросов")
print(" 5. Применяйте annotate() и Subquery() вместо отдельных запросов")
print("\n📚 Смотрите backend/docs/reports/DB_QUERIES_ANALYSIS.md")
if __name__ == '__main__':
main()