From e8240b7af50c781c37eaac128e672a502bb55491 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 | 58 ++++++ .../support/JpaRepositoryFactory.java | 15 ++ .../ProjectionsIntegrationTests.java | 173 ++++++++++++++++++ .../simple-persistence/simple-persistence.xml | 8 + 4 files changed, 254 insertions(+) create mode 100644 src/main/java/org/springframework/data/jpa/projection/CollectionAwareProjectionFactory.java 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..6d56f57a5 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/projection/CollectionAwareProjectionFactory.java @@ -0,0 +1,58 @@ +/* + * 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 + */ +public class CollectionAwareProjectionFactory extends SpelAwareProxyProjectionFactory { + + @Override + protected ProjectionInformation createProjectionInformation(Class projectionType) { + return new CollectionAwareProjectionInformation(projectionType); + } + + private class CollectionAwareProjectionInformation extends SpelAwareProjectionInformation { + + CollectionAwareProjectionInformation(Class projectionType) { + super(projectionType); + } + + @Override + protected boolean isInputProperty(PropertyDescriptor descriptor) { + + if (!super.isInputProperty(descriptor)) { + return false; + } + + boolean isMapOrCollection = // + Collection.class.isAssignableFrom(descriptor.getPropertyType()) // + || Map.class.isAssignableFrom(descriptor.getPropertyType()); + + return !isMapOrCollection; + } + } +} 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 f67262628..042c1c338 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 @@ -22,10 +22,14 @@ import java.util.Optional; 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; @@ -42,6 +46,7 @@ import org.springframework.util.Assert; * @author Oliver Gierke * @author Mark Paluch * @author Christoph Strobl + * @author Jens Schauder */ public class JpaRepositoryFactory extends RepositoryFactorySupport { @@ -118,6 +123,16 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { } } + @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..ec4d6dcae --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/projections/ProjectionsIntegrationTests.java @@ -0,0 +1,173 @@ +/* + * 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.assertj.core.api.Assertions.*; + +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; + +import lombok.Data; + +/** + * 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 + + +