← Назад к вопросам

Может ли одна транзакция в базе данных включать несколько SQL-операций?

1.3 Junior🔥 271 комментариев
#Базы данных и SQL

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Транзакции: несколько SQL-операций

Краткий ответ: Да, одна транзакция может (и часто должна) содержать несколько SQL-операций. Это один из ключевых механизмов обеспечения целостности данных.

Основной концепт

Транзакция — это логическая единица работы, состоящая из одной или нескольких SQL-операций. Все операции в транзакции выполняются атомарно (либо все, либо ничего) благодаря принципам ACID:

@Service
public class BankTransferService {
    @Autowired
    private AccountRepository accountRepository;
    
    @Transactional  // Вся работа в одной транзакции
    public void transferFunds(Long fromAccountId, Long toAccountId, BigDecimal amount) {
        // Операция 1: дебет со счета
        Account sender = accountRepository.findByIdForUpdate(fromAccountId);
        sender.setBalance(sender.getBalance().subtract(amount));
        accountRepository.save(sender);  // SQL UPDATE #1
        
        // Операция 2: кредит на счет
        Account receiver = accountRepository.findByIdForUpdate(toAccountId);
        receiver.setBalance(receiver.getBalance().add(amount));
        accountRepository.save(receiver);  // SQL UPDATE #2
        
        // Если всё прошло успешно → COMMIT
        // Если исключение → ROLLBACK (оба UPDATE отменяются)
    }
}

Структура транзакции

-- BEGIN TRANSACTION
BEGIN;  -- или START TRANSACTION

-- SQL операция 1
UPDATE accounts SET balance = balance - 100 WHERE id = 1;

-- SQL операция 2
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

-- SQL операция 3
INSERT INTO transaction_history (from_id, to_id, amount) VALUES (1, 2, 100);

-- Если всё OK:
COMMIT;  -- Все изменения сохраняются

-- Если ошибка:
ROLLBACK;  -- Все изменения отменяются

Пример: Комплексная операция

@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private InventoryRepository inventoryRepository;
    @Autowired
    private PaymentRepository paymentRepository;
    
    @Transactional  // Все SQL операции в одной транзакции
    public Order createOrder(CreateOrderRequest request) {
        // Операция 1: создание заказа
        Order order = new Order();
        order.setCustomerId(request.getCustomerId());
        order.setTotalAmount(request.getTotalAmount());
        order.setStatus(OrderStatus.CREATED);
        orderRepository.save(order);  // INSERT
        
        // Операция 2: резервирование товара
        Inventory inventory = inventoryRepository.findByProductIdForUpdate(request.getProductId());
        if (inventory.getQuantity() < request.getQuantity()) {
            throw new InsufficientInventoryException();
        }
        inventory.setQuantity(inventory.getQuantity() - request.getQuantity());
        inventoryRepository.save(inventory);  // UPDATE
        
        // Операция 3: создание платежа
        Payment payment = new Payment();
        payment.setOrderId(order.getId());
        payment.setAmount(request.getTotalAmount());
        payment.setStatus(PaymentStatus.PENDING);
        paymentRepository.save(payment);  // INSERT
        
        // Если всё прошло → COMMIT (все 3 SQL выполнены)
        // Если исключение → ROLLBACK (все отменяются)
        return order;
    }
}

Управление транзакциями в Java

1. Spring @Transactional

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private AuditRepository auditRepository;
    
    @Transactional
    public User updateUser(Long userId, UpdateUserRequest request) {
        // SQL операция 1
        User user = userRepository.findById(userId).orElseThrow();
        user.setName(request.getName());
        user.setEmail(request.getEmail());
        userRepository.save(user);  // UPDATE
        
        // SQL операция 2
        AuditLog log = new AuditLog();
        log.setUserId(userId);
        log.setAction("UPDATE_PROFILE");
        log.setChangedFields(String.join(",", getChangedFields(user, request)));
        auditRepository.save(log);  // INSERT
        
        return user;
        // COMMIT если успех, ROLLBACK если ошибка
    }
}

2. Programmatic Transaction Management

@Service
public class ManualTransactionService {
    @Autowired
    private TransactionTemplate transactionTemplate;
    
    public void complexOperation() {
        transactionTemplate.executeWithoutResult(status -> {
            // SQL операция 1
            updateAccount(1, -100);
            
            // SQL операция 2
            updateAccount(2, +100);
            
            // SQL операция 3
            logTransaction(1, 2, 100);
            
            // Автоматический COMMIT
            // Если исключение → автоматический ROLLBACK
        });
    }
}

3. Raw SQL с явным управлением

