Skip to content

Commit a9f653b

Browse files
committed
Support of QueryByExampleExecutor#count(...).
This commit introduces the count by example method `count(...)` to spring-data-jdbc. MyBatis implementation is missing since I do not have the knowledge for this. Related tickets spring-projects#1192
1 parent 03088ef commit a9f653b

File tree

11 files changed

+197
-16
lines changed

11 files changed

+197
-16
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java

+8
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,12 @@ public interface JdbcAggregateOperations {
185185
* @return {@literal true} if the object exists.
186186
*/
187187
<T> boolean exists(Example<T> example);
188+
189+
/**
190+
* Counts the number of aggregates of a given type that match the given <code>example</code>.
191+
*
192+
* @param example the example to match.
193+
* @return the number of instances stored in the database. Guaranteed to be not {@code null}.
194+
*/
195+
<T> long count(Example<T> example);
188196
}

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java

+7
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,13 @@ public <T> boolean exists(Example<T> example) {
287287
return accessStrategy.exists(query, probeType);
288288
}
289289

290+
@Override
291+
public <T> long count(Example<T> example) {
292+
Query query = this.exampleMapper.getMappedExample(example);
293+
Class<T> probeType = example.getProbeType();
294+
return accessStrategy.count(query, probeType);
295+
}
296+
290297
/*
291298
* (non-Javadoc)
292299
* @see org.springframework.data.jdbc.core.JdbcAggregateOperations#findAll(java.lang.Class)

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CascadingDataAccessStrategy.java

+5
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,11 @@ public <T> boolean exists(Query query, Class<T> probeType) {
226226
return collect(das -> das.exists(query, probeType));
227227
}
228228

229+
@Override
230+
public <T> long count(Query query, Class<T> probeType) {
231+
return collect(das -> das.count(query, probeType));
232+
}
233+
229234
private <T> T collect(Function<DataAccessStrategy, T> function) {
230235

231236
// Keep <T> as Eclipse fails to compile if <> is used.

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java

+9
Original file line numberDiff line numberDiff line change
@@ -258,4 +258,13 @@ Iterable<Object> findAllByPath(Identifier identifier,
258258
* @return {@literal true} if the object exists.
259259
*/
260260
<T> boolean exists(Query query, Class<T> probeType);
261+
262+
/**
263+
* Counts the rows in the table representing the given probe type, that match the given <code>query</code>.
264+
*
265+
* @param probeType the probe type for which to count the elements. Must not be {@code null}.
266+
* @param query the query which elements have to match.
267+
* @return the count. Guaranteed to be not {@code null}.
268+
*/
269+
<T> long count(Query query, Class<T> probeType);
261270
}

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java

+12
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,18 @@ public <T> boolean exists(Query query, Class<T> probeType) {
469469
return result;
470470
}
471471

