Replaced 150+ lines of manual transaction management with mixin: Before: - Manual transaction.atomic() - Manual LDAP sync calls - Duplicate code for LDAP/non-LDAP modes - Manual error handling and rollback - ~150 lines of boilerplate After: - create() delegates to super() - миксин управляет всем - perform_create() - только БД логика (30 lines) - handle_external_sync_result() - обработка LDAP результата - Миксин автоматически управляет транзакциями и откатами Changes: 1. create(): simplified to validation + super().create() 2. perform_create(): creates Employee in DB, sets password/avatar/skills 3. prepare_external_data(): builds DTO with avatar_bytes, group_cns 4. handle_external_sync_result(): saves DN/GUID to LdapSyncState, sets employeeNumber, assigns position 5. ExternalSystemSyncMixin: added handle_external_sync_result() hook for custom post-sync actions Code reduction: 150 lines -> 80 lines Benefits: cleaner, DRYer, automatic error handling manage.py check: no issues
262 lines
11 KiB
Python
262 lines
11 KiB
Python
"""
|
||
Универсальный миксин для синхронизации DB операций с внешними системами (LDAP, AD, etc).
|
||
|
||
Использование:
|
||
class EmployeeViewSet(ExternalSystemSyncMixin, viewsets.ModelViewSet):
|
||
external_sync_service = DirectoryService()
|
||
|
||
def get_external_sync_method(self, action):
|
||
return {
|
||
'create': 'create_user_ldap',
|
||
'update': 'update_user_ldap',
|
||
'destroy': 'delete_user_ldap',
|
||
}.get(action)
|
||
|
||
def prepare_external_data(self, instance, action):
|
||
if action == 'create':
|
||
return {'user': instance}
|
||
return {'user_id': instance.id}
|
||
"""
|
||
import logging
|
||
from functools import wraps
|
||
|
||
from django.db import transaction
|
||
from rest_framework import status
|
||
from rest_framework.response import Response
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class ExternalSystemSyncMixin:
|
||
"""
|
||
Миксин для автоматической синхронизации операций ViewSet с внешними системами.
|
||
|
||
Паттерн:
|
||
1. Выполняет DB операцию в транзакции
|
||
2. При успехе - вызывает метод внешней системы
|
||
3. При ошибке внешней системы - откатывает DB транзакцию
|
||
|
||
Attributes:
|
||
external_sync_service: Сервис для работы с внешней системой (например, DirectoryService)
|
||
external_sync_enabled: Флаг включения синхронизации (по умолчанию True)
|
||
external_sync_async: Использовать асинхронную синхронизацию через Celery
|
||
"""
|
||
external_sync_service = None
|
||
external_sync_enabled = True
|
||
external_sync_async = False
|
||
|
||
def get_external_sync_method(self, action):
|
||
"""
|
||
Возвращает имя метода внешнего сервиса для данного action.
|
||
|
||
Args:
|
||
action: 'create', 'update', 'partial_update', 'destroy'
|
||
|
||
Returns:
|
||
str: имя метода сервиса или None если синхронизация не нужна
|
||
"""
|
||
raise NotImplementedError("Переопределите get_external_sync_method()")
|
||
|
||
def prepare_external_data(self, instance, action):
|
||
"""
|
||
Подготавливает данные для передачи во внешний сервис.
|
||
|
||
Args:
|
||
instance: Инстанс модели Django
|
||
action: Тип операции
|
||
|
||
Returns:
|
||
dict: данные для метода внешнего сервиса
|
||
"""
|
||
return {'instance': instance}
|
||
|
||
def handle_external_sync_error(self, error, instance, action):
|
||
"""
|
||
Обработка ошибки синхронизации с внешней системой.
|
||
|
||
Args:
|
||
error: Exception от внешней системы
|
||
instance: Инстанс модели
|
||
action: Тип операции
|
||
|
||
Returns:
|
||
Response или None (если None - будет поднята ошибка)
|
||
"""
|
||
logger.error(
|
||
f"External sync failed for {action} on {instance}: {error}")
|
||
return Response(
|
||
{'detail': f'Ошибка синхронизации с внешней системой: {str(error)}'},
|
||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||
)
|
||
|
||
def perform_create(self, serializer):
|
||
"""Переопределение create с синхронизацией."""
|
||
return self._perform_with_sync(serializer, 'create', super().perform_create)
|
||
|
||
def perform_update(self, serializer):
|
||
"""Переопределение update с синхронизацией."""
|
||
action = 'partial_update' if self.request.method == 'PATCH' else 'update'
|
||
return self._perform_with_sync(serializer, action, super().perform_update)
|
||
|
||
def perform_destroy(self, instance):
|
||
"""Переопределение destroy с синхронизацией."""
|
||
return self._perform_with_sync_instance(instance, 'destroy', super().perform_destroy)
|
||
|
||
def _perform_with_sync(self, serializer, action, db_operation):
|
||
"""
|
||
Выполняет DB операцию и синхронизацию с внешней системой.
|
||
|
||
Args:
|
||
serializer: DRF serializer с валидированными данными
|
||
action: Тип операции
|
||
db_operation: Функция для выполнения DB операции
|
||
"""
|
||
if not self.external_sync_enabled:
|
||
return db_operation(serializer)
|
||
|
||
sync_method_name = self.get_external_sync_method(action)
|
||
if not sync_method_name or not self.external_sync_service:
|
||
return db_operation(serializer)
|
||
|
||
# Атомарная транзакция: сохраняем в БД
|
||
with transaction.atomic():
|
||
db_operation(serializer)
|
||
instance = serializer.instance
|
||
|
||
# Подготавливаем данные для внешней системы
|
||
external_data = self.prepare_external_data(instance, action)
|
||
|
||
try:
|
||
# Вызываем метод внешней системы
|
||
sync_method = getattr(
|
||
self.external_sync_service, sync_method_name)
|
||
result = sync_method(**external_data)
|
||
|
||
# Если внешняя система вернула данные для обновления БД
|
||
if isinstance(result, dict) and result.get('update_db'):
|
||
for field, value in result['update_db'].items():
|
||
setattr(instance, field, value)
|
||
instance.save(update_fields=list(
|
||
result['update_db'].keys()))
|
||
|
||
# Вызываем кастомный обработчик результата если определен
|
||
if hasattr(self, 'handle_external_sync_result'):
|
||
self.handle_external_sync_result(instance, result, action)
|
||
|
||
logger.info(
|
||
f"External sync success for {action} on {instance}")
|
||
|
||
except Exception as e:
|
||
# При ошибке внешней системы - откатываем транзакцию
|
||
logger.error(f"External sync failed, rolling back: {e}")
|
||
transaction.set_rollback(True)
|
||
|
||
# Вызываем обработчик ошибок
|
||
error_response = self.handle_external_sync_error(
|
||
e, instance, action)
|
||
if error_response:
|
||
raise ExternalSyncError(error_response)
|
||
raise
|
||
|
||
def _perform_with_sync_instance(self, instance, action, db_operation):
|
||
"""
|
||
Выполняет DB операцию на инстансе с синхронизацией.
|
||
|
||
Args:
|
||
instance: Инстанс модели
|
||
action: Тип операции
|
||
db_operation: Функция для выполнения DB операции
|
||
"""
|
||
if not self.external_sync_enabled:
|
||
return db_operation(instance)
|
||
|
||
sync_method_name = self.get_external_sync_method(action)
|
||
if not sync_method_name or not self.external_sync_service:
|
||
return db_operation(instance)
|
||
|
||
# Сохраняем данные перед удалением
|
||
external_data = self.prepare_external_data(instance, action)
|
||
|
||
# Атомарная транзакция: удаляем из БД
|
||
with transaction.atomic():
|
||
db_operation(instance)
|
||
|
||
try:
|
||
# Вызываем метод внешней системы
|
||
sync_method = getattr(
|
||
self.external_sync_service, sync_method_name)
|
||
sync_method(**external_data)
|
||
|
||
logger.info(
|
||
f"External sync success for {action} on {instance}")
|
||
|
||
except Exception as e:
|
||
# При ошибке внешней системы - откатываем транзакцию
|
||
logger.error(f"External sync failed, rolling back: {e}")
|
||
transaction.set_rollback(True)
|
||
|
||
error_response = self.handle_external_sync_error(
|
||
e, instance, action)
|
||
if error_response:
|
||
raise ExternalSyncError(error_response)
|
||
raise
|
||
|
||
|
||
class ExternalSyncError(Exception):
|
||
"""Исключение с Response для возврата клиенту."""
|
||
|
||
def __init__(self, response):
|
||
self.response = response
|
||
super().__init__()
|
||
|
||
|
||
def with_external_sync(external_service_attr='external_sync_service', method_map=None):
|
||
"""
|
||
Декоратор для методов ViewSet, добавляющий синхронизацию с внешней системой.
|
||
|
||
Usage:
|
||
@with_external_sync(method_map={'create': 'create_user_ldap'})
|
||
def create(self, request, *args, **kwargs):
|
||
return super().create(request, *args, **kwargs)
|
||
|
||
Args:
|
||
external_service_attr: Имя атрибута ViewSet с сервисом
|
||
method_map: Маппинг action -> метод внешнего сервиса
|
||
"""
|
||
def decorator(func):
|
||
@wraps(func)
|
||
def wrapper(self, request, *args, **kwargs):
|
||
# Выполняем оригинальный метод
|
||
with transaction.atomic():
|
||
response = func(self, request, *args, **kwargs)
|
||
|
||
# Получаем сервис и метод для синхронизации
|
||
service = getattr(self, external_service_attr, None)
|
||
if not service or not method_map:
|
||
return response
|
||
|
||
action = self.action
|
||
sync_method_name = method_map.get(action)
|
||
if not sync_method_name:
|
||
return response
|
||
|
||
try:
|
||
# Получаем созданный/обновленный инстанс
|
||
instance = getattr(response, 'data', {}).get('id')
|
||
if isinstance(response.data, dict):
|
||
instance = self.get_queryset().get(
|
||
id=response.data['id'])
|
||
|
||
# Вызываем метод внешней системы
|
||
sync_method = getattr(service, sync_method_name)
|
||
sync_method(instance=instance)
|
||
|
||
except Exception as e:
|
||
transaction.set_rollback(True)
|
||
logger.error(f"External sync failed: {e}")
|
||
raise
|
||
|
||
return response
|
||
return wrapper
|
||
return decorator
|