spring-data-jpa icon indicating copy to clipboard operation
spring-data-jpa copied to clipboard

Use tuple queries for projections using QueryDSL and Query by Example

Open kekhuai opened this issue 2 years ago • 3 comments

I'm implementing a dynamic query with following code.

userRepository.findBy(QUser.user.id.isNotNull(), q -> q.project("username").as(UserWithUsernameOnly.class).all());

But the generated query still select all of the columns.

select user0_.id as id1_0_, user0_.create_date as create_d2_0_, user0_.created_by as created_3_0_, user0_.last_modified_by as last_mod4_0_, user0_.last_modified_date as last_mod5_0_, user0_.password as password6_0_, user0_.username as username7_0_, user0_.version as version8_0_ from users user0_ where user0_.id is not null

I expect the generated query should only select the specified column.

Here is the reproducible repo. https://github.com/kekhuay/special-palm-tree

kekhuai avatar Jul 18 '22 17:07 kekhuai

I have certainly reproduced this issue. To be clear, the query works. It simply doesn't slim things down to the subset of columns expected.

I tested against both H2 as well as Postgres with the following test case:

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = FetchableFluentQueryByPredicateIntegrationTests.Config.class)
@Transactional
public class FetchableFluentQueryByPredicateIntegrationTests {

	@Autowired FetchableFluentRepository repository;

	@BeforeEach
	void setUp() {
		repository.saveAndFlush(new User("Bilbo", "Baggins", "[email protected]"));
	}

	@Test
	void projectionsOnDtoClassesShouldHaveAReducedProjectionInTheQuery() {

		List<UserDto> users = repository.findBy(QUser.user.firstname.eq("Bilbo"), p -> p //
				.project("firstname") //
				.as(UserDto.class) //
				.all());

		assertThat(users).extracting(UserDto::getFirstname).containsExactly("Bilbo");
	}

	@Test
	void projectionsOnEntitiesShouldHaveAReducedProjectionInTheQuery() {

		List<User> users = repository.findBy(QUser.user.firstname.eq("Bilbo"), p -> p //
				.project("firstname") //
				.all());

		assertThat(users).extracting(User::getFirstname).containsExactly("Bilbo");
	}

	public interface FetchableFluentRepository extends JpaRepository<User, Long>, QuerydslPredicateExecutor<User> {

	}

	public interface UserDto {

		String getFirstname();
	}

	@EnableJpaRepositories(considerNestedRepositories = true, basePackageClasses = FetchableFluentRepository.class, //
			includeFilters = @ComponentScan.Filter(value = { FetchableFluentRepository.class },
					type = FilterType.ASSIGNABLE_TYPE))
	@EnableTransactionManagement
	static class Config {

		@Bean(initMethod = "start", destroyMethod = "stop")
		public PostgreSQLContainer<?> container() {

			return new PostgreSQLContainer<>("postgres:9.6.12") //
					.withUsername("postgres");
		}

		@Bean
		public DataSource dataSource(PostgreSQLContainer<?> container) {

			PGSimpleDataSource dataSource = new PGSimpleDataSource();
			dataSource.setUrl(container.getJdbcUrl());
			dataSource.setUser(container.getUsername());
			dataSource.setPassword(container.getPassword());
			return dataSource;
		}

		@Bean
		public AbstractEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {

			LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
			factoryBean.setDataSource(dataSource);
			factoryBean.setPersistenceUnitRootLocation("simple-persistence");
			factoryBean.setPersistenceUnitName("spring-data-jpa");
			factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());

			Properties properties = new Properties();
			properties.setProperty("hibernate.hbm2ddl.auto", "create");
			properties.setProperty("hibernate.dialect", PostgreSQL91Dialect.class.getCanonicalName());
			factoryBean.setJpaProperties(properties);

			return factoryBean;
		}

		@Bean
		PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
			return new JpaTransactionManager(entityManagerFactory);
		}
	}
}

In both scenarios, first on H2 (before adding the Testcontainers chunk now shown) as well as on Postgres (see above), I saw this query:

2023-03-28 09:05:55,297 DEBUG                        org.hibernate.SQL: 128 - select u1_0.id,u1_0.DTYPE,u1_0.active,u1_0.city,u1_0.country,u1_0.streetName,u1_0.streetNo,u1_0.age,u1_0.binaryData,u1_0.createdAt,u1_0.dateOfBirth,u1_0.emailAddress,u1_0.firstname,u1_0.lastname,u1_0.manager_id from SD_User u1_0 where u1_0.firstname=?
2023-03-28 09:05:55,297 TRACE              org.hibernate.orm.jdbc.bind:  28 - binding parameter [1] as [VARCHAR] - [Bilbo]

This while seeing the hint applied (by design) of:

Screen Shot 2023-03-28 at 9 03 13 AM

All that being said, this is a JPA hint, which means it's not a hard requirement that the persistence provider support this hint. I'm trying to dig in and see if we're doing the hint wrong, or if in reality, these persistence providers simply don't honor it.

gregturn avatar Mar 28 '23 14:03 gregturn

I also flipped to MySQLContainer, and got the same results.

2023-03-28 09:19:12,231 DEBUG                        org.hibernate.SQL: 128 - select u1_0.id,u1_0.DTYPE,u1_0.active,u1_0.city,u1_0.country,u1_0.streetName,u1_0.streetNo,u1_0.age,u1_0.binaryData,u1_0.createdAt,u1_0.dateOfBirth,u1_0.emailAddress,u1_0.firstname,u1_0.lastname,u1_0.manager_id from SD_User u1_0 where u1_0.firstname=?
2023-03-28 09:19:12,232 TRACE              org.hibernate.orm.jdbc.bind:  28 - binding parameter [1] as [VARCHAR] - [Bilbo]

gregturn avatar Mar 28 '23 14:03 gregturn

Query by Example uses specifications that select the Root. For projections we need to resort to tuple queries, including a proper selection:

CriteriaQuery<Tuple> query = criteriaBuilder.createTupleQuery()

query.select(criteriaBuilder.tuple(employee.get(Employee_.name), employee.get(Employee_.salary)));

Related: #487

mp911de avatar Jul 13 '23 13:07 mp911de