We have a problem🤔
Imagine you're working with a default project using Spring Data JPA. Here's the entity in question:
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class Person {
private static final String GENERATOR = "person_generator";
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = GENERATOR)
@SequenceGenerator(name = GENERATOR, sequenceName = "person_sequence")
private Long id;
private String name;
@Column(unique = true)
private String email;
}
What's the output of the following code?
@Transactional
public void someImportantOperation(Long personId) {
var oldPerson = personRepository.findById(personId)
.orElseThrow();
var oldPersonEmail = oldPerson.getEmail();
personRepository.delete(oldPerson);
var newPerson = new Person(
null, "New Person With The Same Email", oldPersonEmail
);
personRepository.save(newPerson);
}
1... 2... 3...
Exception!
ERROR: duplicate key value violates unique constraint "person_email_uniq"
Detail: Key (email)=(mail@gmail.com) already exists.
Q:
But why? I've just deleted the oldPerson
!
A:
It's because Hibernate executes SQL in a specific order!😎
Explanation
Try to delve into Hibernate's code: in methods like Session.persist
, Session.remove
, and others. For example in org.hibernate.internal.SessionImpl#delete
you will find:
@Override
public void delete(Object object) throws HibernateException {
checkOpen();
fireDelete( new DeleteEvent( object, this ) );
}
No actual SQL is constructed or executed! Hibernate simply triggers a DeleteEvent
!
The same happens with ALL other operations (except READ
operations)
These events need processing, right?
The logic for handling those events is related to the class org.hibernate.engine.spi.ActionQueue
.
There are two facts about it:
- These events are handled ONLY on Transaction Commit or Flush operations.
- These events are not handled in the order they were added to the
ActionQueue
. That is why we had aDataIntegrityViolationException
in the example above.
Hibernate documentation states:
The
ActionQueue
executes all operations in the following order:
OrphanRemovalAction
EntityInsertAction
orEntityIdentityInsertAction
EntityUpdateAction
QueuedOperationCollectionAction
CollectionRemoveAction
CollectionUpdateAction
CollectionRecreateAction
EntityDeleteAction
So, it means that in the example above (from the first section), Hibernate should:
- Do select in order to get
oldPerson
- Insert
newPerson
. BecauseEntityInsertAction
has a higher priority. - Delete
oldPerson
. BecauseEntityDeleteAction
has the lowest priority.
And... Here is the SQL log that proves it!
Hibernate: select p1_0.id,p1_0.email,p1_0.name from person p1_0 where p1_0.id=?
Hibernate: select nextval('person_sequence')
Hibernate: insert into person (email,name,id) values (?,?,?)
WARN 60977 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 0, SQLState: 23505
ERROR 60977 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : ERROR: duplicate key value violates unique constraint "person_email_uniq"
Detail: Key (email)=(mail@gmail.com) already exists.
If you're a truly patient person, you can always delve into the ActionQueue
code, set breakpoints, and DYOR (do your own research)!
What's in it for me?
- Now you can flex in technical interviews that you're a TOTAL PRO EXPERT in Hibernate 💪
- You understand that Hibernate is asynchronous 🤓
- You recognize that Hibernate executes SQL commands in a specific order, and you can use this knowledge in your apps to avoid tricky situations like the ones shown at the beginning of the article 😏
Bonus: Why ActionQueue have such a specific order of operations?
Let's break these operations down:
OrphanRemovalAction
Used when you delete something from the@OneToMany(orphanRemoval = true)
association. More about it here: link.EntityInsertAction
orEntityIdentityInsertAction
Insertions are prioritized due to the Hibernate developers' suggestion that it's more consistent to create entities at the beginning. This ensures that any subsequent operations that refer to this entity will find it in the database.
For example: if you prioritizedelete/update
, you might attempt todelete/update
something that hasn't been created yet.EntityUpdateAction
There are no comments necessary – it seems logical to run update queries after creating them. This guarantees that all entities you're attempting to update exist in the database.QueuedOperationCollectionAction
This action refers to all collection-related queued operations (not entity-related!). For these actions to occur, your entity should own that collection relation on the JPA level.CollectionRemoveAction
Try to verify this one yourself!🤪CollectionUpdateAction
Try to verify this one yourself!🤪CollectionRecreateAction
This concerns dropping and recreating a collection in its entirety. This might be necessary when the collection's structure has changed significantly. As this could be a resource-intensive operation, it's prioritized after other more granular collection operations.EntityDeleteAction
Delete the entity. It's likely positioned with the lowest priority to avoid situations where you reference a non-existent entity. So it seems logical.
That's it! Thank you for the reading!
PS: If you find something wrong, please correct me in the comments!
My links:
Linkedin
GitHub (mostly dead)
Top comments (0)