public void manualTransactionControl(DataSource dataSource) {
    Connection connection = null;
    try {
        connection = dataSource.getConnection();
        
        // Начало транзакции
        connection.setAutoCommit(false);
        
        // SQL операция 1
        String sql1 = "UPDATE accounts SET balance = balance - ? WHERE id = ?";
        PreparedStatement stmt1 = connection.prepareStatement(sql1);
        stmt1.setBigDecimal(1, new BigDecimal("100"));
        stmt1.setLong(2, 1);
        stmt1.executeUpdate();
        
        // SQL операция 2
        String sql2 = "UPDATE accounts SET balance = balance + ? WHERE id = ?";
        PreparedStatement stmt2 = connection.prepareStatement(sql2);
        stmt2.setBigDecimal(1, new BigDecimal("100"));
        stmt2.setLong(2, 2);
        stmt2.executeUpdate();
        
        // Коммит если успех
        connection.commit();
        
    } catch (SQLException e) {
        if (connection != null) {
            try {
                // Откат если ошибка
                connection.rollback();
            } catch (SQLException rollbackException) {
                e.addSuppressed(rollbackException);
            }
        }
        throw new RuntimeException(e);
    } finally {
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                // ...
            }
        }
    }
}

Isolation Levels (уровни изоляции)

@Service
public class IsolationExampleService {
    @Autowired
    private OrderRepository orderRepository;
    
    // READ_UNCOMMITTED: грязное чтение
    @Transactional(isolation = Isolation.READ_UNCOMMITTED)
    public Order readDirtyData() {
        return orderRepository.findById(1).orElseThrow();
    }
    
    // READ_COMMITTED: по умолчанию
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public Order readCommittedData() {
        return orderRepository.findById(1).orElseThrow();
    }
    
    // REPEATABLE_READ: повторяемое чтение
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public Order readRepeatableData() {
        Order order = orderRepository.findById(1).orElseThrow();
        // Если другой поток изменит order, мы это не увидим
        Order order2 = orderRepository.findById(1).orElseThrow();
        return order;  // Гарантировано: order equals order2
    }
    
    // SERIALIZABLE: полная изоляция (может быть медленнее)
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public Order serializableRead() {
        return orderRepository.findById(1).orElseThrow();
    }
}

Проблемы при несоблюдении транзакций

Проблема 1: Грязное состояние

// ❌ Плохо: без транзакции
public void transferFunds(Long from, Long to, BigDecimal amount) {
    // Операция 1: дебет
    debit(from, amount);  // SQL UPDATE #1
    
    // ЕСЛИ ЗДЕСЬ ОШИБКА:
    int x = 1 / 0;  // Exception!
    
    // Операция 2 НИКОГДА НЕ ВЫПОЛНИТСЯ
    credit(to, amount);  // SQL UPDATE #2 — НЕ ВЫПОЛНЕН
    
    // Результат: деньги исчезли! 🚨
}

// ✓ Хорошо: с транзакцией
@Transactional
public void transferFunds(Long from, Long to, BigDecimal amount) {
    debit(from, amount);
    
    int x = 1 / 0;  // Exception!
    
    credit(to, amount);
    
    // Транзакция: ROLLBACK, оба изменения отменяются ✓
}

Проблема 2: Lost Update

// ❌ Без транзакции: гонка данных
Thread 1: читает balance = 100
Thread 2: читает balance = 100
Thread 1: пишет balance = 90 (вычел 10)
Thread 2: пишет balance = 80 (вычел 20)
// Результат: потеря 10 (должно быть 70)

// ✓ С SELECT FOR UPDATE:
@Transactional
public void updateBalance(Long accountId, BigDecimal delta) {
    Account account = accountRepository.findByIdForUpdate(accountId);
    // Блокируем строку, другие потоки ждут
    account.setBalance(account.getBalance().add(delta));
    accountRepository.save(account);
}

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

@Service
public class BestPractices {
    @Autowired
    private OrderRepository orderRepository;
    
    // ✓ Правильно: минимальная область транзакции
    @Transactional
    public Order createOrder(CreateOrderRequest request) {
        // Только БД операции
        Order order = new Order();
        order.setCustomerId(request.getCustomerId());
        return orderRepository.save(order);
    }
    
    // ✗ Неправильно: слишком большая область
    @Transactional
    public void createOrderAndSendEmail(CreateOrderRequest request) {
        // БД операции
        Order order = new Order();
        orderRepository.save(order);
        
        // Долгая операция (отправка email)
        emailService.sendConfirmation(order);  // Плохо: держит транзакцию открытой
    }
    
    // ✓ Правильно: разделение ответственности
    @Transactional
    public Order createOrder(CreateOrderRequest request) {
        Order order = new Order();
        return orderRepository.save(order);
    }
    
    public void handleOrderCreated(Order order) {
        // Вне транзакции, асинхронно
        emailService.sendConfirmation(order);
    }
}

Вывод: Да, одна транзакция может (и должна) содержать несколько SQL-операций для обеспечения целостности данных.