"Грязная проверка" - это механизм, используемый Hibernate для определения того, изменилось ли какое-либо значение сущности с момента ее извлечения из базы данных. Это помогает Hibernate оптимизировать запросы к базе данных таким образом, чтобы обновлялись только те поля, которые изменились.
2. Модификация сущности: После получения сущности пользователь может вносить в нее изменения.
3. Синхронизация состояния: Перед любой операцией (например, перед фиксацией транзакции или явным вызовом `flush()`) Hibernate выполняет процесс "грязной проверки", сравнивая текущее состояние сущности с ее начальным состоянием, хранящимся в кэше.
4. Обновление базы данных: если Hibernate обнаруживает какие-либо изменения, он генерирует и выполняет соответствующий SQL-запрос на обновление, чтобы обновить только те поля, которые изменились.
- Оптимизация: В базе данных обновляются только измененные поля, что позволяет повысить производительность, поскольку между приложением и базой данных передается меньше данных.
- Автоматизация: Разработчикам не нужно явно указывать, какие поля изменились. Hibernate делает это автоматически.
- Накладные расходы: Механизм "грязной проверки" может создавать дополнительные накладные расходы, особенно при работе с большим количеством сущностей. В большинстве случаев эти затраты незначительны, но в некоторых сценариях они могут стать проблематичными.
- Прозрачность: Некоторым разработчикам этот механизм может показаться не совсем прозрачным, поскольку Hibernate автоматически определяет, какие поля необходимо обновить.
Для управления процессом "грязной проверки" и оптимизации его работы разработчики могут использовать различные стратегии и аннотации, предоставляемые Hibernate.
В Hibernate механизм "грязной проверки" отслеживает изменения в сущностях, которые находятся в "постоянном" состоянии (т.е. связаны с сессией). Подход Hibernate к управлению состояниями сущностей основан на концепции состояний жизненного цикла объекта: переходное, постоянное и отсоединенное.
О возможных состояниях сущностей в JPA мы рассказывали в предыдущей статье, поэтому здесь мы не будем углубляться в эту тему 🔁.
Отметим, что состояние Persistent: является единственным, в котором работает "грязная проверка". Точнее, как только объект становится связанным с сессией (например, после сохранения, выборки или прикрепления), он переходит в "постоянное" состояние. Все изменения в этих объектах будут автоматически отслеживаться и синхронизироваться с базой данных во время "грязной проверки".
1. После получения сущности: Когда вы получаете сущность с помощью таких методов, как `session.get()`, `session.load()` или HQL-запроса, полученная сущность автоматически становится "постоянной".
User user = session.get(User.class, userId); user.setEmail("[email protected]"); user.setName("UpdatedName"); // "dirty checking" will be applied to this entity.
Стоит отметить, что когда Hibernate выполняет "грязную проверку" (обычно перед закрытием сессии или при явном вызове сессии.flush()), он определяет, что оба эти поля (имя и email) были изменены. Однако Hibernate оптимизирует запросы и выполняет один SQL-запрос UPDATE для обновления обоих этих полей в базе данных, а не два отдельных запроса. Таким образом, будет выполнен только один запрос UPDATE, который обновит оба поля (имя и email) в одной транзакции.
2. После сохранения новой сущности: При сохранении новой сущности с помощью session.save() эта сущность становится "постоянной".
User newUser = new User(); session.save(newUser); newUser.setName("NewName"); // "dirty checking" will be applied to this entity.
3. При переходе из состояния "detached" в состояние "persistent": Если сущность находится в отсоединенном состоянии и вы присоединяете ее обратно к сессии (например, с помощью session.merge() или session.update()), то эта сущность снова становится "постоянной".
session.merge(detachedUser); detachedUser.setName("AnotherName"); // "dirty checking" will be applied to this entity if it became "persistent".
Для того чтобы механизм "грязной проверки" работал, сессия должна оставаться открытой. Если сессия закрыта, изменения не будут отслеживаться до тех пор, пока сущность снова не станет "постоянной" в новой или той же сессии.
Теперь, когда мы поняли, как это работает на уровне Hibernate, давайте рассмотрим примеры с SpringData.
Приводим полный код сервиса, демонстрирующего различные комбинации работы с Spring Data JPA и транзакционностью.
@Service public class UserService { @Autowired private UserRepository userRepository; // 1. Regular update within a transaction (without explicit saving) @Transactional public void updateName(Long userId, String newName) { User user = userRepository.findById(userId).orElseThrow(() -> new EntityNotFoundException("User not found")); user.setName(newName); // Thanks to dirty checking, changes will be saved automatically upon transaction completion. } // 2. Data retrieval without a transaction (changes won't be saved automatically) public void nonTransactionalUpdateName(Long userId, String newName) { User user = userRepository.findById(userId).orElseThrow(() -> new EntityNotFoundException("User not found")); user.setName(newName); // Changes will not be saved as the method is not within a transaction. } // 3. Explicitly saving changes within a transaction @Transactional public void explicitSaveAfterUpdate(Long userId, String newName) { User user = userRepository.findById(userId).orElseThrow(() -> new EntityNotFoundException("User not found")); user.setName(newName); userRepository.save(user); // Explicitly saving changes, although it's not required in this context. } // 4. Creating a new entity and saving it @Transactional public void createUser(String name, String email) { User user = new User(); user.setName(name); user.setEmail(email); userRepository.save(user); // Save is necessary here as the entity is new. } // 5. Retrieving data in read-only mode @Transactional(readOnly = true) public List<User> getAllUsers() { return userRepository.findAll(); // As the method is wrapped in @Transactional with readOnly=true, any attempts to change entities within this method will not result in their saving to the DB. } // 6. Explicitly detaching an entity from the persistence context and then saving it @Transactional public void detachAndUpdate(Long userId, String newName) { User user = userRepository.findById(userId).orElseThrow(() -> new EntityNotFoundException("User not found")); userRepository.detach(user); // Detaching the entity from the persistence context. user.setName(newName); userRepository.save(user); // Now we need to explicitly save changes as the entity is detached. } }
Данный сервис демонстрирует различные сценарии взаимодействия с базой данных в контексте Spring Data JPA и транзакций. Надеюсь, это поможет вам лучше понять работу с данной технологией.
Давайте еще раз рассмотрим пример 3.
@Transactional public void explicitSaveAfterUpdate(Long userId, String newName) { User user = userRepository.findById(userId).orElseThrow(() -> new EntityNotFoundException("User not found")); user.setName(newName); userRepository.save(user); // Explicit saving of changes, although it is not required in this context }
В данном примере явное сохранение не требуется, поскольку "грязная проверка" Hibernate учитывает изменения в управляемых сущностях и автоматически синхронизирует их с базой данных в конце транзакции.
Если вы все же вызовете userRepository.save(user); для управляемой сущности, то в большинстве случаев это не приведет к каким-либо прямым негативным последствиям, но есть несколько моментов, на которые следует обратить внимание:
Производительность: Вызов сохранения потенциально может вызвать дополнительные операции, такие как объединение сущностей, что может оказаться менее эффективным, чем простое ожидание автоматического сохранения изменений в конце транзакции.
Удобство чтения кода: Явный вызов сохранения для сущностей, которые уже находятся в контексте управления Hibernate, может запутать разработчиков, не знакомых с контекстом кода. Они могут задаться вопросом, почему здесь происходит явное сохранение.
Поведение при сохранении: На практике сохранение в Spring Data JPA работает как persist или merge в зависимости от состояния сущности. Если сущность новая, то она будет сохранена как новая запись, если сущность уже существует (например, была извлечена из базы данных), то она будет объединена. В большинстве сценариев это не вызовет проблем, но знание этого поведения необходимо для понимания некоторых более сложных случаев.
Поэтому, хотя явное сохранение управляемых сущностей не является ошибкой, его лучше избегать, если нет особой необходимости упрощать код и повышать его производительность.
Обертывание метода в @Transactional - это самый простой способ обеспечить работу механизма "проверки на загрязненность" Hibernate. Когда метод обернут в @Transactional, все сущности, полученные или сохраненные в этом методе, автоматически становятся частью сессии Hibernate (или JPA EntityManager). Эти сущности находятся в "управляемом" или "постоянном" состоянии.
Любые изменения, внесенные в управляемые сущности в рамках транзакции, будут автоматически отслежены и синхронизированы с базой данных по окончании транзакции благодаря механизму "грязной проверки".