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

Как работает transactional read-only?

2.3 Middle🔥 161 комментариев
#ORM и Hibernate#Spring Framework

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

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

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

# Как работает @Transactional(readOnly = true)

Аннотация @Transactional(readOnly = true) — это оптимизация для методов, которые только читают данные без модификации. Это дает различные преимущества на уровне БД и приложения.

1. Основная концепция

import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    // Обычная транзакция
    @Transactional
    public User createUser(String name) {
        User user = new User(name);
        userRepository.save(user);  // INSERT в БД
        return user;
    }
    
    // Read-only транзакция
    @Transactional(readOnly = true)
    public User findUser(Long id) {
        return userRepository.findById(id).orElse(null);  // SELECT из БД
    }
}

2. Что происходит с readOnly = true

2.1 На уровне БД

// При readOnly = true Spring/Hibernate:
// - Устанавливает Connection в READ_ONLY режим
// - Начинает READ_ONLY транзакцию на БД
// - Оптимизирует блокировки (нет ненужных блокировок)

@Transactional(readOnly = true)
public List<User> getAllUsers() {
    // Эквивалент SQL:
    // SET TRANSACTION READ ONLY;  -- на некоторых БД
    return userRepository.findAll();
}

2.2 На уровне Hibernate

// Hibernate отключает некоторые оптимизации для write операций
@Transactional(readOnly = true)
public User getUser(Long id) {
    // Hibernate:
    // - Не отслеживает изменения сущности
    // - Не создает снимки объектов для dirty checking
    // - Использует другой режим загрузки (READ режим)
    // - Может использовать различные стратегии кэширования
    
    return userRepository.findById(id).orElse(null);
}

3. Различие: readOnly = true vs readOnly = false

@Service
public class UserService {
    
    @Autowired
    private UserRepository repository;
    
    // readOnly = false (по умолчанию)
    @Transactional
    public User updateUser(Long id, String newName) {
        User user = repository.findById(id).orElse(null);
        if (user != null) {
            user.setName(newName);
            // UPDATE automatically
        }
        return user;  // Hibernate update
    }
    
    // readOnly = true
    @Transactional(readOnly = true)
    public User getUser(Long id) {
        User user = repository.findById(id).orElse(null);
        if (user != null) {
            user.setName("New Name");  // Изменение НЕ сохранится
        }
        return user;  // Никакого UPDATE
    }
}

// Тест
public void demonstrateDifference() {
    User user = getUser(1L);  // readOnly = true
    System.out.println(user.getName());  // "Original Name"
    // изменение было потеряно!
    
    user = updateUser(1L, "New Name");  // readOnly = false
    System.out.println(user.getName());  // "New Name"
    // изменение было сохранено
}

4. Производительность: readOnly = true

// ПЛОХО: readOnly = false для чтения
@Transactional(readOnly = false)
public List<User> getAllUsers() {
    // Spring создает объекты для отслеживания изменений
    // Hibernate создает снимки всех объектов
    // Больше памяти и CPU
    // Время: 500ms для 10000 записей
    return userRepository.findAll();
}

// ХОРОШО: readOnly = true для чтения
@Transactional(readOnly = true)
public List<User> getAllUsers() {
    // Spring оптимизирует под читаемые операции
    // Hibernate не отслеживает изменения
    // Меньше памяти и CPU
    // Время: 50ms для 10000 записей (в 10 раз быстрее!)
    return userRepository.findAll();
}

5. Примеры использования

5.1 DAO/Repository слой

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    // Явно помечаем read-only методы
    @Transactional(readOnly = true)
    @Query("SELECT u FROM User u WHERE u.email = :email")
    Optional<User> findByEmail(@Param("email") String email);
    
    @Transactional(readOnly = true)
    @Query("SELECT u FROM User u WHERE u.role = :role")
    List<User> findByRole(@Param("role") String role);
}

5.2 Service слой

@Service
public class ReportService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private OrderRepository orderRepository;
    
    // Генерация отчёта (только чтение)
    @Transactional(readOnly = true)
    public ReportDTO generateMonthlyReport(int month) {
        List<User> users = userRepository.findAll();
        List<Order> orders = orderRepository.findByMonth(month);
        
        return new ReportDTO(
            users.size(),
            orders.size(),
            orders.stream()
                .mapToDouble(Order::getAmount)
                .sum()
        );
    }
    
    // Поиск с фильтрацией
    @Transactional(readOnly = true)
    public List<UserDTO> searchUsers(String query) {
        return userRepository.findAll().stream()
            .filter(u -> u.getName().contains(query))
            .map(u -> new UserDTO(u.getId(), u.getName(), u.getEmail()))
            .collect(Collectors.toList());
    }
}

