Транзакции в 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 для подготовки данных.