diff --git a/pom.xml b/pom.xml index e936a553f9..39c809795b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa - 1.7.0.BUILD-SNAPSHOT + 1.7.0.DATAJPA-562-SNAPSHOT Spring Data JPA Spring Data module for JPA repositories. diff --git a/src/main/java/org/springframework/data/jpa/repository/support/DetachingJpaResultPostProcessor.java b/src/main/java/org/springframework/data/jpa/repository/support/DetachingJpaResultPostProcessor.java new file mode 100644 index 0000000000..c6ce78ce1a --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/support/DetachingJpaResultPostProcessor.java @@ -0,0 +1,76 @@ +/* + * Copyright 2008-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.support; + +import java.util.ArrayList; +import java.util.List; + +import javax.persistence.EntityManager; + +import org.springframework.util.Assert; + +/** + * A {@link JpaResultPostProcessor} that potentially detaches resulting Domain-Objects from the configured + * {@link EntityManager}. + * + * @author Thomas Darimont + */ +public class DetachingJpaResultPostProcessor implements JpaResultPostProcessor { + + private final EntityManager em; + + /** + * Creates a new {@link DetachingJpaResultPostProcessor}. + * + * @param em must not be {@literal null}. + */ + public DetachingJpaResultPostProcessor(EntityManager em) { + + Assert.notNull(em, "EntityManager must not be null!"); + + this.em = em; + } + + /* (non-Javadoc) + * @see org.springframework.data.jpa.repository.support.JpaResultPostProcessor#postProcessResult(java.lang.Class, java.lang.Object) + */ + @Override + public R postProcessResult(Class domainClass, R entity) { + + if (domainClass.isInstance(entity)) { + em.detach(entity); + } + + return entity; + } + + /* (non-Javadoc) + * @see org.springframework.data.jpa.repository.support.JpaResultPostProcessor#postProcessResults(java.lang.Class, java.util.List) + */ + @Override + public List postProcessResults(Class domainClass, List results) { + + List postProcessedResults = new ArrayList(); + + for (T entity : results) { + if (domainClass.isInstance(entity)) { + postProcessedResults.add(postProcessResult(domainClass, entity)); + } + } + + return postProcessedResults; + } +} diff --git a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java index eb417336a5..062cbecd4b 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java @@ -35,12 +35,14 @@ * JPA specific generic repository factory. * * @author Oliver Gierke + * @author Thomas Darimont */ public class JpaRepositoryFactory extends RepositoryFactorySupport { private final EntityManager entityManager; private final QueryExtractor extractor; private final CrudMethodMetadataPostProcessor lockModePostProcessor; + private final JpaResultPostProcessor jpaResultPostProcessor; /** * Creates a new {@link JpaRepositoryFactory}. @@ -48,12 +50,23 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { * @param entityManager must not be {@literal null} */ public JpaRepositoryFactory(EntityManager entityManager) { + this(entityManager, NoopJpaResultPostProcessor.INSTANCE); + } + + /** + * Creates a new {@link JpaRepositoryFactory}. + * + * @param entityManager must not be {@literal null} + * @param evaluationContextProvider must not be {@literal null} + */ + public JpaRepositoryFactory(EntityManager entityManager, JpaResultPostProcessor jpaResultPostProcessor) { Assert.notNull(entityManager); this.entityManager = entityManager; this.extractor = PersistenceProvider.fromEntityManager(entityManager); this.lockModePostProcessor = CrudMethodMetadataPostProcessor.INSTANCE; + this.jpaResultPostProcessor = jpaResultPostProcessor; addRepositoryProxyPostProcessor(lockModePostProcessor); } @@ -68,6 +81,10 @@ protected Object getTargetRepository(RepositoryMetadata metadata) { SimpleJpaRepository repository = getTargetRepository(metadata, entityManager); repository.setRepositoryMethodMetadata(lockModePostProcessor.getLockMetadataProvider()); + if (jpaResultPostProcessor != null) { + repository.setJpaResultPostProcessor(jpaResultPostProcessor); + } + return repository; } diff --git a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java index 68ec233f99..be53f6d931 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java @@ -37,7 +37,9 @@ public class JpaRepositoryFactoryBean, S, ID extends Serializable> extends TransactionalRepositoryFactoryBeanSupport { - private EntityManager entityManager; + protected EntityManager entityManager; + + private JpaResultPostProcessor jpaResultPostProcessor; /** * The {@link EntityManager} to be used. @@ -58,6 +60,16 @@ public void setMappingContext(MappingContext mappingContext) { super.setMappingContext(mappingContext); } + /** + * The {@link JpaResultPostProcessor} to be used. + * + * @param jpaResultPostProcessor the jpaResultPostProcessor to set. + * @since 1.7 + */ + public void setJpaResultPostProcessor(JpaResultPostProcessor jpaResultPostProcessor) { + this.jpaResultPostProcessor = jpaResultPostProcessor; + } + /* * (non-Javadoc) * @@ -76,7 +88,7 @@ protected RepositoryFactorySupport doCreateRepositoryFactory() { * @return */ protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) { - return new JpaRepositoryFactory(entityManager); + return new JpaRepositoryFactory(entityManager, jpaResultPostProcessor); } /* diff --git a/src/main/java/org/springframework/data/jpa/repository/support/JpaResultPostProcessor.java b/src/main/java/org/springframework/data/jpa/repository/support/JpaResultPostProcessor.java new file mode 100644 index 0000000000..b3a483ee46 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/support/JpaResultPostProcessor.java @@ -0,0 +1,40 @@ +/* + * Copyright 2008-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.support; + +import java.util.List; + +/** + * A {@link JpaResultPostProcessor} allows to post-process a single or multiple entity result. + * + * @author Thomas Darimont + */ +public interface JpaResultPostProcessor { + + /** + * @param domainClass + * @param entity + * @return + */ + R postProcessResult(Class domainClass, R entity); + + /** + * @param domainClass + * @param results + * @return + */ + List postProcessResults(Class domainClass, List results); +} diff --git a/src/main/java/org/springframework/data/jpa/repository/support/NoopJpaResultPostProcessor.java b/src/main/java/org/springframework/data/jpa/repository/support/NoopJpaResultPostProcessor.java new file mode 100644 index 0000000000..7bd53a79aa --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/support/NoopJpaResultPostProcessor.java @@ -0,0 +1,44 @@ +/* + * Copyright 2008-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.support; + +import java.util.List; + +/** + * A {@link JpaResultPostProcessor} that simply returns the results as is. + * + * @author Thomas Darimont + */ +enum NoopJpaResultPostProcessor implements JpaResultPostProcessor { + + INSTANCE; + + /* (non-Javadoc) + * @see org.springframework.data.jpa.repository.support.JpaResultPostProcessor#postProcessResult(java.lang.Class, java.lang.Object) + */ + @Override + public R postProcessResult(Class domainClass, R entity) { + return entity; + } + + /* (non-Javadoc) + * @see org.springframework.data.jpa.repository.support.JpaResultPostProcessor#postProcessResults(java.lang.Class, java.util.List) + */ + @Override + public List postProcessResults(Class domainClass, List results) { + return results; + } +} diff --git a/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index 67c9b2a98d..85039f585e 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java @@ -70,6 +70,8 @@ public class SimpleJpaRepository implements JpaRepos private CrudMethodMetadata crudMethodMetadata; + private JpaResultPostProcessor jpaResultPostProcessor = NoopJpaResultPostProcessor.INSTANCE; + /** * Creates a new {@link SimpleJpaRepository} to manage objects of the given {@link JpaEntityInformation}. * @@ -106,6 +108,19 @@ public void setRepositoryMethodMetadata(CrudMethodMetadata crudMethodMetadata) { this.crudMethodMetadata = crudMethodMetadata; } + /** + * Configures a custom {@link JpaResultPostProcessor} to be used to post-process query results. + * + * @param jpaResultPostProcessor must not be {@literal null}. + * @since 1.7 + */ + public void setJpaResultPostProcessor(JpaResultPostProcessor jpaResultPostProcessor) { + + Assert.notNull(jpaResultPostProcessor, "JpaResultPostProcessor must not be null!"); + + this.jpaResultPostProcessor = jpaResultPostProcessor; + } + protected Class getDomainClass() { return entityInformation.getJavaType(); } @@ -213,13 +228,14 @@ public T findOne(ID id) { Class domainType = getDomainClass(); if (crudMethodMetadata == null) { - return em.find(domainType, id); + return potentiallyPostProcessResult(em.find(domainType, id)); } LockModeType type = crudMethodMetadata.getLockModeType(); Map hints = crudMethodMetadata.getQueryHints(); - return type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints); + return potentiallyPostProcessResult(type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, + hints)); } /* @@ -230,7 +246,7 @@ public T findOne(ID id) { public T getOne(ID id) { Assert.notNull(id, "The given id must not be null!"); - return em.getReference(getDomainClass(), id); + return potentiallyPostProcessResult(em.getReference(getDomainClass(), id)); } /* @@ -276,12 +292,20 @@ public boolean exists(ID id) { return query.getSingleResult() == 1L; } + private List potentiallyPostProcessResults(List resultList) { + return jpaResultPostProcessor.postProcessResults(getDomainClass(), resultList); + } + + private R potentiallyPostProcessResult(R result) { + return jpaResultPostProcessor.postProcessResult(getDomainClass(), result); + } + /* * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaRepository#findAll() */ public List findAll() { - return getQuery(null, (Sort) null).getResultList(); + return potentiallyPostProcessResults(getQuery(null, (Sort) null).getResultList()); } /* @@ -297,7 +321,7 @@ public List findAll(Iterable ids) { ByIdsSpecification specification = new ByIdsSpecification(); TypedQuery query = getQuery(specification, (Sort) null); - return query.setParameter(specification.parameter, ids).getResultList(); + return potentiallyPostProcessResults(query.setParameter(specification.parameter, ids).getResultList()); } /* @@ -305,7 +329,7 @@ public List findAll(Iterable ids) { * @see org.springframework.data.jpa.repository.JpaRepository#findAll(org.springframework.data.domain.Sort) */ public List findAll(Sort sort) { - return getQuery(null, sort).getResultList(); + return potentiallyPostProcessResults(getQuery(null, sort).getResultList()); } /* @@ -328,7 +352,7 @@ public Page findAll(Pageable pageable) { public T findOne(Specification spec) { try { - return getQuery(spec, (Sort) null).getSingleResult(); + return potentiallyPostProcessResult(getQuery(spec, (Sort) null).getSingleResult()); } catch (NoResultException e) { return null; } @@ -339,7 +363,7 @@ public T findOne(Specification spec) { * @see org.springframework.data.jpa.repository.JpaSpecificationExecutor#findAll(org.springframework.data.jpa.domain.Specification) */ public List findAll(Specification spec) { - return getQuery(spec, (Sort) null).getResultList(); + return potentiallyPostProcessResults(getQuery(spec, (Sort) null).getResultList()); } /* @@ -349,7 +373,8 @@ public List findAll(Specification spec) { public Page findAll(Specification spec, Pageable pageable) { TypedQuery query = getQuery(spec, pageable); - return pageable == null ? new PageImpl(query.getResultList()) : readPage(query, pageable, spec); + return pageable == null ? new PageImpl(potentiallyPostProcessResults(query.getResultList())) : readPage(query, + pageable, spec); } /* @@ -358,7 +383,7 @@ public Page findAll(Specification spec, Pageable pageable) { */ public List findAll(Specification spec, Sort sort) { - return getQuery(spec, sort).getResultList(); + return potentiallyPostProcessResults(getQuery(spec, sort).getResultList()); } /* @@ -366,7 +391,7 @@ public List findAll(Specification spec, Sort sort) { * @see org.springframework.data.repository.CrudRepository#count() */ public long count() { - return em.createQuery(getCountQueryString(), Long.class).getSingleResult(); + return this.potentiallyPostProcessResult(em.createQuery(getCountQueryString(), Long.class).getSingleResult()); } /* @@ -375,7 +400,7 @@ public long count() { */ public long count(Specification spec) { - return getCountQuery(spec).getSingleResult(); + return this.potentiallyPostProcessResult(getCountQuery(spec).getSingleResult()); } /* @@ -451,7 +476,8 @@ protected Page readPage(TypedQuery query, Pageable pageable, Specification query.setMaxResults(pageable.getPageSize()); Long total = QueryUtils.executeCountQuery(getCountQuery(spec)); - List content = total > pageable.getOffset() ? query.getResultList() : Collections. emptyList(); + List content = total > pageable.getOffset() ? potentiallyPostProcessResults(query.getResultList()) : Collections + . emptyList(); return new PageImpl(content, pageable, total); } diff --git a/src/test/java/org/springframework/data/jpa/domain/sample/SampleEntity.java b/src/test/java/org/springframework/data/jpa/domain/sample/SampleEntity.java index 51efd9675e..69d33a5a3c 100644 --- a/src/test/java/org/springframework/data/jpa/domain/sample/SampleEntity.java +++ b/src/test/java/org/springframework/data/jpa/domain/sample/SampleEntity.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2011 the original author or authors. + * Copyright 2008-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,14 +18,18 @@ import javax.persistence.EmbeddedId; import javax.persistence.Entity; +import org.springframework.util.ObjectUtils; + /** * @author Oliver Gierke + * @author Thomas Darimont */ @Entity public class SampleEntity { - @EmbeddedId - protected SampleEntityPK id; + @EmbeddedId protected SampleEntityPK id; + + protected String attribute1; protected SampleEntity() { @@ -49,12 +53,19 @@ public boolean equals(Object obj) { SampleEntity that = (SampleEntity) obj; - return this.id.equals(that.id); + return this.id.equals(that.id) && ObjectUtils.nullSafeEquals(this.attribute1, that.attribute1); + } + + public String getAttribute1() { + return attribute1; + } + + public void setAttribute1(String attribute1) { + this.attribute1 = attribute1; } @Override public int hashCode() { - - return id.hashCode(); + return 17 * ObjectUtils.nullSafeHashCode(id) + ObjectUtils.nullSafeHashCode(attribute1); } } diff --git a/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryTests.java b/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryTests.java index b827d9ea94..8238bf68f1 100644 --- a/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryTests.java @@ -51,12 +51,15 @@ public class JpaRepositoryTests { JpaRepository repository; CrudRepository idClassRepository; + SampleEntityRepository detachingRepository; @Before public void setUp() { repository = new JpaRepositoryFactory(em).getRepository(SampleEntityRepository.class); idClassRepository = new JpaRepositoryFactory(em).getRepository(SampleWithIdClassRepository.class); + detachingRepository = new JpaRepositoryFactory(em, new DetachingJpaResultPostProcessor(em)) + .getRepository(SampleEntityRepository.class); } @Test @@ -121,6 +124,59 @@ public void executesExistsForEntityWithIdClass() { assertThat(idClassRepository.exists(id), is(true)); } + /** + * @see DATAJPA-562 + */ + @Test + public void shouldNotPropagateChangesViaImplicitFlushing() { + + SampleEntityPK id = new SampleEntityPK("foo", "bar"); + SampleEntity entity = new SampleEntity("foo", "bar"); + entity = detachingRepository.saveAndFlush(entity); + + SampleEntity detachedEntity = detachingRepository.findOne(id); + detachedEntity.setAttribute1("bubu"); + + SampleEntity detachedEntity2 = detachingRepository.findOne(id); + assertThat(detachedEntity2.getAttribute1(), is(nullValue())); + } + + /** + * @see DATAJPA-562 + */ + @Test + public void shouldPropagateChangesViaExplcitSaveAndFlush() { + + SampleEntityPK id = new SampleEntityPK("foo", "bar"); + SampleEntity entity = new SampleEntity("foo", "bar"); + entity = detachingRepository.saveAndFlush(entity); + + SampleEntity detachedEntity = detachingRepository.findOne(id); + detachedEntity.setAttribute1("bubu"); + detachedEntity = detachingRepository.saveAndFlush(detachedEntity); + + SampleEntity detachedEntity2 = detachingRepository.findOne(id); + assertThat(detachedEntity2.getAttribute1(), is(detachedEntity.getAttribute1())); + } + + /** + * @see DATAJPA-562 + */ + @Test + public void shouldPropagateChangesViaExplcitSave() { + + SampleEntityPK id = new SampleEntityPK("foo", "bar"); + SampleEntity entity = new SampleEntity("foo", "bar"); + entity = detachingRepository.saveAndFlush(entity); + + SampleEntity detachedEntity = detachingRepository.findOne(id); + detachedEntity.setAttribute1("bubu"); + detachedEntity = detachingRepository.save(detachedEntity); + + SampleEntity detachedEntity2 = detachingRepository.findOne(id); + assertThat(detachedEntity2.getAttribute1(), is(detachedEntity.getAttribute1())); + } + private static interface SampleEntityRepository extends JpaRepository { }