You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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")
publicvoiddelete(Tentity) {
Assert.notNull(entity, "Entity must not be null");
if (entityInformation.isNew(entity)) {
return;
}
Class<?> type = ProxyUtils.getUserClass(entity);
Texisting = (T) entityManager.find(type, entityInformation.getId(entity));
// if the entity to be deleted doesn't exist, delete is a NOOPif (existing == null) {
return;
}
entityManager.remove(entityManager.contains(entity) ? entity : entityManager.merge(entity));
}
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.
the same row in the database gets modified in the database by some other process, updating the version attribute.
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.
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.
the same row in the database gets modified in the database by some other process, updating the version attribute.
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.
it still cannot prevent throwing OptimisticLockingFailureException.
merge(entity) gets called.
the same row in the database gets modified in the database by some other process, updating the version attribute.
delete(entity) gets called will throw OptimisticLockingFailureException.
OptimisticLockingFailureException should be thrown for safety if optimistic lock failed.
OptimisticLockingFailureException should be thrown for safety if optimistic lock failed.
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.
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.
OptimisticLockingFailureException may be thrown no matter merge() is called or not.
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 use existing as an argument for remove which would not throw an optimistic locking exception in the scenario I out lined.
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 use existing as an argument for remove which would not throw an optimistic locking exception in the scenario I out lined.
Using existing instead of entity may throw OptimisticLockingFailureException also if the database row is updated between find() and remove().
I find out that merge gets called to avoid InvalidDataAccessApiUsageException not OptimisticLockingFailureException, because there is no way to reattach a stale detached entity in JPA, see https://stackoverflow.com/a/4438358.
Here is failed tests:
[ERROR] Errors:
[ERROR] JavaConfigUserRepositoryTests>UserRepositoryTests.removeDetachedObject:629 » InvalidDataAccessApiUsage Removing a detached instance org.springframework.data.jpa.domain.sample.User#756
[ERROR] NamespaceUserRepositoryTests>UserRepositoryTests.removeDetachedObject:629 » InvalidDataAccessApiUsage Removing a detached instance org.springframework.data.jpa.domain.sample.User#756
[ERROR] UserRepositoryTests.removeDetachedObject:629 » InvalidDataAccessApiUsage Removing a detached instance org.springframework.data.jpa.domain.sample.User#757
[ERROR] JpaRepositoryTests.testCrudOperationsForCompoundKeyEntity:76 » IllegalArgument Removing a detached instance org.springframework.data.jpa.domain.sample.SampleEntity#org.springframework.data.jpa.domain.sample.SampleEntityPK@5e1258
Nobody said that merge gets called to avoid OptimisticLockingFailureException. The opposite is true. In the situation described in my first comment we must have an OptimisticLockingFailureException!
changed the title [-] 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.[/+]on Mar 20, 2024
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 the contains() method is called again at the point of invoking the remove() method, even though the entity is already considered to be in a persistent state when the entityManager.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.
Thread A: Loads and modifies the entity (Entity@v1 -> Entity@v2, stored in the persistence context).
Thread B: Attempts to delete the same entity by calling the delete() method.
Thread B: Inside the 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 the delete() method in Thread B, at the point where entityManager.contains() is called internally to check the persistent state, the entity has already been retrieved via the find() 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 the find() method has been called and existing != null?
@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 condition existing != null is satisfied, it doesn't mean that the entity is in a persistent state by the time contains() 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?
I'm curious about why SimpleJpaRepository checks twice whether the entity is in a persistent state when calling the delete method. · Issue #3401 · spring-projects/spring-data-jpa
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.quaff commentedon Mar 18, 2024
@schauder How about this improvement?
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
![-] 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.