Транзакции в Django

Транзакции обеспечивают целостность данных при выполнении нескольких операций с БД. Они гарантируют, что либо все операции выполнятся успешно, либо ни одна из них не будет применена.

Основы транзакций

Транзакция должна соответствовать принципам ACID:

  • Atomicity - атомарность (все или ничего)
  • Consistency - консистентность (данные остаются валидными)
  • Isolation - изоляция (транзакции не влияют друг на друга)
  • Durability - долговечность (изменения сохраняются)

Декоратор @transaction.atomic

 1from django.db import transaction
 2from django.contrib.auth.models import User
 3from .models import Profile, UserSettings
 4
 5@transaction.atomic
 6def create_user_with_profile(username, email, first_name, last_name):
 7    """Создает пользователя с профилем в одной транзакции"""
 8    try:
 9        # Создаем пользователя
10        user = User.objects.create_user(
11            username=username,
12            email=email,
13            first_name=first_name,
14            last_name=last_name
15        )
16
17        # Создаем профиль
18        profile = Profile.objects.create(
19            user=user,
20            bio=f"Профиль пользователя {username}"
21        )
22
23        # Создаем настройки по умолчанию
24        UserSettings.objects.create(
25            user=user,
26            notifications_enabled=True,
27            theme='light'
28        )
29
30        return user
31    except Exception as e:
32        # При любой ошибке все изменения откатятся автоматически
33        logger.error(f"Ошибка создания пользователя: {e}")
34        raise

Контекстный менеджер transaction.atomic()

 1from django.db import transaction
 2from .models import Order, OrderItem, Product
 3
 4def process_order(order_data, items_data):
 5    """Обрабатывает заказ с проверкой наличия товаров"""
 6    with transaction.atomic():
 7        # Создаем заказ
 8        order = Order.objects.create(
 9            customer=order_data['customer'],
10            total_amount=0,
11            status='pending'
12        )
13
14        total = 0
15        for item_data in items_data:
16            product = Product.objects.select_for_update().get(
17                id=item_data['product_id']
18            )
19
20            # Проверяем наличие
21            if product.stock < item_data['quantity']:
22                raise ValueError(f"Недостаточно товара {product.name}")
23
24            # Создаем позицию заказа
25            OrderItem.objects.create(
26                order=order,
27                product=product,
28                quantity=item_data['quantity'],
29                price=product.price
30            )
31
32            # Уменьшаем остаток
33            product.stock -= item_data['quantity']
34            product.save()
35
36            total += product.price * item_data['quantity']
37
38        # Обновляем общую сумму заказа
39        order.total_amount = total
40        order.save()
41
42        return order

Savepoints - точки сохранения

 1from django.db import transaction
 2from .models import Blog, Post, Comment
 3
 4def create_blog_with_content(blog_data, posts_data):
 5    """Создает блог с постами, используя savepoints"""
 6    with transaction.atomic():
 7        # Создаем блог
 8        blog = Blog.objects.create(
 9            title=blog_data['title'],
10            description=blog_data['description']
11        )
12
13        # Создаем savepoint после создания блога
14        sid = transaction.savepoint()
15
16        try:
17            # Создаем посты
18            for post_data in posts_data:
19                post = Post.objects.create(
20                    blog=blog,
21                    title=post_data['title'],
22                    content=post_data['content']
23                )
24
25                # Если есть комментарии, создаем их
26                if 'comments' in post_data:
27                    for comment_data in post_data['comments']:
28                        Comment.objects.create(
29                            post=post,
30                            author=comment_data['author'],
31                            text=comment_data['text']
32                        )
33
34            # Если все прошло успешно, коммитим транзакцию
35            transaction.savepoint_commit(sid)
36
37        except Exception as e:
38            # При ошибке откатываемся к savepoint
39            transaction.savepoint_rollback(sid)
40            # Блог остается созданным, но посты не создаются
41            logger.warning(f"Ошибка создания постов: {e}")
42            raise

Уровни изоляции транзакций

 1from django.db import transaction
 2from django.conf import settings
 3
 4# Настройка уровня изоляции в settings.py
 5# DATABASES = {
 6#     'default': {
 7#         'ENGINE': 'django.db.backends.postgresql',
 8#         'OPTIONS': {
 9#             'isolation_level': 'READ_COMMITTED',  # или 'SERIALIZABLE'
