Skip to content

Commit 03088ef

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

File tree

11 files changed

+200
-18
lines changed

11 files changed

+200
-18
lines changed

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

+8
Original file line numberDiff line numberDiff line change
@@ -177,4 +177,12 @@ public interface JdbcAggregateOperations {
177177
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
178178
*/
179179
<T> Iterable<T> select(Example<T> example, Sort sort);
180+
181+
/**
182+
* Determine whether the result matches {@link Example}
183+
*
184+
* @param example must not be {@literal null}.
185+
* @return {@literal true} if the object exists.
186+
*/
187+
<T> boolean exists(Example<T> example);
180188
}

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

+11-1
Original file line numberDiff line numberDiff line change
@@ -270,13 +270,23 @@ public <T> Optional<T> selectOne(Example<T> example) {
270270
return accessStrategy.selectOne(query, probeType);
271271
}
272272

273-
@Override public <T> Iterable<T> select(Example<T> example, Sort sort) {
273+
@Override
274+
public <T> Iterable<T> select(Example<T> example, Sort sort) {
274275
Query query = this.exampleMapper.getMappedExample(example).sort(sort);
275276
Class<T> probeType = example.getProbeType();
276277

277278
return accessStrategy.select(query, probeType);
278279
}
279280

281+
@Override
282+
public <T> boolean exists(Example<T> example) {
283+
Query query = this.exampleMapper.getMappedExample(example);
284+
285+
Class<T> probeType = example.getProbeType();
286+
287+
return accessStrategy.exists(query, probeType);
288+
}
289+
280290
/*
281291
* (non-Javadoc)
282292
* @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
@@ -221,6 +221,11 @@ public <T> Iterable<T> select(Query query, Class<T> probeType) {
221221
return collect(das -> das.select(query, probeType));
222222
}
223223

224+
@Override
225+
public <T> boolean exists(Query query, Class<T> probeType) {
226+
return collect(das -> das.exists(query, probeType));
227+
}
228+
224229
private <T> T collect(Function<DataAccessStrategy, T> function) {
225230

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

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

+11-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import java.util.Map;
1919
import java.util.Optional;
2020

21-
import org.springframework.dao.DataAccessException;
2221
import org.springframework.dao.OptimisticLockingFailureException;
2322
import org.springframework.data.domain.Pageable;
2423
import org.springframework.data.domain.Sort;
@@ -235,6 +234,7 @@ Iterable<Object> findAllByPath(Identifier identifier,
235234
* Execute a {@code SELECT} query and convert the resulting item to an entity ensuring exactly one result.
236235
*
237236
* @param query must not be {@literal null}.
237+
* @param probeType the type of entities. Must not be {@code null}.
238238
* @return exactly one result or {@link Optional#empty()} if no match found.
239239
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
240240
*/
@@ -244,8 +244,18 @@ Iterable<Object> findAllByPath(Identifier identifier,
244244
* Execute a {@code SELECT} query and convert the resulting items to a {@link Iterable}.
245245
*
246246
* @param query must not be {@literal null}.
247+
* @param probeType the type of entities. Must not be {@code null}.
247248
* @return a non null list with all the matching results.
248249
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
249250
*/
250251
<T> Iterable<T> select(Query query, Class<T> probeType);
252+
253+
/**
254+
* Determine whether there is an aggregate of type <code>probeType</code> that matches the provided {@link Query}.
255+
*
256+
* @param query must not be {@literal null}.
257+
* @param probeType the type of entities. Must not be {@code null}.
258+
* @return {@literal true} if the object exists.
259+
*/
260+
<T> boolean exists(Query query, Class<T> probeType);
251261
}

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

+12
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,18 @@ public <T> Iterable<T> select(Query query, Class<T> probeType) {
457457
return operations.query(sqlQuery, parameterSource, (RowMapper<T>) getEntityRowMapper(probeType));
458458
}
459459

460+
@Override
461+
public <T> boolean exists(Query query, Class<T> probeType) {
462+
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
463+
464+
String sqlQuery = sql(probeType).existsByQuery(query, parameterSource);
465+
466+
Boolean result = operations.queryForObject(sqlQuery, parameterSource, Boolean.class);
467+
Assert.notNull(result, "The result of an exists query must not be null");
468+
469+
return result;
470+
}
471+
460472
private <S, T> SqlIdentifierParameterSource getParameterSource(@Nullable S instance,
461473
RelationalPersistentEntity<S> persistentEntity, String prefix,
462474
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
@@ -217,6 +217,11 @@ public <T> Iterable<T> select(Query query, Class<T> probeType) {
217217
return delegate.select(query, probeType);
218218
}
219219

220+
@Override
221+
public <T> boolean exists(Query query, Class<T> probeType) {
222+
return delegate.exists(query, probeType);
223+
}
224+
220225
/**
221226
* Must be called exactly once before calling any of the other methods.
222227
*

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

+48-8
Original file line numberDiff line numberDiff line change
@@ -737,10 +737,55 @@ public String selectByQuery(Query query, MapSqlParameterSource parameterSource)
737737

738738
Assert.notNull(parameterSource, "parameterSource must not be null");
739739

740-
Table table = Table.create(this.entity.getTableName());
741-
742740
SelectBuilder.SelectWhere selectBuilder = selectBuilder();
743741

742+
Select select = applyQueryOnSelect(query, parameterSource, selectBuilder) //
743+
.build();
744+
745+
return render(select);
746+
}
747+
748+
/**
749+
* Constructs a single sql query that performs select count based on the provided query. Additional the bindings for
750+
* 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+
Table table = getTable();
758+
759+
SelectBuilder.SelectFromAndJoin selectBuilder = StatementBuilder //
760+
.select(Functions.count(getIdColumn())) //
761+
.from(table);//
762+
763+
SelectBuilder.SelectJoin baseSelect = selectBuilder;
764+
765+
// add possible joins
766+
for (PersistentPropertyPath<RelationalPersistentProperty> path : mappingContext
767+
.findPersistentPropertyPaths(entity.getType(), p -> true)) {
768+
769+
PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(mappingContext, path);
770+
771+
// add a join if necessary
772+
Join join = getJoin(extPath);
773+
if (join != null) {
774+
baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId);
775+
}
776+
}
777+
778+
Select select = applyQueryOnSelect(query, parameterSource, (SelectBuilder.SelectWhere) baseSelect) //
779+
.build();
780+
781+
return render(select);
782+
}
783+
784+
private SelectBuilder.SelectOrdered applyQueryOnSelect(Query query, MapSqlParameterSource parameterSource,
785+
SelectBuilder.SelectWhere selectBuilder) {
786+
787+
Table table = Table.create(this.entity.getTableName());
788+
744789
SelectBuilder.SelectOrdered selectOrdered = query //
745790
.getCriteria() //
746791
.map(item -> this.applyCriteria(item, selectBuilder, parameterSource, table)) //
@@ -760,12 +805,7 @@ public String selectByQuery(Query query, MapSqlParameterSource parameterSource)
760805
if (query.getOffset() > 0) {
761806
limitable = limitable.offset(query.getOffset());
762807
}
763-
764-
selectOrdered = (SelectBuilder.SelectOrdered) limitable;
765-
Select select = selectOrdered //
766-
.build();
767-
768-
return render(select);
808+
return (SelectBuilder.SelectOrdered) limitable;
769809
}
770810

771811
SelectBuilder.SelectOrdered applyCriteria(@Nullable CriteriaDefinition criteria,

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

+6
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,12 @@ 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) {
385+
// TODO: DIEGO find help for this one
386+
// I have zero MyBatis knowledge.
387+
return false;
388+
}
389+
384390
/*
385391
* (non-Javadoc)
386392
* @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-8
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ public <S extends T> Iterable<S> findAll(Example<S> example, Sort sort) {
209209
Assert.notNull(example, "Example must not be null!");
210210
Assert.notNull(sort, "Sort must not be null!");
211211

212-
return this.entityOperations.select(example,sort);
212+
return this.entityOperations.select(example, sort);
213213
}
214214

215215
@Override
@@ -231,14 +231,9 @@ public <S extends T> long count(Example<S> example) {
231231

232232
@Override
233233
public <S extends T> boolean exists(Example<S> example) {
234+
Assert.notNull(example, "Example must not be null!");
234235

235-
/*Assert.notNull(example, "Example must not be null!");
236-
237-
Query query = this.exampleMapper.getMappedExample(example);
238-
239-
return this.entityOperations.exists(query, example.getProbeType());*/
240-
// TODO: impl
241-
return false;
236+
return this.entityOperations.exists(example);
242237
}
243238

244239
@Override

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

+19
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,25 @@ void selectByQueryValidTest() {
742742
.containsOnly(entry("x_name", probe.name));
743743
}
744744

745+
@Test
746+
void existsByQuerySimpleValidTest() {
747+
final SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class);
748+
749+
DummyEntity probe = new DummyEntity();
750+
probe.name = "Diego";
751+
752+
Criteria criteria = Criteria.where("name").is(probe.name);
753+
Query query = Query.query(criteria);
754+
755+
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
756+
757+
String generatedSQL = sqlGenerator.existsByQuery(query, parameterSource);
758+
assertThat(generatedSQL).isNotNull().contains(":x_name");
759+
760+
assertThat(parameterSource.getValues()) //
761+
.containsOnly(entry("x_name", probe.name));
762+
}
763+
745764
private SqlIdentifier getAlias(Object maybeAliased) {
746765

747766
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
@@ -693,6 +693,78 @@ void findAllByExampleShouldGetNone() {
693693
.isEmpty();
694694
}
695695

696+
@Test
697+
void existsByExampleShouldGetOne() {
698+
699+
DummyEntity dummyEntity1 = createDummyEntity();
700+
dummyEntity1.setFlag(true);
701+
702+
repository.save(dummyEntity1);
703+
704+
DummyEntity dummyEntity2 = createDummyEntity();
705+
dummyEntity2.setName("Diego");
706+
707+
repository.save(dummyEntity2);
708+
709+
Example<DummyEntity> example = Example.of(new DummyEntity("Diego"));
710+
711+
boolean exists = repository.exists(example);
712+
713+
assertThat(exists).isTrue();
714+
}
715+
716+
@Test
717+
void existsByExampleMultipleMatchShouldGetOne() {
718+
719+
DummyEntity dummyEntity1 = createDummyEntity();
720+
repository.save(dummyEntity1);
721+
722+
DummyEntity dummyEntity2 = createDummyEntity();
723+
repository.save(dummyEntity2);
724+
725+
Example<DummyEntity> example = Example.of(createDummyEntity());
726+
727+
boolean exists = repository.exists(example);
728+
assertThat(exists).isTrue();
729+
}
730+
731+
@Test
732+
void existsByExampleShouldGetNone() {
733+
734+
DummyEntity dummyEntity1 = createDummyEntity();
735+
dummyEntity1.setFlag(true);
736+
737+
repository.save(dummyEntity1);
738+
739+
Example<DummyEntity> example = Example.of(new DummyEntity("NotExisting"));
740+
741+
boolean exists = repository.exists(example);
742+
743+
assertThat(exists).isFalse();
744+
}
745+
746+
@Test
747+
void existsByExampleComplex() {
748+
749+
final Instant pointInTime = Instant.now().minusSeconds(10000);
750+
751+
final DummyEntity one = repository.save(createDummyEntity());
752+
753+
DummyEntity two = createDummyEntity();
754+
two.setName("Diego");
755+
two.setPointInTime(pointInTime);
756+
two = repository.save(two);
757+
758+
DummyEntity exampleEntitiy = createDummyEntity();
759+
exampleEntitiy.setName("Diego");
760+
exampleEntitiy.setPointInTime(pointInTime);
761+
762+
Example<DummyEntity> example = Example.of(exampleEntitiy);
763+
764+
boolean exists = repository.exists(example);
765+
assertThat(exists).isTrue();
766+
}
767+
696768
private Instant createDummyBeforeAndAfterNow() {
697769

698770
Instant now = Instant.now();

0 commit comments

Comments
 (0)