From f999800e5d28cc921caef089dca002adf7002648 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Wed, 13 Sep 2017 12:48:18 +0200 Subject: [PATCH] DATAJPA-1173 - Projections containing collections are considered open. It is not reasonably possible to create a JPA query that creates tuples with collections of referenced entities. Therefore projections that contain attributes of type Map or Collection are now considered open. We now use a custom ProjectionFactory to achieve this. The added integration test does not depend on the existing integration tests or their infrastructure and uses java configuration instead of XML. This reduces the setup to what actually is needed for this test. Original pull request: #218. Related issue: DATACMNS-1158. --- .../CollectionAwareProjectionFactory.java | 64 +++++++ ...CollectionAwareProjectionFactory.java.orig | 70 +++++++ .../support/JpaRepositoryFactory.java | 20 ++ .../ProjectionsIntegrationTests.java | 176 ++++++++++++++++++ .../simple-persistence/simple-persistence.xml | 8 + 5 files changed, 338 insertions(+) create mode 100644 src/main/java/org/springframework/data/jpa/projection/CollectionAwareProjectionFactory.java create mode 100644 src/main/java/org/springframework/data/jpa/projection/CollectionAwareProjectionFactory.java.orig create mode 100644 src/test/java/org/springframework/data/jpa/repository/projections/ProjectionsIntegrationTests.java create mode 100644 src/test/resources/simple-persistence/simple-persistence.xml diff --git a/src/main/java/org/springframework/data/jpa/projection/CollectionAwareProjectionFactory.java b/src/main/java/org/springframework/data/jpa/projection/CollectionAwareProjectionFactory.java new file mode 100644 index 000000000..08969d81b --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/projection/CollectionAwareProjectionFactory.java @@ -0,0 +1,64 @@ +/* + * Copyright 2017 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.projection; + +import java.beans.PropertyDescriptor; +import java.util.Collection; +import java.util.Map; + +import org.springframework.data.projection.ProjectionInformation; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; + +/** + * A {@link org.springframework.data.projection.ProjectionFactory} considering projections containing collections or + * maps to be open. + * + * @author Jens Schauder + * @author Oliver Gierke + */ +public class CollectionAwareProjectionFactory extends SpelAwareProxyProjectionFactory { + + /* + * (non-Javadoc) + * @see org.springframework.data.projection.SpelAwareProxyProjectionFactory#getProjectionInformation(java.lang.Class) + */ + @Override + public ProjectionInformation getProjectionInformation(Class projectionType) { + return new CollectionAwareProjectionInformation(projectionType); + } + + private static class CollectionAwareProjectionInformation extends SpelAwareProjectionInformation { + + CollectionAwareProjectionInformation(Class projectionType) { + super(projectionType); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.projection.SpelAwareProxyProjectionFactory.SpelAwareProjectionInformation#isInputProperty(java.beans.PropertyDescriptor) + */ + @Override + protected boolean isInputProperty(PropertyDescriptor descriptor) { + + if (!super.isInputProperty(descriptor)) { + return false; + } + + return !(Collection.class.isAssignableFrom(descriptor.getPropertyType()) // + || Map.class.isAssignableFrom(descriptor.getPropertyType())); + } + } +} diff --git a/src/main/java/org/springframework/data/jpa/projection/CollectionAwareProjectionFactory.java.orig b/src/main/java/org/springframework/data/jpa/projection/CollectionAwareProjectionFactory.java.orig new file mode 100644 index 000000000..25bb1f4b3 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/projection/CollectionAwareProjectionFactory.java.orig @@ -0,0 +1,70 @@ +/* + * Copyright 2017 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.projection; + +import java.beans.PropertyDescriptor; +import java.util.Collection; +import java.util.Map; + +import org.springframework.data.projection.ProjectionInformation; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; + +/** + * A {@link org.springframework.data.projection.ProjectionFactory} considering projections containing collections or + * maps to be open. + * + * @author Jens Schauder + * @author Oliver Gierke + */ +public class CollectionAwareProjectionFactory extends SpelAwareProxyProjectionFactory { + +<<<<<<< HEAD + /* + * (non-Javadoc) + * @see org.springframework.data.projection.SpelAwareProxyProjectionFactory#getProjectionInformation(java.lang.Class) +======= + /* + * (non-Javadoc) + * @see org.springframework.data.projection.SpelAwareProxyProjectionFactory#createProjectionInformation(java.lang.Class) +>>>>>>> bc828b9a... DATAJPA-1173 - Polishing. + */ + @Override + public ProjectionInformation getProjectionInformation(Class projectionType) { + return new CollectionAwareProjectionInformation(projectionType); + } + + private static class CollectionAwareProjectionInformation extends SpelAwareProjectionInformation { + + CollectionAwareProjectionInformation(Class projectionType) { + super(projectionType); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.projection.SpelAwareProxyProjectionFactory.SpelAwareProjectionInformation#isInputProperty(java.beans.PropertyDescriptor) + */ + @Override + protected boolean isInputProperty(PropertyDescriptor descriptor) { + + if (!super.isInputProperty(descriptor)) { + return false; + } + + return !(Collection.class.isAssignableFrom(descriptor.getPropertyType()) // + || Map.class.isAssignableFrom(descriptor.getPropertyType())); + } + } +} 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 a4a2bfa84..172fb86b0 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 @@ -21,10 +21,14 @@ import java.io.Serializable; import javax.persistence.EntityManager; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.data.jpa.projection.CollectionAwareProjectionFactory; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.querydsl.QueryDslPredicateExecutor; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; @@ -39,6 +43,7 @@ import org.springframework.util.Assert; * * @author Oliver Gierke * @author Mark Paluch + * @author Jens Schauder */ public class JpaRepositoryFactory extends RepositoryFactorySupport { @@ -68,6 +73,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { */ @Override public void setBeanClassLoader(ClassLoader classLoader) { + super.setBeanClassLoader(classLoader); this.crudMethodMetadataPostProcessor.setBeanClassLoader(classLoader); } @@ -119,6 +125,20 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { } } + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getProjectionFactory(java.lang.ClassLoader, org.springframework.beans.factory.BeanFactory) + */ + @Override + protected ProjectionFactory getProjectionFactory(ClassLoader classLoader, BeanFactory beanFactory) { + + CollectionAwareProjectionFactory factory = new CollectionAwareProjectionFactory(); + factory.setBeanClassLoader(classLoader); + factory.setBeanFactory(beanFactory); + + return factory; + } + /** * Returns whether the given repository interface requires a QueryDsl specific implementation to be chosen. * diff --git a/src/test/java/org/springframework/data/jpa/repository/projections/ProjectionsIntegrationTests.java b/src/test/java/org/springframework/data/jpa/repository/projections/ProjectionsIntegrationTests.java new file mode 100644 index 000000000..11a3fd45e --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/projections/ProjectionsIntegrationTests.java @@ -0,0 +1,176 @@ +/* + * Copyright 2017 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.projections; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.EntityManagerFactory; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.sql.DataSource; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.jpa.repository.projections.ProjectionsIntegrationTests.Config; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the behavior of projections. + * + * @author Jens Schauder + */ +@Transactional +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = Config.class) +public class ProjectionsIntegrationTests { + + @Autowired DummyEntityWithCollectionRepository repository; + + @Before + public void setup() { + + DummyEntityWithCollection entity = new DummyEntityWithCollection(); + entity.setName("A Name"); + entity.getSubs().add(createSubEntity(1)); + entity.getSubs().add(createSubEntity(2)); + + repository.save(entity); + } + + @Test // DATAJPA-1173 + public void findAllFindsTheSingleEntity() { + assertThat(repository.findAll(), hasSize(1)); + } + + @Test // DATAJPA-1173 + public void findAllProjectedFindsTheSingleEntity() { + assertThat(repository.findAllProjectedBy(), hasSize(1)); + } + + private SubEntity createSubEntity(int index) { + + SubEntity entity = new SubEntity(); + entity.setName("sub-" + index); + return entity; + } + + @Data + @Entity(name = "Dummy") + @Table(name = "DummyEntity") + static class DummyEntityWithCollection { + + @GeneratedValue @Id // + Long id; + + String name; + + @OneToMany(cascade = CascadeType.ALL) @JoinColumn(name = "subs") // + List subs = new ArrayList(); + + String otherAttribute; + } + + @Data + @Entity + @Table(name = "SubEntity") + static class SubEntity { + + @GeneratedValue @Id Long id; + String name; + String otherAttribute; + } + + interface DummyEntityProjection { + + String getName(); + + List getSubs(); + } + + interface SubEntityProjection { + String getName(); + } + + interface DummyEntityWithCollectionRepository extends JpaRepository { + List findAllProjectedBy(); + } + + @EnableJpaRepositories(considerNestedRepositories = true) + @EnableTransactionManagement + static class Config { + + @Bean + DataSource dataSource() { + + return new EmbeddedDatabaseBuilder() // + .generateUniqueName(true) // + .setType(EmbeddedDatabaseType.HSQL) // + .setScriptEncoding("UTF-8") // + .ignoreFailedDrops(true) // + .build(); + } + + @Bean + AbstractEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) { + + LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean(); + factoryBean.setDataSource(dataSource); + factoryBean.setPersistenceUnitRootLocation("simple-persistence"); + factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); + + factoryBean.setPackagesToScan(this.getClass().getPackage().getName()); + + Properties properties = new Properties(); + properties.setProperty("hibernate.hbm2ddl.auto", "create"); + properties.setProperty("hibernate.dialect", "org.hibernate.dialect.HSQLDialect"); + factoryBean.setJpaProperties(properties); + + return factoryBean; + } + + @Bean + PlatformTransactionManager transactionManager(EntityManagerFactory emf) { + return new JpaTransactionManager(emf); + } + } +} diff --git a/src/test/resources/simple-persistence/simple-persistence.xml b/src/test/resources/simple-persistence/simple-persistence.xml new file mode 100644 index 000000000..adc69aa86 --- /dev/null +++ b/src/test/resources/simple-persistence/simple-persistence.xml @@ -0,0 +1,8 @@ + + + + + true + + +