fix: Обработка race condition при дублировании slug товаров
Реализована трёхуровневая защита от IntegrityError при создании товаров с одинаковым названием: 1. SlugService улучшен: - Добавлен метод _get_base_queryset() для работы с мягким удалением - Проверка всех объектов включая удалённые (all_objects) - Новый метод get_next_available_slug() для retry обработки - Максимум 100 попыток поиска уникального slug 2. BaseProductEntity.save() защищена: - transaction.atomic() для атомарности операции - Retry логика с 5 попытками при IntegrityError - При конфликте добавляется суффикс (-1, -2, -3...) - Fallback на timestamp если суффиксы исчерпаны 3. Views обрабатывают IntegrityError: - ProductCreateView.form_valid() перехватывает ошибку - ProductUpdateView.form_valid() перехватывает ошибку - Пользователю показывается дружелюбное сообщение об ошибке - Нет 500 ошибок - вместо этого form_invalid() с сообщением Эффект: - До: User создаёт товар "Роза красная" 2 раза → IntegrityError → 500 ошибка - После: User создаёт товар "Роза красная" 2 раза → slug автоматически становится "roza-krasnaya-1" Протестировано на Django shell и синтаксис проверен. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -165,14 +165,55 @@ class BaseProductEntity(models.Model):
|
||||
super().delete()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Автогенерация slug из name если не задан"""
|
||||
if not self.slug or self.slug.strip() == '':
|
||||
# Используем централизованный сервис для генерации slug
|
||||
from ..services.slug_service import SlugService
|
||||
self.slug = SlugService.generate_unique_slug(
|
||||
self.name,
|
||||
self.__class__,
|
||||
self.pk
|
||||
)
|
||||
"""
|
||||
Автогенерация slug из name если не задан.
|
||||
Использует transaction.atomic() и retry логику для обработки race condition.
|
||||
"""
|
||||
from django.db import transaction, IntegrityError
|
||||
from ..services.slug_service import SlugService
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
# Генерируем базовый slug
|
||||
if not self.slug or self.slug.strip() == '':
|
||||
transliterated_name = __import__('unidecode', fromlist=['unidecode']).unidecode(self.name)
|
||||
base_slug = __import__('django.utils.text', fromlist=['slugify']).slugify(transliterated_name)
|
||||
else:
|
||||
base_slug = self.slug
|
||||
|
||||
# Пытаемся сохранить с retry при IntegrityError
|
||||
max_retries = 5
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# Попытка 1: используем обычный способ генерации
|
||||
if not self.slug or self.slug.strip() == '':
|
||||
if attempt == 0:
|
||||
# Первая попытка - используем обычный generate_unique_slug
|
||||
self.slug = SlugService.generate_unique_slug(
|
||||
self.name,
|
||||
self.__class__,
|
||||
self.pk
|
||||
)
|
||||
else:
|
||||
# Retry попытки - используем get_next_available_slug с суффиксом
|
||||
try:
|
||||
self.slug = SlugService.get_next_available_slug(
|
||||
base_slug,
|
||||
self.__class__,
|
||||
self.pk,
|
||||
start_counter=attempt
|
||||
)
|
||||
except ValueError:
|
||||
# Если все попытки с суффиксом исчерпаны, добавляем timestamp
|
||||
import time
|
||||
self.slug = f"{base_slug}-{int(time.time() % 10000)}"
|
||||
|
||||
# Основное сохранение
|
||||
super().save(*args, **kwargs)
|
||||
return # Успешно сохранили, выходим
|
||||
|
||||
except IntegrityError as e:
|
||||
# Если это последняя попытка, выбрасываем исключение
|
||||
if attempt == max_retries - 1:
|
||||
raise
|
||||
# Иначе пытаемся снова со следующим slug'ом
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user