Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Eliminate unnecessary merge() if entity is non-versioned #3402

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import java.util.Collection;
import java.util.Map;
import java.util.Optional;

import org.springframework.data.jpa.repository.query.JpaEntityMetadata;
import org.springframework.data.repository.core.EntityInformation;
Expand All @@ -30,6 +31,7 @@
* @author Oliver Gierke
* @author Thomas Darimont
* @author Mark Paluch
* @author Yanming Zhou
*/
public interface JpaEntityInformation<T, ID> extends EntityInformation<T, ID>, JpaEntityMetadata<T> {

Expand All @@ -39,6 +41,11 @@ public interface JpaEntityInformation<T, ID> extends EntityInformation<T, ID>, J
@Nullable
SingularAttribute<? super T, ?> getIdAttribute();

/**
* Returns the version attribute of the entity.
*/
Optional<SingularAttribute<? super T, ?>> getVersionAttribute();

/**
* Returns the required identifier type.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,15 @@

/**
* Implementation of {@link org.springframework.data.repository.core.EntityInformation} that uses JPA {@link Metamodel}
* to find the domain class' id field.
* to find the domain class' id and version field.
*
* @author Oliver Gierke
* @author Thomas Darimont
* @author Christoph Strobl
* @author Mark Paluch
* @author Jens Schauder
* @author Greg Turnquist
* @author Yanming Zhou
*/
public class JpaMetamodelEntityInformation<T, ID> extends JpaEntityInformationSupport<T, ID> {

Expand Down Expand Up @@ -191,6 +192,11 @@ public Class<ID> getIdType() {
return idMetadata.getSimpleIdAttribute();
}

@Override
public Optional<SingularAttribute<? super T, ?>> getVersionAttribute() {
return versionAttribute;
}

@Override
public boolean hasCompositeId() {
return !idMetadata.hasSimpleId();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,13 @@ public void delete(T entity) {
return;
}

entityManager.remove(entityManager.contains(entity) ? entity : entityManager.merge(entity));
if (entityInformation.getVersionAttribute().isPresent()) {
// call merge() to raise ObjectOptimisticLockingFailureException if entity is stale
entityManager.remove(entityManager.contains(entity) ? entity : entityManager.merge(entity));
}
else {
entityManager.remove(existing);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

/**
* @author Oliver Gierke
* @author Yanming Zhou
*/
@Entity
public class VersionedUser {
Expand All @@ -33,6 +34,8 @@ public class VersionedUser {
@Version
private Long version;

private String name;

public Long getId() {
return id;
}
Expand All @@ -48,4 +51,12 @@ public Long getVersion() {
public void setVersion(Long version) {
this.version = version;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
Expand All @@ -42,6 +43,7 @@
*
* @author Oliver Gierke
* @author Jens Schauder
* @author Yanming Zhou
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
Expand Down Expand Up @@ -88,6 +90,11 @@ static class DummyJpaEntityInformation<T, ID> extends JpaEntityInformationSuppor
return null;
}

@Override
public Optional<SingularAttribute<? super T, ?>> getVersionAttribute() {
return Optional.empty();
}

@Override
public ID getId(T entity) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,36 @@
package org.springframework.data.jpa.repository.support;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;

import jakarta.persistence.EntityManager;
import jakarta.persistence.OptimisticLockException;
import jakarta.persistence.PersistenceContext;

import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.sql.DataSource;

import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.sample.PersistableWithIdClass;
import org.springframework.data.jpa.domain.sample.PersistableWithIdClassPK;
import org.springframework.data.jpa.domain.sample.SampleEntity;
import org.springframework.data.jpa.domain.sample.SampleEntityPK;
import org.springframework.data.jpa.domain.sample.VersionedUser;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -46,21 +58,31 @@
* @author Jens Schauder
* @author Greg Turnquist
* @author Krzysztof Krason
* @author Yanming Zhou
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration({ "classpath:infrastructure.xml" })
@Transactional
class JpaRepositoryTests {

@Autowired DataSource dataSource;

@PersistenceContext EntityManager em;

private EntityManager spiedEntityManager;

private JpaRepository<SampleEntity, SampleEntityPK> repository;
private CrudRepository<PersistableWithIdClass, PersistableWithIdClassPK> idClassRepository;
private JpaRepository<VersionedUser, Long> versionedUserRepository;
private NamedParameterJdbcOperations jdbcOperations;

@BeforeEach
void setUp() {
repository = new JpaRepositoryFactory(em).getRepository(SampleEntityRepository.class);
spiedEntityManager = Mockito.spy(em);
repository = new JpaRepositoryFactory(spiedEntityManager).getRepository(SampleEntityRepository.class);
idClassRepository = new JpaRepositoryFactory(em).getRepository(SampleWithIdClassRepository.class);
versionedUserRepository = new JpaRepositoryFactory(em).getRepository(VersionedUserRepository.class);
jdbcOperations = new NamedParameterJdbcTemplate(dataSource);
}

@Test
Expand Down Expand Up @@ -162,6 +184,60 @@ public Iterator<SampleEntityPK> iterator() {
assertThat(repository.findAll()).containsExactly(two);
}

@Test
void deleteDirtyDetachedVersionedEntityShouldRaiseOptimisticLockException() {

VersionedUser entity = new VersionedUser();
entity.setName("name");
versionedUserRepository.save(entity);
versionedUserRepository.flush();
em.detach(entity);

versionedUserRepository.findById(entity.getId()).ifPresent(u -> {
u.setName("new name");
versionedUserRepository.flush();
});

assertThatExceptionOfType(OptimisticLockException.class).isThrownBy(() -> {
versionedUserRepository.delete(entity);
versionedUserRepository.flush();
});

jdbcOperations.update("delete from VersionedUser", Map.of());
}

@Test
void deleteDirtyManagedVersionedEntityShouldRaiseOptimisticLockException() {

VersionedUser entity = new VersionedUser();
entity.setName("name");
versionedUserRepository.save(entity);
versionedUserRepository.flush();


assertThat(jdbcOperations.update("update VersionedUser set version=version+1 where id=:id",
Map.of("id", entity.getId()))).isEqualTo(1);

assertThatExceptionOfType(OptimisticLockException.class).isThrownBy(() -> {
versionedUserRepository.delete(entity);
versionedUserRepository.flush();
});

jdbcOperations.update("delete from VersionedUser", Map.of());
}

@Test //GH-3401
void deleteNonVersionedEntityShouldNotInvokeMerge() {
SampleEntity entity = new SampleEntity("one", "eins");
repository.save(entity);
repository.flush();
em.detach(entity);

reset(spiedEntityManager);
repository.delete(entity);
then(spiedEntityManager).should(never()).merge(entity);
}

private interface SampleEntityRepository extends JpaRepository<SampleEntity, SampleEntityPK> {

}
Expand All @@ -170,4 +246,8 @@ private interface SampleWithIdClassRepository
extends CrudRepository<PersistableWithIdClass, PersistableWithIdClassPK> {

}

private interface VersionedUserRepository extends JpaRepository<VersionedUser, Long> {

}
}