Skip to content

refactor(panorama/cities): resolución N+1, indexación unaccent y eliminación de patrón Fat Controller - #69

Merged
Ochoa-Stack merged 7 commits into
developfrom
refactor/panorama-service-and-city-decoupling
Aug 1, 2026
Merged

refactor(panorama/cities): resolución N+1, indexación unaccent y eliminación de patrón Fat Controller#69
Ochoa-Stack merged 7 commits into
developfrom
refactor/panorama-service-and-city-decoupling

Conversation

@Ochoa-Stack

Copy link
Copy Markdown
Owner

refactor/panorama-service-and-city-decoupling

Descripción

En este ciclo de reestructuración arquitectónica profunda, mitigamos problemas severos de rendimiento ocasionados por consultas iterativas a la base de datos, delegamos la orquestación de la geocodificación a la capa de servicios adecuada e implementamos índices funcionales para optimizar las búsquedas textuales. Culminamos el proceso despojando a los endpoints de la capa de red de cualquier responsabilidad de acceso directo a persistencia.

Detalles técnicos que integramos:

  • Resolución de Problema N+1 (DT-26): Iniciamos con una prueba de caracterización que expuso el comportamiento ineficiente del endpoint de comparativas (ejecutando 15 consultas iterativas para 5 habilidades). Implementamos métodos de lectura por lotes (batch) en los repositorios utilizando operaciones eficientes como DISTINCT ON. Se migró la lógica hacia PanoramaService, reduciendo el impacto en base de datos a solo 3 consultas, sin modificar el contrato de respuesta hacia el cliente.
  • Indexación Funcional (DT-27 - Parte 1): Habilitamos la extensión unaccent en PostgreSQL. Para soportar su indexación (dado que la extensión nativa es STABLE y no IMMUTABLE), implementamos un wrapper inmutable que permitió reemplazar la búsqueda lineal de nombres de ciudades O(n) por consultas respaldadas por índices reales en la base de datos.
  • Desacoplamiento de Orquestación Geográfica (DT-27 - Parte 2): Redujimos CityRepository a un componente de persistencia pura, eliminando transacciones acopladas y llamadas a redes externas. La interacción con Nominatim y la resolución geográfica fue encapsulada dentro de CityService. Inyectamos tolerancia a fallos frente a condiciones de carrera concurrentes capturando de forma explícita el error de colisión de llave única (ConflictError).
  • Erradicación del Patrón Fat Controller: Los 7 endpoints del dominio Panorama (panorama_bp.py) delegaron la totalidad de su lógica de negocio y consultas directas hacia PanoramaService. El controlador ahora funge estrictamente como gestor del contrato HTTP (entradas y códigos de estado).
  • Adaptación y Expansión de la Suite de Pruebas: Migramos sistemáticamente las pruebas desde los repositorios hacia los servicios. Manejamos intencionalmente los fallos temporales inter-commits y ajustamos los aislamientos simulados (mocks). El ciclo cierra certificando 140 pruebas totales, libres de regresiones acumuladas.

Tipo de cambio

  • feat
  • fix
  • refactor
  • chore
  • docs
  • test
  • ci

Cómo probar

  1. Tras el merge, sincronizamos la rama base en nuestro entorno local.
  2. En la raíz del backend (cd backend), activamos el entorno virtual.
  3. Aseguramos la inicialización de la extensión PostgreSQL localizando y aplicando la última migración (necesaria para unaccent):
    flask db upgrade
  4. Ejecutamos la suite de pruebas automatizada para certificar el éxito del desacoplamiento, la resolución de condiciones de carrera y la preservación de la lógica sin regresiones:
    pytest -v
  5. Opcionalmente, interactuamos directamente mediante un cliente HTTP sobre los endpoints /api/panorama/compare comprobando que los resultados arrojados permanecen idénticos, a pesar de que el costo computacional subyacente bajó drásticamente.

Checklist

  • Sigue las convenciones de commits del proyecto (separación atómica rigurosa)
  • El problema de N+1 (DT-26) en el panel de comparación está resuelto arquitectónicamente
  • La orquestación geográfica (DT-27) fue aislada hacia la capa de Servicios previniendo condiciones de carrera
  • Los controladores públicos no operan llamadas directas contra la capa de repositorios
  • La suite automatizada finalizó en 140/140 pruebas superadas

Ochoa-Stack and others added 6 commits July 31, 2026 16:53
Documenta el comportamiento N+1 actual de get_compare (confirmado:
15 queries exactas para 5 skills, 3 por skill). Este test se actualiza,
no se elimina, cuando la Ronda 3 introduzca los métodos batch que
reduzcan el conteo a 3 queries.

Co-authored-by: Oscar Soriano <neko.dev@outlook.com>
Co-authored-by: Aylin Chavira <aylinchavirachv@gmail.com>
Co-authored-by: Alejandro Balderrama <alejandro64.bp@gmail.com>
…shots