472+
@Override
473+
public <T> long count(Query query, Class<T> probeType) {
474+
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
475+
String sqlQuery = sql(probeType).countByQuery(query, parameterSource);
476+
477+
Long result = operations.queryForObject(sqlQuery, parameterSource, Long.class);
478+
479+
Assert.notNull(result, "The result of a count query must not be null.");
480+
481+
return result;
482+
}
483+
472484
private <S, T> SqlIdentifierParameterSource getParameterSource(@Nullable S instance,
473485
RelationalPersistentEntity<S> persistentEntity, String prefix,
474486
Predicate<RelationalPersistentProperty> skipProperty, IdentifierProcessing identifierProcessing) {

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DelegatingDataAccessStrategy.java

+5
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,11 @@ public <T> boolean exists(Query query, Class<T> probeType) {
222222
return delegate.exists(query, probeType);
223223
}
224224

225+
@Override
226+
public <T> long count(Query query, Class<T> probeType) {
227+
return delegate.count(query, probeType);
228+
}
229+
225230
/**
226231
* Must be called exactly once before calling any of the other methods.
227232
*

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java

+45-8
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,25 @@ public String selectByQuery(Query query, MapSqlParameterSource parameterSource)
745745
return render(select);
746746
}
747747

748+
/**
749+
* Constructs a single sql query that performs select count based on the provided query for checking existence.
750+
* Additional the bindings for the where clause are stored after execution into the <code>parameterSource</code>
751+
*
752+
* @param query the query to base the select on. Must not be null
753+
* @param parameterSource the source for holding the bindings
754+
* @return a non null query string.
755+
*/
756+
public String existsByQuery(Query query, MapSqlParameterSource parameterSource) {
757+
758+
Expression idColumn = getIdColumn();
759+
SelectBuilder.SelectJoin baseSelect = getSelectCountWithExpression(idColumn);
760+
761+
Select select = applyQueryOnSelect(query, parameterSource, (SelectBuilder.SelectWhere) baseSelect) //
762+
.build();
763+
764+
return render(select);
765+
}
766+
748767
/**
749768
* Constructs a single sql query that performs select count based on the provided query. Additional the bindings for
750769
* the where clause are stored after execution into the <code>parameterSource</code>
@@ -753,11 +772,33 @@ public String selectByQuery(Query query, MapSqlParameterSource parameterSource)
753772
* @param parameterSource the source for holding the bindings
754773
* @return a non null query string.
755774
*/
756-
public String existsByQuery(Query query, MapSqlParameterSource parameterSource) {
757-
Table table = getTable();
775+
public String countByQuery(Query query, MapSqlParameterSource parameterSource) {
776+
777+
Expression countExpression = Expressions.just("1");
778+
SelectBuilder.SelectJoin baseSelect = getSelectCountWithExpression(countExpression);
758779

780+
Select select = applyQueryOnSelect(query, parameterSource, (SelectBuilder.SelectWhere) baseSelect) //
781+
.build();
782+
783+
return render(select);
784+
}
785+
786+
/**
787+
* Generates a {@link org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin} with a
788+
* <code>COUNT(...)</code> where the <code>countExpressions</code> are the parameters of the count.
789+
*
790+
* @param countExpressions the expression to use as count parameter.
791+
* @return a non-null {@link org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin} that joins all the
792+
* columns and has only a count in the projection of the select.
793+
*/
794+
private SelectBuilder.SelectJoin getSelectCountWithExpression(Expression... countExpressions) {
795+
796+
Assert.notNull(countExpressions, "countExpressions must not be null");
797+
Assert.state(countExpressions.length >= 1, "countExpressions must contain at least one expression");
798+
799+
Table table = getTable();
759800
SelectBuilder.SelectFromAndJoin selectBuilder = StatementBuilder //
760-
.select(Functions.count(getIdColumn())) //
801+
.select(Functions.count(countExpressions)) //
761802
.from(table);//
762803

763804
SelectBuilder.SelectJoin baseSelect = selectBuilder;
@@ -774,11 +815,7 @@ public String existsByQuery(Query query, MapSqlParameterSource parameterSource)
774815
baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId);
775816
}
776817
}
777-
778-
Select select = applyQueryOnSelect(query, parameterSource, (SelectBuilder.SelectWhere) baseSelect) //
779-
.build();
780-
781-
return render(select);
818+
return baseSelect;
782819
}
783820