10#         }
11#     }
12# }
13
14@transaction.atomic
15def transfer_money(from_account_id, to_account_id, amount):
16    """Перевод денег между счетами с высоким уровнем изоляции"""
17    # Используем select_for_update для блокировки строк
18    from_account = Account.objects.select_for_update().get(id=from_account_id)
19    to_account = Account.objects.select_for_update().get(id=to_account_id)
20
21    if from_account.balance < amount:
22        raise ValueError("Недостаточно средств")
23
24    from_account.balance -= amount
25    from_account.save()
26
27    to_account.balance += amount
28    to_account.save()
29
30    # Создаем запись о транзакции
31    Transaction.objects.create(
32        from_account=from_account,
33        to_account=to_account,
34        amount=amount,
35        timestamp=timezone.now()
36    )

Обработка ошибок и rollback

 1from django.db import transaction, IntegrityError
 2from django.core.exceptions import ValidationError
 3from .models import User, UserProfile, UserPreferences
 4
 5def create_complete_user_profile(user_data, profile_data, preferences_data):
 6    """Создает полный профиль пользователя с обработкой ошибок"""
 7    try:
 8        with transaction.atomic():
 9            # Создаем пользователя
10            user = User.objects.create_user(
11                username=user_data['username'],
12                email=user_data['email'],
13                password=user_data['password']
14            )
15
16            # Создаем профиль
17            profile = UserProfile.objects.create(
18                user=user,
19                **profile_data
20            )
21
22            # Создаем настройки
23            preferences = UserPreferences.objects.create(
24                user=user,
25                **preferences_data
26            )
27
28            return user
29
30    except IntegrityError as e:
31        # Ошибка целостности БД (дублирование, нарушение constraints)
32        logger.error(f"Ошибка целостности: {e}")
33        raise ValidationError("Пользователь с таким именем или email уже существует")
34
35    except ValidationError as e:
36        # Ошибка валидации данных
37        logger.error(f"Ошибка валидации: {e}")
38        raise
39
40    except Exception as e:
41        # Любая другая ошибка
42        logger.error(f"Неожиданная ошибка: {e}")
43        raise

Транзакции в Django ORM

 1from django.db import transaction
 2from django.db.models import F, Sum
 3from .models import Product, Inventory, Purchase
 4
 5def process_purchase(product_id, quantity, customer_id):
 6    """Обрабатывает покупку с обновлением инвентаря"""
 7    with transaction.atomic():
 8        # Получаем продукт с блокировкой
 9        product = Product.objects.select_for_update().get(id=product_id)
10        inventory = Inventory.objects.select_for_update().get(product=product)
11
12        # Проверяем наличие
13        if inventory.quantity < quantity:
14            raise ValueError("Недостаточно товара на складе")
15
16        # Создаем покупку
17        purchase = Purchase.objects.create(
18            product=product,
19            customer_id=customer_id,
20            quantity=quantity,
21            total_price=product.price * quantity
22        )
23
24        # Обновляем инвентарь
25        inventory.quantity = F('quantity') - quantity
26        inventory.save()
27
28        # Обновляем статистику продаж
29        product.total_sales = F('total_sales') + quantity
30        product.save()
31
32        return purchase
33
34def bulk_update_with_transaction(updates_data):
35    """Массовое обновление в транзакции"""
36    with transaction.atomic():
37        for update_data in updates_data:
38            Product.objects.filter(
39                id=update_data['id']
40            ).update(
41                price=update_data['new_price'],
42                updated_at=timezone.now()
43            )

Транзакции в Django REST Framework

 1from rest_framework import status
 2from rest_framework.decorators import api_view
 3from rest_framework.response import Response
 4from django.db import transaction
 5from .serializers import OrderSerializer, OrderItemSerializer
 6
 7@api_view(['POST'])
 8def create_order(request):
 9    """API endpoint для создания заказа"""
10    try:
11        with transaction.atomic():
12            # Валидируем данные заказа
13            order_serializer = OrderSerializer(data=request.data)
14            if not order_serializer.is_valid():
15                return Response(
16                    order_serializer.errors,
17                    status=status.HTTP_400_BAD_REQUEST
18                )
19
20            # Создаем заказ
21            order = order_serializer.save()
22
23            # Валидируем и создаем позиции заказа
24            items_data = request.data.get('items', [])
25            for item_data in items_data:
26                item_serializer = OrderItemSerializer(data=item_data)
27                if not item_serializer.is_valid():
28                    raise ValidationError(item_serializer.errors)
29
30                item_serializer.save(order=order)
31
32            # Возвращаем созданный заказ
33            return Response(
34                OrderSerializer(order).data,
35                status=status.HTTP_201_CREATED
36            )
37
38    except ValidationError as e:
39        return Response(
40            {'error': str(e)},
41            status=status.HTTP_400_BAD_REQUEST
42        )
43    except Exception as e:
44        return Response(
45            {'error': 'Внутренняя ошибка сервера'},
46            status=status.HTTP_500_INTERNAL_SERVER_ERROR
47        )

