Fix COUNT/EXISTS projections for entities without an identifier.
We now issue a COUNT(1) respective SELECT 1 for COUNT queries and EXISTS queries for entities that do not specify an identifier. Previously these query projections could fail because of empty select lists. Closes #773
This commit is contained in:
@@ -327,7 +327,7 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw
|
|||||||
|
|
||||||
Expression countExpression = entity.hasIdProperty()
|
Expression countExpression = entity.hasIdProperty()
|
||||||
? table.column(entity.getRequiredIdProperty().getColumnName())
|
? table.column(entity.getRequiredIdProperty().getColumnName())
|
||||||
: Expressions.asterisk();
|
: Expressions.just("1");
|
||||||
return spec.withProjection(Functions.count(countExpression));
|
return spec.withProjection(Functions.count(countExpression));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -362,13 +362,14 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw
|
|||||||
RelationalPersistentEntity<?> entity = getRequiredEntity(entityClass);
|
RelationalPersistentEntity<?> entity = getRequiredEntity(entityClass);
|
||||||
StatementMapper statementMapper = dataAccessStrategy.getStatementMapper().forType(entityClass);
|
StatementMapper statementMapper = dataAccessStrategy.getStatementMapper().forType(entityClass);
|
||||||
|
|
||||||
SqlIdentifier columnName = entity.hasIdProperty() ? entity.getRequiredIdProperty().getColumnName()
|
StatementMapper.SelectSpec selectSpec = statementMapper.createSelect(tableName).limit(1);
|
||||||
: SqlIdentifier.unquoted("*");
|
if (entity.hasIdProperty()) {
|
||||||
|
selectSpec = selectSpec //
|
||||||
|
.withProjection(entity.getRequiredIdProperty().getColumnName());
|
||||||
|
|
||||||
StatementMapper.SelectSpec selectSpec = statementMapper //
|
} else {
|
||||||
.createSelect(tableName) //
|
selectSpec = selectSpec.withProjection(Expressions.just("1"));
|
||||||
.withProjection(columnName) //
|
}
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
Optional<CriteriaDefinition> criteria = query.getCriteria();
|
Optional<CriteriaDefinition> criteria = query.getCriteria();
|
||||||
if (criteria.isPresent()) {
|
if (criteria.isPresent()) {
|
||||||
|
|||||||
@@ -153,7 +153,8 @@ public class QueryMapper {
|
|||||||
*/
|
*/
|
||||||
public Expression getMappedObject(Expression expression, @Nullable RelationalPersistentEntity<?> entity) {
|
public Expression getMappedObject(Expression expression, @Nullable RelationalPersistentEntity<?> entity) {
|
||||||
|
|
||||||
if (entity == null || expression instanceof AsteriskFromTable) {
|
if (entity == null || expression instanceof AsteriskFromTable
|
||||||
|
|| expression instanceof Expressions.SimpleExpression) {
|
||||||
return expression;
|
return expression;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -154,26 +154,22 @@ class R2dbcQueryCreator extends RelationalQueryCreator<PreparedOperation<?>> {
|
|||||||
for (String projectedProperty : projectedProperties) {
|
for (String projectedProperty : projectedProperties) {
|
||||||
|
|
||||||
RelationalPersistentProperty property = entity.getPersistentProperty(projectedProperty);
|
RelationalPersistentProperty property = entity.getPersistentProperty(projectedProperty);
|
||||||
Column column = table.column(property != null ? property.getColumnName() : SqlIdentifier.unquoted(projectedProperty));
|
Column column = table
|
||||||
|
.column(property != null ? property.getColumnName() : SqlIdentifier.unquoted(projectedProperty));
|
||||||
expressions.add(column);
|
expressions.add(column);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (tree.isExistsProjection()) {
|
} else if (tree.isExistsProjection() || tree.isCountProjection()) {
|
||||||
|
|
||||||
expressions = dataAccessStrategy.getIdentifierColumns(entityToRead).stream()
|
|
||||||
.map(table::column)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
} else if (tree.isCountProjection()) {
|
|
||||||
|
|
||||||
Expression countExpression = entityMetadata.getTableEntity().hasIdProperty()
|
Expression countExpression = entityMetadata.getTableEntity().hasIdProperty()
|
||||||
? table.column(entityMetadata.getTableEntity().getRequiredIdProperty().getColumnName())
|
? table.column(entityMetadata.getTableEntity().getRequiredIdProperty().getColumnName())
|
||||||
: Expressions.asterisk();
|
: Expressions.just("1");
|
||||||
|
|
||||||
expressions = Collections.singletonList(Functions.count(countExpression));
|
expressions = Collections
|
||||||
|
.singletonList(tree.isCountProjection() ? Functions.count(countExpression) : countExpression);
|
||||||
} else {
|
} else {
|
||||||
expressions = dataAccessStrategy.getAllColumns(entityToRead).stream()
|
expressions = dataAccessStrategy.getAllColumns(entityToRead).stream().map(table::column)
|
||||||
.map(table::column)
|
.collect(Collectors.toList());
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return expressions.toArray(new Expression[0]);
|
return expressions.toArray(new Expression[0]);
|
||||||
|
|||||||
@@ -90,8 +90,7 @@ public class R2dbcEntityTemplateUnitTests {
|
|||||||
|
|
||||||
MockRowMetadata metadata = MockRowMetadata.builder()
|
MockRowMetadata metadata = MockRowMetadata.builder()
|
||||||
.columnMetadata(MockColumnMetadata.builder().name("name").type(R2dbcType.VARCHAR).build()).build();
|
.columnMetadata(MockColumnMetadata.builder().name("name").type(R2dbcType.VARCHAR).build()).build();
|
||||||
MockResult result = MockResult.builder()
|
MockResult result = MockResult.builder().row(MockRow.builder().identified(0, Long.class, 1L).build()).build();
|
||||||
.row(MockRow.builder().identified(0, Long.class, 1L).build()).build();
|
|
||||||
|
|
||||||
recorder.addStubbing(s -> s.startsWith("SELECT"), result);
|
recorder.addStubbing(s -> s.startsWith("SELECT"), result);
|
||||||
|
|
||||||
@@ -109,10 +108,7 @@ public class R2dbcEntityTemplateUnitTests {
|
|||||||
@Test // gh-469
|
@Test // gh-469
|
||||||
void shouldProjectExistsResult() {
|
void shouldProjectExistsResult() {
|
||||||
|
|
||||||
MockRowMetadata metadata = MockRowMetadata.builder()
|
MockResult result = MockResult.builder().row(MockRow.builder().identified(0, Object.class, null).build()).build();
|
||||||
.columnMetadata(MockColumnMetadata.builder().name("name").type(R2dbcType.VARCHAR).build()).build();
|
|
||||||
MockResult result = MockResult.builder()
|
|
||||||
.row(MockRow.builder().identified(0, Object.class, null).build()).build();
|
|
||||||
|
|
||||||
recorder.addStubbing(s -> s.startsWith("SELECT"), result);
|
recorder.addStubbing(s -> s.startsWith("SELECT"), result);
|
||||||
|
|
||||||
@@ -124,13 +120,36 @@ public class R2dbcEntityTemplateUnitTests {
|
|||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test // gh-773
|
||||||
|
void shouldProjectExistsResultWithoutId() {
|
||||||
|
|
||||||
|
MockResult result = MockResult.builder().row(MockRow.builder().identified(0, Object.class, null).build()).build();
|
||||||
|
|
||||||
|
recorder.addStubbing(s -> s.startsWith("SELECT 1"), result);
|
||||||
|
|
||||||
|
entityTemplate.select(WithoutId.class).exists() //
|
||||||
|
.as(StepVerifier::create) //
|
||||||
|
.expectNext(true).verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test // gh-773
|
||||||
|
void shouldProjectCountResultWithoutId() {
|
||||||
|
|
||||||
|
MockResult result = MockResult.builder().row(MockRow.builder().identified(0, Long.class, 1L).build()).build();
|
||||||
|
|
||||||
|
recorder.addStubbing(s -> s.startsWith("SELECT COUNT(1)"), result);
|
||||||
|
|
||||||
|
entityTemplate.select(WithoutId.class).count() //
|
||||||
|
.as(StepVerifier::create) //
|
||||||
|
.expectNext(1L).verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
@Test // gh-469
|
@Test // gh-469
|
||||||
void shouldExistsByCriteria() {
|
void shouldExistsByCriteria() {
|
||||||
|
|
||||||
MockRowMetadata metadata = MockRowMetadata.builder()
|
MockRowMetadata metadata = MockRowMetadata.builder()
|
||||||
.columnMetadata(MockColumnMetadata.builder().name("name").type(R2dbcType.VARCHAR).build()).build();
|
.columnMetadata(MockColumnMetadata.builder().name("name").type(R2dbcType.VARCHAR).build()).build();
|
||||||
MockResult result = MockResult.builder()
|
MockResult result = MockResult.builder().row(MockRow.builder().identified(0, Long.class, 1L).build()).build();
|
||||||
.row(MockRow.builder().identified(0, Long.class, 1L).build()).build();
|
|
||||||
|
|
||||||
recorder.addStubbing(s -> s.startsWith("SELECT"), result);
|
recorder.addStubbing(s -> s.startsWith("SELECT"), result);
|
||||||
|
|
||||||
@@ -480,6 +499,12 @@ public class R2dbcEntityTemplateUnitTests {
|
|||||||
Parameter.from("before-save"));
|
Parameter.from("before-save"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Value
|
||||||
|
static class WithoutId {
|
||||||
|
|
||||||
|
String name;
|
||||||
|
}
|
||||||
|
|
||||||
@Value
|
@Value
|
||||||
@With
|
@With
|
||||||
static class Person {
|
static class Person {
|
||||||
|
|||||||
@@ -77,8 +77,6 @@ class PartTreeR2dbcQueryUnitTests {
|
|||||||
".age", ".active" };
|
".age", ".active" };
|
||||||
private static final String[] ALL_FIELDS_ARRAY_PREFIXED = Arrays.stream(ALL_FIELDS_ARRAY).map(f -> TABLE + f)
|
private static final String[] ALL_FIELDS_ARRAY_PREFIXED = Arrays.stream(ALL_FIELDS_ARRAY).map(f -> TABLE + f)
|
||||||
.toArray(String[]::new);
|
.toArray(String[]::new);
|
||||||
private static final String ALL_FIELDS = String.join(", ", ALL_FIELDS_ARRAY_PREFIXED);
|
|
||||||
private static final String DISTINCT = "DISTINCT";
|
|
||||||
|
|
||||||
@Mock ConnectionFactory connectionFactory;
|
@Mock ConnectionFactory connectionFactory;
|
||||||
@Mock R2dbcConverter r2dbcConverter;
|
@Mock R2dbcConverter r2dbcConverter;
|
||||||
@@ -698,6 +696,32 @@ class PartTreeR2dbcQueryUnitTests {
|
|||||||
.where(TABLE + ".first_name = $1");
|
.where(TABLE + ".first_name = $1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test // GH-773
|
||||||
|
void createsQueryWithoutIdForCountProjection() throws Exception {
|
||||||
|
|
||||||
|
R2dbcQueryMethod queryMethod = getQueryMethod(WithoutIdRepository.class, "countByFirstName", String.class);
|
||||||
|
PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, operations, r2dbcConverter, dataAccessStrategy);
|
||||||
|
PreparedOperation<?> query = createQuery(queryMethod, r2dbcQuery, "John");
|
||||||
|
|
||||||
|
PreparedOperationAssert.assertThat(query) //
|
||||||
|
.selects("COUNT(1)") //
|
||||||
|
.from(TABLE) //
|
||||||
|
.where(TABLE + ".first_name = $1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test // GH-773
|
||||||
|
void createsQueryWithoutIdForExistsProjection() throws Exception {
|
||||||
|
|
||||||
|
R2dbcQueryMethod queryMethod = getQueryMethod(WithoutIdRepository.class, "existsByFirstName", String.class);
|
||||||
|
PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, operations, r2dbcConverter, dataAccessStrategy);
|
||||||
|
PreparedOperation<?> query = createQuery(queryMethod, r2dbcQuery, "John");
|
||||||
|
|
||||||
|
PreparedOperationAssert.assertThat(query) //
|
||||||
|
.selects("1") //
|
||||||
|
.from(TABLE) //
|
||||||
|
.where(TABLE + ".first_name = $1 LIMIT 1");
|
||||||
|
}
|
||||||
|
|
||||||
private PreparedOperation<?> createQuery(R2dbcQueryMethod queryMethod, PartTreeR2dbcQuery r2dbcQuery,
|
private PreparedOperation<?> createQuery(R2dbcQueryMethod queryMethod, PartTreeR2dbcQuery r2dbcQuery,
|
||||||
Object... parameters) {
|
Object... parameters) {
|
||||||
return createQuery(r2dbcQuery, getAccessor(queryMethod, parameters));
|
return createQuery(r2dbcQuery, getAccessor(queryMethod, parameters));
|
||||||
@@ -709,8 +733,13 @@ class PartTreeR2dbcQueryUnitTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private R2dbcQueryMethod getQueryMethod(String methodName, Class<?>... parameterTypes) throws Exception {
|
private R2dbcQueryMethod getQueryMethod(String methodName, Class<?>... parameterTypes) throws Exception {
|
||||||
Method method = UserRepository.class.getMethod(methodName, parameterTypes);
|
return getQueryMethod(UserRepository.class, methodName, parameterTypes);
|
||||||
return new R2dbcQueryMethod(method, new DefaultRepositoryMetadata(UserRepository.class),
|
}
|
||||||
|
|
||||||
|
private R2dbcQueryMethod getQueryMethod(Class<?> repository, String methodName, Class<?>... parameterTypes)
|
||||||
|
throws Exception {
|
||||||
|
Method method = repository.getMethod(methodName, parameterTypes);
|
||||||
|
return new R2dbcQueryMethod(method, new DefaultRepositoryMetadata(repository),
|
||||||
new SpelAwareProxyProjectionFactory(), mappingContext);
|
new SpelAwareProxyProjectionFactory(), mappingContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -887,6 +916,13 @@ class PartTreeR2dbcQueryUnitTests {
|
|||||||
Mono<Long> countByFirstName(String firstName);
|
Mono<Long> countByFirstName(String firstName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface WithoutIdRepository extends Repository<WithoutId, Long> {
|
||||||
|
|
||||||
|
Mono<Boolean> existsByFirstName(String firstName);
|
||||||
|
|
||||||
|
Mono<Long> countByFirstName(String firstName);
|
||||||
|
}
|
||||||
|
|
||||||
@Table("users")
|
@Table("users")
|
||||||
@Data
|
@Data
|
||||||
private static class User {
|
private static class User {
|
||||||
@@ -899,6 +935,13 @@ class PartTreeR2dbcQueryUnitTests {
|
|||||||
private Boolean active;
|
private Boolean active;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Table("users")
|
||||||
|
@Data
|
||||||
|
private static class WithoutId {
|
||||||
|
|
||||||
|
private String firstName;
|
||||||
|
}
|
||||||
|
|
||||||
interface UserProjection {
|
interface UserProjection {
|
||||||
|
|
||||||
String getFirstName();
|
String getFirstName();
|
||||||
|
|||||||
Reference in New Issue
Block a user