Not planned
Description
I think if the entity was retrieved using the find()
in the delete method of SimpleJpaRepository, it can be considered as already managed by the persistence context.
However, I'm curious if there is a need to check if the entity exists in the persistence context using the contains()
method before executing the remove()
method below.
@Override
@Transactional
@SuppressWarnings("unchecked")
public void delete(T entity) {
Assert.notNull(entity, "Entity must not be null");
if (entityInformation.isNew(entity)) {
return;
}
Class<?> type = ProxyUtils.getUserClass(entity);
T existing = (T) entityManager.find(type, entityInformation.getId(entity));
// if the entity to be deleted doesn't exist, delete is a NOOP
if (existing == null) {
return;
}
entityManager.remove(entityManager.contains(entity) ? entity : entityManager.merge(entity));
}
If so, isn't it unnecessary to call contains()
?
Activity
schauder commentedon Mar 18, 2024
I think this somewhat convoluted process is necessary for cases where
entity
was modified, before the call to delete, and/or to make sure optimistic locking works as intended.Consider this:
entity
gets loaded, and detached.delete(entity)
gets called.In this case the delete should fail, but if we use
entityManager.remove(entityManger.find(..))
instead, it will actually succeed.On the other hand, if we don't check if it exists first, calling
delete
on a new entity will actually persist it, which probably isn't what was intended.Eliminate unnecessary merge() if possible
quaff commentedon Mar 18, 2024
@schauder How about this improvement?
Eliminate unnecessary merge() if possible
quaff commentedon Mar 18, 2024
it still cannot prevent throwing
OptimisticLockingFailureException
.merge(entity)
gets called.delete(entity)
gets called will throwOptimisticLockingFailureException
.OptimisticLockingFailureException
should be thrown for safety if optimistic lock failed.schauder commentedon Mar 18, 2024
That's the point: If we don't use the entity passed into the method we might end up doing a delete which should fail with an optimistic locking exception.
quaff commentedon Mar 18, 2024
OptimisticLockingFailureException
may be thrown no mattermerge()
is called or not.schauder commentedon Mar 18, 2024
If you want to use the positive result of the
find
call as an indication that the entity in question is managed, you'd have to useexisting
as an argument forremove
which would not throw an optimistic locking exception in the scenario I out lined.quaff commentedon Mar 18, 2024
Using
existing
instead ofentity
may throwOptimisticLockingFailureException
also if the database row is updated betweenfind()
andremove()
.schauder commentedon Mar 18, 2024
Yes, but it won't throw an exception when the change happend before the
find
quaff commentedon Mar 19, 2024
So is it worthy to perform
find()
andmerge()
thenremove()
since the exception can not be avoided completely?Eliminate unnecessary merge()
quaff commentedon Mar 19, 2024
I find out that
merge
gets called to avoidInvalidDataAccessApiUsageException
notOptimisticLockingFailureException
, because there is no way to reattach a stale detached entity in JPA, see https://stackoverflow.com/a/4438358.Here is failed tests:
The fix is very simple, I've updated the PR.
schauder commentedon Mar 19, 2024
Nobody said that
merge
gets called to avoidOptimisticLockingFailureException
. The opposite is true. In the situation described in my first comment we must have anOptimisticLockingFailureException
!Eliminate unnecessary merge() if entity is non-versioned
[-] The inquiry is about the invocation of the merge() within the delete() of the SimpleJpaRepository class.[/-][+] I'm curious about why SimpleJpaRepository checks twice whether the entity is in a persistent state when calling the delete method.[/+]oxahex commentedon Mar 20, 2024
Issue title revised for clarity as it seemed unclear and caused confusion regarding the topic I intended to inquire about.
I agree with your explanation. @schauder
Probably, your initial response regarding the necessity of the
merge()
method was in response to my inquiry. There seems to have been some confusion there. What I was curious about was why thecontains()
method is called again at the point of invoking theremove()
method, even though the entity is already considered to be in a persistent state when theentityManager.find()
method is called.Now, let's consider another scenario: Threads A and B are running independently in separate transactions. Entity@v1 is already loaded into Thread A's persistence context, and Thread A is currently modifying it. At this point, Thread B attempts to delete the same entity.
delete()
method.delete()
method,entityManager.find()
is used to retrieve the entity. Since Thread A's changes have not been committed, the entity's version in the database remains at v1. Therefore, existing is assigned Entity@v1 retrieved from the database.In this case, although
existing != null
within thedelete()
method in Thread B, at the point whereentityManager.contains()
is called internally to check the persistent state, the entity has already been retrieved via thefind()
method into Thread B's persistence context or directly from the database.Therefore, at the time of the
remove()
method call, the entity is indeed in a persistent state.If so, isn't it unnecessary to call
contains()
?Is there a scenario where the return value of the
contains()
method is false even when thefind()
method has been called andexisting != null
?schauder commentedon Mar 20, 2024
Yes, when
entity
is in detached state.find
will return a different instance andentity
is still detached, i.e.contains(entity)
will returnfalse
.oxahex commentedon Mar 20, 2024
@schauder
I had thought that a detached entity transitions to a persistent state by the time
find()
is called based on the id field, but I realize now that this is incorrect. Therefore, just because the conditionexisting != null
is satisfied, it doesn't mean that the entity is in a persistent state by the timecontains()
method is invoked. As you mentioned, it can return a different entity instance.So, calling the
contains()
method ultimately serves as a kind of defensive strategy, doesn't it?Eliminate unnecessary merge() if entity is non-versioned
mp911de commentedon Jan 31, 2025
Wrapping up that discussion. We want to keep things as they are unless there is a significant improvement.