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:
parent
e97dc26514
commit
dfe98244e8
6
backend/package-lock.json
generated
Normal file
6
backend/package-lock.json
generated
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
251
backend/scripts/analyze_queries.py
Normal file
251
backend/scripts/analyze_queries.py
Normal 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()
|
||||
Loading…
x
Reference in New Issue
Block a user