784821
private SelectBuilder.SelectOrdered applyQueryOnSelect(Query query, MapSqlParameterSource parameterSource,

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -381,12 +381,20 @@ public <T> Iterable<T> select(Query query, Class<T> probeType) {
381381
return null;
382382
}
383383

384-
@Override public <T> boolean exists(Query query, Class<T> probeType) {
384+
@Override
385+
public <T> boolean exists(Query query, Class<T> probeType) {
385386
// TODO: DIEGO find help for this one
386387
// I have zero MyBatis knowledge.
387388
return false;
388389
}
389390

391+
@Override
392+
public <T> long count(Query query, Class<T> probeType) {
393+
// TODO: DIEGO find help for this one
394+
// I have zero MyBatis knowledge.
395+
return 0;
396+
}
397+
390398
/*
391399
* (non-Javadoc)
392400
* @see org.springframework.data.jdbc.core.DataAccessStrategy#count(java.lang.Class)

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java

+3-7
Original file line numberDiff line numberDiff line change
@@ -220,13 +220,9 @@ public <S extends T> Page<S> findAll(Example<S> example, Pageable pageable) {
220220

221221
@Override
222222
public <S extends T> long count(Example<S> example) {
223-
/*Assert.notNull(example, "Example must not be null!");
224-
225-
Query query = this.exampleMapper.getMappedExample(example);
226-
227-
return this.entityOperations.count(query, example.getProbeType());*/
228-
// TODO: impl
229-
return 0;
223+
Assert.notNull(example, "Example must not be null!");
224+
225+
return this.entityOperations.count(example);
230226
}
231227

232228
@Override

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java

+22
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,28 @@ void existsByQuerySimpleValidTest() {
761761
.containsOnly(entry("x_name", probe.name));
762762
}
763763

764+
@Test
765+
void countByQuerySimpleValidTest() {
766+
final SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class);
767+
768+
DummyEntity probe = new DummyEntity();
769+
probe.name = "Diego";
770+
771+
Criteria criteria = Criteria.where("name").is(probe.name);
772+
Query query = Query.query(criteria);
773+
774+
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
775+
776+
String generatedSQL = sqlGenerator.countByQuery(query, parameterSource);
777+
assertThat(generatedSQL) //
778+
.isNotNull() //
779+
.containsIgnoringCase("COUNT(1)") //
780+
.contains(":x_name");
781+
782+
assertThat(parameterSource.getValues()) //
783+
.containsOnly(entry("x_name", probe.name));
784+
}
785+
764786
private SqlIdentifier getAlias(Object maybeAliased) {
765787

766788
if (maybeAliased instanceof Aliased) {

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java

+72
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,78 @@ void existsByExampleComplex() {
765765
assertThat(exists).isTrue();
766766
}
767767

768+
@Test
769+
void countByExampleShouldGetOne() {
770+
771+
DummyEntity dummyEntity1 = createDummyEntity();
772+
dummyEntity1.setFlag(true);
773+
774+
repository.save(dummyEntity1);
775+
776+
DummyEntity dummyEntity2 = createDummyEntity();
777+
dummyEntity2.setName("Diego");
778+
779+
repository.save(dummyEntity2);
780+
781+
Example<DummyEntity> example = Example.of(new DummyEntity("Diego"));
782+
783+
long count = repository.count(example);
784+
785+
assertThat(count).isOne();
786+
}
787+
788+
@Test
789+
void countByExampleMultipleMatchShouldGetOne() {
790+
791+
DummyEntity dummyEntity1 = createDummyEntity();
792+
repository.save(dummyEntity1);
793+
794+
DummyEntity dummyEntity2 = createDummyEntity();
795+
repository.save(dummyEntity2);
796+
797+
Example<DummyEntity> example = Example.of(createDummyEntity());
798+
799+
long count = repository.count(example);
800+
assertThat(count).isEqualTo(2);
801+
}
802+
803+
@Test
804+
void countByExampleShouldGetNone() {
805+
806+
DummyEntity dummyEntity1 = createDummyEntity();
807+
dummyEntity1.setFlag(true);
808+
809+
repository.save(dummyEntity1);
810+
811+
Example<DummyEntity> example = Example.of(new DummyEntity("NotExisting"));
812+
813+
long count = repository.count(example);
814+
815+
assertThat(count).isNotNull().isZero();
816+
}
817+
818+
@Test
819+
void countByExampleComplex() {
820+
821+
final Instant pointInTime = Instant.now().minusSeconds(10000);
822+
823+
final DummyEntity one = repository.save(createDummyEntity());
824+
825+
DummyEntity two = createDummyEntity();
826+
two.setName("Diego");
827+
two.setPointInTime(pointInTime);
828+
two = repository.save(two);
829+
830+
DummyEntity exampleEntitiy = createDummyEntity();
831+
exampleEntitiy.setName("Diego");
832+
exampleEntitiy.setPointInTime(pointInTime);
833+
834+
Example<DummyEntity> example = Example.of(exampleEntitiy);
835+
836+
long count = repository.count(example);
837+
assertThat(count).isOne();
838+
}
839+
768840
private Instant createDummyBeforeAndAfterNow() {
769841

770842
Instant now = Instant.now();

0 commit comments

Comments
 (0)