Тестирование транзакций

 1import pytest
 2from django.test import TestCase
 3from django.db import transaction
 4from .models import Account, Transaction
 5
 6class TransactionTestCase(TestCase):
 7    def setUp(self):
 8        self.account1 = Account.objects.create(
 9            name="Счет 1",
10            balance=1000
11        )
12        self.account2 = Account.objects.create(
13            name="Счет 2",
14            balance=500
15        )
16
17    def test_money_transfer_success(self):
18        """Тест успешного перевода денег"""
19        initial_balance1 = self.account1.balance
20        initial_balance2 = self.account2.balance
21        transfer_amount = 200
22
23        with transaction.atomic():
24            self.account1.balance -= transfer_amount
25            self.account1.save()
26
27            self.account2.balance += transfer_amount
28            self.account2.save()
29
30            Transaction.objects.create(
31                from_account=self.account1,
32                to_account=self.account2,
33                amount=transfer_amount
34            )
35
36        # Обновляем объекты из БД
37        self.account1.refresh_from_db()
38        self.account2.refresh_from_db()
39
40        self.assertEqual(
41            self.account1.balance,
42            initial_balance1 - transfer_amount
43        )
44        self.assertEqual(
45            self.account2.balance,
46            initial_balance2 + transfer_amount
47        )
48
49    def test_money_transfer_rollback(self):
50        """Тест отката транзакции при ошибке"""
51        initial_balance1 = self.account1.balance
52        initial_balance2 = self.account2.balance
53
54        try:
55            with transaction.atomic():
56                self.account1.balance -= 200
57                self.account1.save()
58
59                # Имитируем ошибку
60                raise ValueError("Ошибка в процессе перевода")
61
62                self.account2.balance += 200
63                self.account2.save()
64        except ValueError:
65            pass
66
67        # Проверяем, что изменения откатились
68        self.account1.refresh_from_db()
69        self.account2.refresh_from_db()
70
71        self.assertEqual(self.account1.balance, initial_balance1)
72        self.assertEqual(self.account2.balance, initial_balance2)

Лучшие практики

  • Используй транзакции для логически связанных операций - не оборачивай в транзакцию весь запрос
  • Минимизируй время выполнения транзакции - длинные транзакции блокируют ресурсы
  • Используй select_for_update() для критических операций - предотвращает race conditions
  • Обрабатывай исключения правильно - не глотай ошибки в транзакциях
  • Используй savepoints для сложной логики - позволяет частичный откат
  • Тестируй rollback сценарии - убедись, что откат работает корректно
  • Мониторь производительность - длинные транзакции могут вызывать deadlocks

FAQ

Q: Когда использовать транзакции?
A: Когда нужно выполнить несколько операций как единое целое или откатить изменения при ошибке. Например: создание пользователя с профилем, перевод денег между счетами, обработка заказа.

Q: В чем разница между @transaction.atomic и transaction.atomic()?
A: Декоратор применяется ко всей функции, контекстный менеджер - только к блоку кода внутри него. Контекстный менеджер более гибкий.

Q: Как обрабатывать ошибки в транзакциях?
A: Используй try-except блоки, но помни, что при любом исключении транзакция автоматически откатится. Логируй ошибки для отладки.

Q: Что такое savepoints?
A: Savepoints позволяют создать промежуточные точки в транзакции, к которым можно откатиться при ошибке, не отменяя всю транзакцию.

Q: Как избежать deadlocks?
A: Всегда получай блокировки в одинаковом порядке, минимизируй время удержания блокировок, используй короткие транзакции.

Q: Можно ли использовать транзакции в Django ORM?
A: Да, Django ORM полностью поддерживает транзакции. Все операции с моделями внутри transaction.atomic() выполняются в одной транзакции.

Q: Как тестировать транзакции?
A: Создавай тесты, которые проверяют как успешное выполнение, так и откат при ошибках. Используй setUp и tearDown для подготовки данных.