Skip to content

Commit 5c8dc09

Browse files
committed
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
1 parent dd2d94e commit 5c8dc09

File tree

5 files changed

+98
-32
lines changed

5 files changed

+98
-32
lines changed

src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java

+8-7
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ Mono<Long> doCount(Query query, Class<?> entityClass, SqlIdentifier tableName) {
327327

328328
Expression countExpression = entity.hasIdProperty()
329329
? table.column(entity.getRequiredIdProperty().getColumnName())
330-
: Expressions.asterisk();
330+
: Expressions.just("1");
331331
return spec.withProjection(Functions.count(countExpression));
332332
});
333333

@@ -362,13 +362,14 @@ Mono<Boolean> doExists(Query query, Class<?> entityClass, SqlIdentifier tableNam
362362
RelationalPersistentEntity<?> entity = getRequiredEntity(entityClass);
363363
StatementMapper statementMapper = dataAccessStrategy.getStatementMapper().forType(entityClass);
364364

365-
SqlIdentifier columnName = entity.hasIdProperty() ? entity.getRequiredIdProperty().getColumnName()
366-
: SqlIdentifier.unquoted("*");
365+
StatementMapper.SelectSpec selectSpec = statementMapper.createSelect(tableName).limit(1);
366+
if (entity.hasIdProperty()) {
367+
selectSpec = selectSpec //
368+
.withProjection(entity.getRequiredIdProperty().getColumnName());
367369

368-
StatementMapper.SelectSpec selectSpec = statementMapper //
369-
.createSelect(tableName) //
370-
.withProjection(columnName) //
371-
.limit(1);
370+
} else {
371+
selectSpec = selectSpec.withProjection(Expressions.just("1"));
372+
}
372373

373374
Optional<CriteriaDefinition> criteria = query.getCriteria();
374375
if (criteria.isPresent()) {

src/main/java/org/springframework/data/r2dbc/query/QueryMapper.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,8 @@ public List<OrderByField> getMappedSort(Table table, Sort sort, @Nullable Relati
153153
*/
154154
public Expression getMappedObject(Expression expression, @Nullable RelationalPersistentEntity<?> entity) {
155155

156-
if (entity == null || expression instanceof AsteriskFromTable) {
156+
if (entity == null || expression instanceof AsteriskFromTable
157+
|| expression instanceof Expressions.SimpleExpression) {
157158
return expression;
158159
}
159160

src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryCreator.java

+8-12
Original file line numberDiff line numberDiff line change
@@ -154,26 +154,22 @@ private Expression[] getSelectProjection() {
154154
for (String projectedProperty : projectedProperties) {
155155

156156
RelationalPersistentProperty property = entity.getPersistentProperty(projectedProperty);
157-
Column column = table.column(property != null ? property.getColumnName() : SqlIdentifier.unquoted(projectedProperty));
157+
Column column = table
158+
.column(property != null ? property.getColumnName() : SqlIdentifier.unquoted(projectedProperty));
158159
expressions.add(column);
159160
}
160161

161-
} else if (tree.isExistsProjection()) {
162-
163-
expressions = dataAccessStrategy.getIdentifierColumns(entityToRead).stream()
164-
.map(table::column)
165-
.collect(Collectors.toList());
166-
} else if (tree.isCountProjection()) {
162+
} else if (tree.isExistsProjection() || tree.isCountProjection()) {
167163

168164
Expression countExpression = entityMetadata.getTableEntity().hasIdProperty()
169165
? table.column(entityMetadata.getTableEntity().getRequiredIdProperty().getColumnName())
170-
: Expressions.asterisk();
166+
: Expressions.just("1");
171167

172-
expressions = Collections.singletonList(Functions.count(countExpression));
168+
expressions = Collections
169+
.singletonList(tree.isCountProjection() ? Functions.count(countExpression) : countExpression);
173170
} else {
174-
expressions = dataAccessStrategy.getAllColumns(entityToRead).stream()
175-
.map(table::column)
176-
.collect(Collectors.toList());
171+
expressions = dataAccessStrategy.getAllColumns(entityToRead).stream().map(table::column)
172+
.collect(Collectors.toList());
177173
}
178174

179175
return expressions.toArray(new Expression[0]);

src/test/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplateUnitTests.java

+33-8
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,7 @@ void shouldCountBy() {
9090

9191
MockRowMetadata metadata = MockRowMetadata.builder()
9292
.columnMetadata(MockColumnMetadata.builder().name("name").type(R2dbcType.VARCHAR).build()).build();
93-
MockResult result = MockResult.builder()
94-
.row(MockRow.builder().identified(0, Long.class, 1L).build()).build();
93+
MockResult result = MockResult.builder().row(MockRow.builder().identified(0, Long.class, 1L).build()).build();
9594

9695
recorder.addStubbing(s -> s.startsWith("SELECT"), result);
9796

@@ -109,10 +108,7 @@ void shouldCountBy() {
109108
@Test // gh-469
110109
void shouldProjectExistsResult() {
111110

112-
MockRowMetadata metadata = MockRowMetadata.builder()
113-
.columnMetadata(MockColumnMetadata.builder().name("name").type(R2dbcType.VARCHAR).build()).build();
114-
MockResult result = MockResult.builder()
115-
.row(MockRow.builder().identified(0, Object.class, null).build()).build();
111+
MockResult result = MockResult.builder().row(MockRow.builder().identified(0, Object.class, null).build()).build();
116112

117113
recorder.addStubbing(s -> s.startsWith("SELECT"), result);
118114

@@ -124,13 +120,36 @@ void shouldProjectExistsResult() {
124120
.verifyComplete();
125121
}
126122

123+
@Test // gh-773
124+
void shouldProjectExistsResultWithoutId() {
125+
126+
MockResult result = MockResult.builder().row(MockRow.builder().identified(0, Object.class, null).build()).build();
127+
128+
recorder.addStubbing(s -> s.startsWith("SELECT 1"), result);
129+
130+
entityTemplate.select(WithoutId.class).exists() //
131+
.as(StepVerifier::create) //
132+
.expectNext(true).verifyComplete();
133+
}
134+
135+
@Test // gh-773
136+
void shouldProjectCountResultWithoutId() {
137+
138+
MockResult result = MockResult.builder().row(MockRow.builder().identified(0, Long.class, 1L).build()).build();
139+
140+
recorder.addStubbing(s -> s.startsWith("SELECT COUNT(1)"), result);
141+
142+
entityTemplate.select(WithoutId.class).count() //
143+
.as(StepVerifier::create) //
144+
.expectNext(1L).verifyComplete();
145+
}
146+
127147
@Test // gh-469
128148
void shouldExistsByCriteria() {
129149

130150
MockRowMetadata metadata = MockRowMetadata.builder()
131151
.columnMetadata(MockColumnMetadata.builder().name("name").type(R2dbcType.VARCHAR).build()).build();
132-
MockResult result = MockResult.builder()
133-
.row(MockRow.builder().identified(0, Long.class, 1L).build()).build();
152+
MockResult result = MockResult.builder().row(MockRow.builder().identified(0, Long.class, 1L).build()).build();
134153

135154
recorder.addStubbing(s -> s.startsWith("SELECT"), result);
136155

@@ -480,6 +499,12 @@ void updateShouldInvokeCallback() {
480499
Parameter.from("before-save"));
481500
}
482501

502+
@Value
503+
static class WithoutId {
504+
505+
String name;
506+
}
507+
483508
@Value
484509
@With
485510
static class Person {

src/test/java/org/springframework/data/r2dbc/repository/query/PartTreeR2dbcQueryUnitTests.java

+47-4
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,6 @@ class PartTreeR2dbcQueryUnitTests {
7777
".age", ".active" };
7878
private static final String[] ALL_FIELDS_ARRAY_PREFIXED = Arrays.stream(ALL_FIELDS_ARRAY).map(f -> TABLE + f)
7979
.toArray(String[]::new);
80-
private static final String ALL_FIELDS = String.join(", ", ALL_FIELDS_ARRAY_PREFIXED);
81-
private static final String DISTINCT = "DISTINCT";
8280

8381
@Mock ConnectionFactory connectionFactory;
8482
@Mock R2dbcConverter r2dbcConverter;
@@ -698,6 +696,32 @@ void createsQueryForCountProjection() throws Exception {
698696
.where(TABLE + ".first_name = $1");
699697
}
700698

699+
@Test // GH-773
700+
void createsQueryWithoutIdForCountProjection() throws Exception {
701+
702+
R2dbcQueryMethod queryMethod = getQueryMethod(WithoutIdRepository.class, "countByFirstName", String.class);
703+
PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, operations, r2dbcConverter, dataAccessStrategy);
704+
PreparedOperation<?> query = createQuery(queryMethod, r2dbcQuery, "John");
705+
706+
PreparedOperationAssert.assertThat(query) //
707+
.selects("COUNT(1)") //
708+
.from(TABLE) //
709+
.where(TABLE + ".first_name = $1");
710+
}
711+
712+
@Test // GH-773
713+
void createsQueryWithoutIdForExistsProjection() throws Exception {
714+
715+
R2dbcQueryMethod queryMethod = getQueryMethod(WithoutIdRepository.class, "existsByFirstName", String.class);
716+
PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, operations, r2dbcConverter, dataAccessStrategy);
717+
PreparedOperation<?> query = createQuery(queryMethod, r2dbcQuery, "John");
718+
719+
PreparedOperationAssert.assertThat(query) //
720+
.selects("1") //
721+
.from(TABLE) //
722+
.where(TABLE + ".first_name = $1 LIMIT 1");
723+
}
724+
701725
private PreparedOperation<?> createQuery(R2dbcQueryMethod queryMethod, PartTreeR2dbcQuery r2dbcQuery,
702726
Object... parameters) {
703727
return createQuery(r2dbcQuery, getAccessor(queryMethod, parameters));
@@ -709,8 +733,13 @@ private PreparedOperation<?> createQuery(PartTreeR2dbcQuery r2dbcQuery,
709733
}
710734

711735
private R2dbcQueryMethod getQueryMethod(String methodName, Class<?>... parameterTypes) throws Exception {
712-
Method method = UserRepository.class.getMethod(methodName, parameterTypes);
713-
return new R2dbcQueryMethod(method, new DefaultRepositoryMetadata(UserRepository.class),
736+
return getQueryMethod(UserRepository.class, methodName, parameterTypes);
737+
}
738+
739+
private R2dbcQueryMethod getQueryMethod(Class<?> repository, String methodName, Class<?>... parameterTypes)
740+
throws Exception {
741+
Method method = repository.getMethod(methodName, parameterTypes);
742+
return new R2dbcQueryMethod(method, new DefaultRepositoryMetadata(repository),
714743
new SpelAwareProxyProjectionFactory(), mappingContext);
715744
}
716745

@@ -887,6 +916,13 @@ interface UserRepository extends Repository<User, Long> {
887916
Mono<Long> countByFirstName(String firstName);
888917
}
889918

919+
interface WithoutIdRepository extends Repository<WithoutId, Long> {
920+
921+
Mono<Boolean> existsByFirstName(String firstName);
922+
923+
Mono<Long> countByFirstName(String firstName);
924+
}
925+
890926
@Table("users")
891927
@Data
892928
private static class User {
@@ -899,6 +935,13 @@ private static class User {
899935
private Boolean active;
900936
}
901937

938+
@Table("users")
939+
@Data
940+
private static class WithoutId {
941+
942+
private String firstName;
943+
}
944+
902945
interface UserProjection {
903946

904947
String getFirstName();

0 commit comments

Comments
 (0)