151 lines
5.7 KiB
Python
151 lines
5.7 KiB
Python
"""
|
|
Management command : refresh_reporting_views
|
|
|
|
Crée (première exécution) ou rafraîchit (exécutions suivantes) les vues
|
|
matérialisées du schéma "reporting".
|
|
|
|
Utilise REFRESH MATERIALIZED VIEW CONCURRENTLY pour ne pas bloquer les
|
|
lectures — exige qu'un index unique existe sur chaque vue (créé lors du
|
|
premier CREATE via le fichier SQL correspondant).
|
|
|
|
Usage :
|
|
python manage.py refresh_reporting_views
|
|
python manage.py refresh_reporting_views --force-recreate
|
|
"""
|
|
|
|
import time
|
|
from pathlib import Path
|
|
|
|
from django.core.management.base import BaseCommand
|
|
from django.db import connection
|
|
|
|
|
|
# Ordre de création/refresh : les dépendances d'abord.
|
|
# Clés : "schema.view_name" (doit correspondre au nom du fichier SQL sans extension)
|
|
VIEWS_IN_ORDER = [
|
|
"reporting.mv_location_hierarchy",
|
|
"reporting.mv_interventions",
|
|
]
|
|
|
|
SQL_DIR = (
|
|
Path(__file__).resolve()
|
|
.parent # commands/
|
|
.parent # management/
|
|
.parent # common/
|
|
.parent # streetup/ (racine Django)
|
|
/ "reporting"
|
|
/ "sql"
|
|
)
|
|
|
|
|
|
def _view_exists(cursor, view_name: str) -> bool:
|
|
"""Vérifie si la vue matérialisée existe dans pg_matviews."""
|
|
schema, name = view_name.split(".")
|
|
cursor.execute(
|
|
"SELECT 1 FROM pg_matviews WHERE schemaname = %s AND matviewname = %s",
|
|
[schema, name],
|
|
)
|
|
return cursor.fetchone() is not None
|
|
|
|
|
|
class Command(BaseCommand):
|
|
help = (
|
|
"Crée ou rafraîchit les vues matérialisées du schéma reporting. "
|
|
"Destiné à être exécuté nuitamment via ScheduledCommand (3h00)."
|
|
)
|
|
|
|
def add_arguments(self, parser):
|
|
parser.add_argument(
|
|
"--force-recreate",
|
|
action="store_true",
|
|
default=False,
|
|
help=(
|
|
"Force la recréation complète des vues (DROP + CREATE + INDEX). "
|
|
"Bloquant pendant la recréation. Nécessaire après un changement "
|
|
"de structure dans les fichiers SQL."
|
|
),
|
|
)
|
|
|
|
def handle(self, *args, **options):
|
|
force = options["force_recreate"]
|
|
total_start = time.time()
|
|
|
|
self.stdout.write(self.style.MIGRATE_HEADING("Reporting views refresh"))
|
|
|
|
with connection.cursor() as cursor:
|
|
|
|
# 1. Créer le schéma si nécessaire
|
|
cursor.execute("CREATE SCHEMA IF NOT EXISTS reporting;")
|
|
self.stdout.write(" Schéma reporting : OK")
|
|
|
|
for full_name in VIEWS_IN_ORDER:
|
|
_, name = full_name.split(".")
|
|
start = time.time()
|
|
|
|
sql_file = SQL_DIR / f"{name}.sql"
|
|
if not sql_file.exists():
|
|
self.stderr.write(
|
|
self.style.ERROR(f" Fichier SQL introuvable : {sql_file}")
|
|
)
|
|
continue
|
|
|
|
if not _view_exists(cursor, full_name) or force:
|
|
# ── Première création ou recréation forcée ──────────────
|
|
try:
|
|
sql = sql_file.read_text(encoding="utf-8")
|
|
|
|
# Adapter le schéma django. pour l'environnement de test ou de développement
|
|
from django.conf import settings
|
|
import os
|
|
db_schema = os.getenv("DB_SCHEMA")
|
|
if not db_schema:
|
|
options = settings.DATABASES.get('default', {}).get('OPTIONS', {}).get('options', '')
|
|
if 'search_path=' in options:
|
|
db_schema = options.split('search_path=')[1].split(',')[0].strip()
|
|
else:
|
|
db_schema = 'public'
|
|
if settings.DATABASES['default']['NAME'].startswith('test_'):
|
|
db_schema = 'public'
|
|
|
|
sql = sql.replace("django.", f"{db_schema}.")
|
|
cursor.execute(sql)
|
|
elapsed = time.time() - start
|
|
verb = "recréée" if force else "créée"
|
|
self.stdout.write(
|
|
self.style.SUCCESS(
|
|
f" {full_name} : {verb} ({elapsed:.1f}s)"
|
|
)
|
|
)
|
|
except Exception as exc:
|
|
self.stderr.write(
|
|
self.style.ERROR(
|
|
f" {full_name} : ERREUR lors de la création — {exc}"
|
|
)
|
|
)
|
|
raise
|
|
else:
|
|
# ── Refresh non-bloquant ────────────────────────────────
|
|
try:
|
|
cursor.execute(
|
|
f"REFRESH MATERIALIZED VIEW CONCURRENTLY {full_name};"
|
|
)
|
|
elapsed = time.time() - start
|
|
self.stdout.write(
|
|
self.style.SUCCESS(
|
|
f" {full_name} : rafraîchie ({elapsed:.1f}s)"
|
|
)
|
|
)
|
|
except Exception as exc:
|
|
self.stderr.write(
|
|
self.style.ERROR(
|
|
f" {full_name} : ERREUR lors du refresh — {exc}"
|
|
)
|
|
)
|
|
raise
|
|
|
|
total = time.time() - total_start
|
|
self.stdout.write(
|
|
self.style.SUCCESS(
|
|
f"\nReporting views refresh terminé en {total:.1f}s"
|
|
)
|
|
)
|