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 + + +