6. Транзакции с разными изоляциями

import org.springframework.transaction.annotation.Isolation;

@Service
public class UserService {
    
    // Чтение с наименьшей изоляцией (быстро, но небезопасно)
    @Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED)
    public User getUserFast(Long id) {
        // Может прочитать грязные данные (dirty read)
        return userRepository.findById(id).orElse(null);
    }
    
    // Чтение с средней изоляцией
    @Transactional(readOnly = true, isolation = Isolation.READ_COMMITTED)
    public User getUser(Long id) {
        // Стандартный уровень
        return userRepository.findById(id).orElse(null);
    }
    
    // Чтение с высокой изоляцией (безопасно, но медленно)
    @Transactional(readOnly = true, isolation = Isolation.SERIALIZABLE)
    public User getUserSafe(Long id) {
        // Максимальная консистентность
        return userRepository.findById(id).orElse(null);
    }
}

7. Timeout для read-only транзакций

@Service
public class LongRunningReportService {
    
    @Autowired
    private ReportRepository reportRepository;
    
    // Долгая операция чтения
    @Transactional(readOnly = true, timeout = 300)  // 5 минут
    public ReportDTO generateComplexReport() {
        // Если операция занимает > 5 минут, будет TimeoutException
        List<Report> data = reportRepository.findAllWithDetails();
        return buildReport(data);
    }
}

8. Отключение изменения отслеживания

import org.hibernate.Session;
import javax.persistence.EntityManager;

@Service
public class PerformanceOptimizedService {
    
    @Autowired
    private EntityManager entityManager;
    
    @Transactional(readOnly = true)
    public List<User> getUsers() {
        Session session = entityManager.unwrap(Session.class);
        
        // Явно отключаем отслеживание изменений
        session.setDefaultReadOnly(true);
        
        List<User> users = session.createQuery("FROM User", User.class)
            .setReadOnly(true)  // еще более агрессивная оптимизация
            .getResultList();
        
        return users;
    }
}

9. Распространённые ошибки

Ошибка 1: Забыли поставить readOnly = true

// ПЛОХО: читаем данные, но потребляем ресурсы как для write
@Transactional  // readOnly = false по умолчанию
public List<User> getAllUsers() {
    return userRepository.findAll();
}

// ХОРОШО
@Transactional(readOnly = true)
public List<User> getAllUsers() {
    return userRepository.findAll();
}

Ошибка 2: Попытка модифицировать в readOnly транзакции

@Transactional(readOnly = true)
public void updateUserInReadOnly(Long id) {
    User user = userRepository.findById(id).orElse(null);
    if (user != null) {
        user.setName("New Name");
        // TransactionRequiredException: No EntityManager with actual transaction
        userRepository.save(user);  // ОШИБКА!
    }
}

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

@Service
public class BestPracticesService {
    
    // ✓ 1. Помечайте read-only методы явно
    @Transactional(readOnly = true)
    public User getUser(Long id) {
        return repository.findById(id).orElse(null);
    }
    
    // ✓ 2. Используйте readOnly на уровне Repository
    // interface UserRepository extends JpaRepository<User, Long> {
    //     @Transactional(readOnly = true)
    //     List<User> findAll();
    // }
    
    // ✓ 3. Используйте правильный isolation level
    @Transactional(readOnly = true, isolation = Isolation.READ_COMMITTED)
    public List<User> getActiveUsers() {
        return repository.findByActiveTrue();
    }
    
    // ✓ 4. Не смешивайте чтение и запись в одном методе
    // ПЛОХО:
    // @Transactional(readOnly = true)
    // public void processAndSave() {
    //     List<Data> data = read();
    //     save(data);  // Ошибка!
    // }
    
    // ХОРОШО: разделите на два метода
    @Transactional(readOnly = true)
    public List<Data> readData() {
        return repository.findAll();
    }
    
    @Transactional(readOnly = false)
    public void saveData(List<Data> data) {
        repository.saveAll(data);
    }
    
    // ✓ 5. Используйте @Transactional для batch операций
    @Transactional(readOnly = true, batchSize = 50)
    public void processBatch() {
        // обработка большого объема данных
    }
}

Резюме

readOnly = true:

  • Оптимизирует операции чтения
  • Отключает отслеживание изменений в Hibernate
  • Устанавливает Connection в READ-ONLY на некоторых БД
  • Быстрее и требует меньше памяти
  • Предотвращает случайные модификации

Когда использовать:

  • Запросы SELECT
  • Отчёты и аналитика
  • Поиск и фильтрация
  • Любые операции только для чтения

Производительность: read-only может быть в 5-10 раз быстрее для больших наборов данных благодаря отключению overhead отслеживания изменений.

Как работает transactional read-only? | PrepBro