# -*- coding: utf-8 -*- """ Management команда для полной очистки данных тенанта без удаления схемы и самого тенанта. ВАЖНО: Это необратимая операция! Все данные тенанта будут удалены, но схема и запись в Client останутся. Использование: # Очистка всех данных тенанта python manage.py clear_tenant_data --schema=anatol --noinput # С подтверждением python manage.py clear_tenant_data --schema=anatol """ from django.core.management.base import BaseCommand from django.db import connection from django.apps import apps from django_tenants.utils import schema_context from tenants.models import Client class Command(BaseCommand): help = 'Полная очистка всех данных в схеме тенанта без удаления схемы и Client' def add_arguments(self, parser): parser.add_argument( '--schema', type=str, required=True, help='Имя схемы БД тенанта для очистки (пример: anatol)' ) parser.add_argument( '--noinput', action='store_true', help='Не запрашивать подтверждение (для автоматизации)' ) def handle(self, *args, **options): self.stdout.write(self.style.SUCCESS('\n=== Очистка данных тенанта ===\n')) # Получаем параметры schema_name = options.get('schema') noinput = options.get('noinput', False) # Проверяем что тенант существует try: tenant = Client.objects.get(schema_name=schema_name) except Client.DoesNotExist: self.stdout.write(self.style.ERROR(f'\nОШИБКА: Тенант со схемой "{schema_name}" не найден\n')) return # Получаем список всех таблиц в схеме (кроме служебных django) tables = self._get_tenant_tables(schema_name) # Показываем информацию о том что будет удалено self.stdout.write('='*70) self.stdout.write(self.style.WARNING('ВНИМАНИЕ! Следующие данные будут удалены:')) self.stdout.write('='*70) self.stdout.write(f'\n[Тенант]') self.stdout.write(f' Название: {tenant.name}') self.stdout.write(f' Схема: {tenant.schema_name}') self.stdout.write(f' Владелец: {tenant.owner_email}') self.stdout.write(f'\n[База данных]') self.stdout.write(f' Схема: "{schema_name}" (НЕ будет удалена)') self.stdout.write(f' Будет очищено таблиц: {len(tables)}') if tables: self.stdout.write(f'\n[Таблицы для очистки]') for table in tables[:20]: # Показываем первые 20 self.stdout.write(f' - {table}') if len(tables) > 20: self.stdout.write(f' ... и ещё {len(tables) - 20} таблиц') self.stdout.write(f'\n[Что НЕ будет удалено]') self.stdout.write(f' ✓ Запись Client (тенант)') self.stdout.write(f' ✓ Схема БД "{schema_name}"') self.stdout.write(f' ✓ Домены тенанта') self.stdout.write('\n' + '='*70) # Запрашиваем подтверждение if not noinput: confirm = input(self.style.WARNING('\nВы уверены? Введите "yes" для подтверждения: ')) if confirm.lower() not in ['yes', 'да', 'y']: self.stdout.write(self.style.ERROR('\nОтменено\n')) return # Начинаем очистку self.stdout.write('\n' + '='*70) self.stdout.write(self.style.SUCCESS('Начинаю очистку данных...')) self.stdout.write('='*70 + '\n') try: deleted_count = self._clear_tenant_data(schema_name, tables) # Итоговое сообщение self.stdout.write('\n' + '='*70) self.stdout.write(self.style.SUCCESS('УСПЕХ: Данные тенанта очищены!')) self.stdout.write('='*70) self.stdout.write(self.style.WARNING('\nИтоги очистки:')) self.stdout.write(f' - Очищено таблиц: {deleted_count}') self.stdout.write(f' - Схема "{schema_name}" сохранена') self.stdout.write(f' - Тенант "{tenant.name}" сохранён') # Предлагаем инициализировать системные данные self._suggest_init_data(schema_name) except Exception as e: self.stdout.write(self.style.ERROR(f'\nОШИБКА при очистке данных: {e}')) self.stdout.write(self.style.ERROR(' Данные могут быть частично удалены. Проверьте состояние БД.\n')) raise def _get_tenant_tables(self, schema_name): """ Получает список всех таблиц в схеме тенанта (кроме служебных). """ with connection.cursor() as cursor: cursor.execute(""" SELECT table_name FROM information_schema.tables WHERE table_schema = %s AND table_type = 'BASE TABLE' AND table_name NOT IN ( 'django_migrations', 'django_content_type', 'django_admin_log', 'django_session' ) ORDER BY table_name """, [schema_name]) return [row[0] for row in cursor.fetchall()] def _clear_tenant_data(self, schema_name, tables): """ Очищает все данные в таблицах тенанта. Использует TRUNCATE CASCADE для удаления связанных данных. """ deleted_count = 0 with connection.cursor() as cursor: # Устанавливаем search_path на схему тенанта cursor.execute(f'SET search_path TO {schema_name}') # Отключаем триггеры и ограничения для быстрой очистки self.stdout.write('[1] Отключение триггеров и ограничений...') cursor.execute('SET session_replication_role = replica;') # Очищаем каждую таблицу self.stdout.write(f'[2] Очистка {len(tables)} таблиц...') for i, table in enumerate(tables, 1): try: # Используем TRUNCATE CASCADE для удаления связанных данных cursor.execute(f'TRUNCATE TABLE {table} CASCADE;') deleted_count += 1 if i % 10 == 0: self.stdout.write(f' Очищено таблиц: {i}/{len(tables)}') except Exception as e: self.stdout.write( self.style.WARNING(f' Предупреждение: не удалось очистить таблицу {table}: {e}') ) # Включаем триггеры и ограничения обратно self.stdout.write('[3] Включение триггеров и ограничений...') cursor.execute('SET session_replication_role = DEFAULT;') # Сбрасываем последовательности (sequences) для ID self.stdout.write('[4] Сброс последовательностей (ID sequences)...') cursor.execute(""" SELECT 'SELECT SETVAL(' || quote_literal(quote_ident(sequence_schema) || '.' || quote_ident(sequence_name)) || ', 1, false);' FROM information_schema.sequences WHERE sequence_schema = %s; """, [schema_name]) for row in cursor.fetchall(): try: cursor.execute(row[0]) except Exception as e: # Игнорируем ошибки при сбросе последовательностей pass self.stdout.write(self.style.SUCCESS(f' OK: Все данные очищены')) return deleted_count def _suggest_init_data(self, schema_name): """ Предлагает пользователю инициализировать системные данные. """ self.stdout.write('\n' + '-'*70) self.stdout.write(self.style.WARNING('Рекомендация:')) self.stdout.write('-'*70) self.stdout.write('Для работы системы необходимы системные данные.') self.stdout.write('Выполните команду для их создания:\n') self.stdout.write(self.style.SUCCESS(f' python manage.py init_tenant_data --schema={schema_name}')) self.stdout.write('')