Agrega SkillRepository.get_by_ids, TrendSnapshotRepository.get_by_skill_ids
y TrendSnapshotRepository.get_latest_by_skill_ids (via DISTINCT ON) para
resolver DT-26. Sin consumidores todavía; panorama_bp.get_compare se migra
en la siguiente ronda.

Co-authored-by: Oscar Soriano <neko.dev@outlook.com>
Co-authored-by: Aylin Chavira <aylinchavirachv@gmail.com>
Co-authored-by: Alejandro Balderrama <alejandro64.bp@gmail.com>
Extrae la logica de get_compare a PanoramaService.get_compare_data,
usando los metodos batch de la ronda anterior. Reduce el patron N+1
de 15 queries a 3 para 5 habilidades, verificado con test de
caracterizacion. Comportamiento observable del endpoint sin cambios.

Co-authored-by: Oscar Soriano <neko.dev@outlook.com>
Co-authored-by: Aylin Chavira <aylinchavirachv@gmail.com>
Co-authored-by: Alejandro Balderrama <alejandro64.bp@gmail.com>
Habilita la extension unaccent de PostgreSQL (via wrapper immutable_unaccent
para permitir indexacion, dado que unaccent nativo es STABLE no IMMUTABLE)
y agrega CityRepository.find_by_normalized_name, verificado en 1 query
indexada. Prepara la base para DT-27: get_or_create_city y _normalize
siguen intactos, se reconectan en la siguiente ronda.

Co-authored-by: Oscar Soriano <neko.dev@outlook.com>
Co-authored-by: Aylin Chavira <aylinchavirachv@gmail.com>
Co-authored-by: Alejandro Balderrama <alejandro64.bp@gmail.com>
Resuelve DT-27. CityRepository queda reducido a persistencia pura
(find_by_normalized_name, get_by_name, create heredado). CityService
nuevo orquesta busqueda indexada + Nominatim + persistencia, capturando
ConflictError para condiciones de carrera bajo la constraint unique de
City.name. ingestion_service.py actualizado para consumir CityService.

11 tests fallan intencionalmente en este punto (esperado, corregidos en
la siguiente ronda): 6 en test_city_repository.py probaban el metodo
eliminado, 5 en test_ingestion_service.py mockeaban la ruta antigua.

Co-authored-by: Oscar Soriano <neko.dev@outlook.com>
Co-authored-by: Aylin Chavira <aylinchavirachv@gmail.com>
Co-authored-by: Alejandro Balderrama <alejandro64.bp@gmail.com>
test_city_repository.py reducido a persistencia pura (get_by_name).
test_city_service.py nuevo: migra los 6 tests de orquestacion con mocks
corregidos al namespace correcto, mas un test nuevo de recuperacion
ante ConflictError por condicion de carrera concurrente.
test_ingestion_service.py: 5 mocks corregidos de CityRepository a
CityService. Suite completa: 140 tests, cero regresiones.

Co-authored-by: Oscar Soriano <neko.dev@outlook.com>
Co-authored-by: Aylin Chavira <aylinchavirachv@gmail.com>
Co-authored-by: Alejandro Balderrama <alejandro64.bp@gmail.com>
@Ochoa-Stack
Ochoa-Stack force-pushed the refactor/panorama-service-and-city-decoupling branch 4 times, most recently from 5d0ae52 to f7733ae Compare August 1, 2026 04:27
…ints

Extrae get_skills, get_catalogs, get_summary, get_top_skills, get_trends,
get_geo y get_salaries a PanoramaService. panorama_bp.py ya no llama a
ningun repositorio directamente en ningun endpoint. SkillRepository.get_by_id
y TrendSnapshotRepository.get_top_skills permanecen sin modificar dado que
tienen consumidores externos (profile_bp.py, alerts_service.py,
profile_service.py), confirmado sin cambios via git status.

Cierra el alcance completo de esta rama: DT-26, DT-27, dos hallazgos sin
numerar (transaccion movida fuera de CityRepository, busqueda O(n)
reemplazada por indice unaccent). Suite completa: 140 tests, cero
regresiones acumuladas en las 7 rondas.

Co-authored-by: Oscar Soriano <neko.dev@outlook.com>
Co-authored-by: Aylin Chavira <aylinchavirachv@gmail.com>
Co-authored-by: Alejandro Balderrama <alejandro64.bp@gmail.com>
@Ochoa-Stack
Ochoa-Stack force-pushed the refactor/panorama-service-and-city-decoupling branch from f7733ae to 83e6c63 Compare August 1, 2026 16:29
@Ochoa-Stack
Ochoa-Stack merged commit 6e6a9be into develop Aug 1, 2026
1 check passed
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 95.34884% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
backend/app/services/panorama_service.py 91.78% 6 Missing ⚠️

📢 Thoughts on this report? Let us know!

@Ochoa-Stack
Ochoa-Stack deleted the refactor/panorama-service-and-city-decoupling branch August 1, 2026 16:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants