Browse Source

Polishing.

Added `@since` comments for new methods and classes.
General formatting and code style tweaking.
Github references for new tests added.
Fixes for integration tests with various databases:
- Not all stores support submillisecond precision for Instant.
- Count for exists query doesn't work for all databases, nor does `LEAST(COUNT(1), 1)`
- MariaDB defaults timestamp columns to the current time.
- Ordering was applied twice.
- DATETIME in SqlServer has a most peculiar preceision. We switch to DATETIME2.

Original pull request #1195
See #1192
pull/1298/head
Jens Schauder 3 years ago
parent
commit
aad40a32b0
No known key found for this signature in database
GPG Key ID: 45CC872F17423DBF
  1. 8
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java
  2. 8
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java
  3. 6
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CascadingDataAccessStrategy.java
  4. 13
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java
  5. 42
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java
  6. 3
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DelegatingDataAccessStrategy.java
  7. 39
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/QueryMapper.java
  8. 51
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java
  9. 7
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java
  10. 19
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FetchableFluentQueryByExample.java
  11. 3
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FluentQuerySupport.java
  12. 31
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java
  13. 22
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java
  14. 241
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java
  15. 2
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql
  16. 13
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java
  17. 8
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java
  18. 5
      spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java

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

@ -196,6 +196,7 @@ public interface JdbcAggregateOperations {
* @param entityClass the entity type must not be {@literal null}. * @param entityClass the entity type must not be {@literal null}.
* @return exactly one result or {@link Optional#empty()} if no match found. * @return exactly one result or {@link Optional#empty()} if no match found.
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
* @since 3.0
*/ */
<T> Optional<T> selectOne(Query query, Class<T> entityClass); <T> Optional<T> selectOne(Query query, Class<T> entityClass);
@ -204,11 +205,11 @@ public interface JdbcAggregateOperations {
* *
* @param query must not be {@literal null}. * @param query must not be {@literal null}.
* @param entityClass the entity type must not be {@literal null}. * @param entityClass the entity type must not be {@literal null}.
* @param sort the sorting that should be used on the result.
* @return a non-null sorted list with all the matching results. * @return a non-null sorted list with all the matching results.
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
* @since 3.0
*/ */
<T> Iterable<T> select(Query query, Class<T> entityClass, Sort sort); <T> Iterable<T> select(Query query, Class<T> entityClass);
/** /**
* Determine whether there are aggregates that match the {@link Query} * Determine whether there are aggregates that match the {@link Query}
@ -216,6 +217,7 @@ public interface JdbcAggregateOperations {
* @param query must not be {@literal null}. * @param query must not be {@literal null}.
* @param entityClass the entity type must not be {@literal null}. * @param entityClass the entity type must not be {@literal null}.
* @return {@literal true} if the object exists. * @return {@literal true} if the object exists.
* @since 3.0
*/ */
<T> boolean exists(Query query, Class<T> entityClass); <T> boolean exists(Query query, Class<T> entityClass);
@ -225,6 +227,7 @@ public interface JdbcAggregateOperations {
* @param query must not be {@literal null}. * @param query must not be {@literal null}.
* @param entityClass the entity type must not be {@literal null}. * @param entityClass the entity type must not be {@literal null}.
* @return the number of instances stored in the database. Guaranteed to be not {@code null}. * @return the number of instances stored in the database. Guaranteed to be not {@code null}.
* @since 3.0
*/ */
<T> long count(Query query, Class<T> entityClass); <T> long count(Query query, Class<T> entityClass);
@ -236,6 +239,7 @@ public interface JdbcAggregateOperations {
* @param entityClass the entity type must not be {@literal null}. * @param entityClass the entity type must not be {@literal null}.
* @param pageable can be null. * @param pageable can be null.
* @return a {@link Page} of entities matching the given {@link Example}. * @return a {@link Page} of entities matching the given {@link Example}.
* @since 3.0
*/ */
<T> Page<T> select(Query query, Class<T> entityClass, Pageable pageable); <T> Page<T> select(Query query, Class<T> entityClass, Pageable pageable);
} }

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

@ -74,7 +74,6 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
private final DataAccessStrategy accessStrategy; private final DataAccessStrategy accessStrategy;
private final AggregateChangeExecutor executor; private final AggregateChangeExecutor executor;
private final JdbcConverter converter; private final JdbcConverter converter;
private EntityCallbacks entityCallbacks = EntityCallbacks.create(); private EntityCallbacks entityCallbacks = EntityCallbacks.create();
@ -248,7 +247,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
} }
@Override @Override
public <T> Iterable<T> select(Query query, Class<T> entityClass, Sort sort) { public <T> Iterable<T> select(Query query, Class<T> entityClass) {
return accessStrategy.select(query, entityClass); return accessStrategy.select(query, entityClass);
} }
@ -264,16 +263,13 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
@Override @Override
public <T> Page<T> select(Query query, Class<T> entityClass, Pageable pageable) { public <T> Page<T> select(Query query, Class<T> entityClass, Pageable pageable) {
Iterable<T> items = triggerAfterConvert(accessStrategy.select(query, entityClass, pageable)); Iterable<T> items = triggerAfterConvert(accessStrategy.select(query, entityClass, pageable));
List<T> content = StreamSupport.stream(items.spliterator(), false).collect(Collectors.toList()); List<T> content = StreamSupport.stream(items.spliterator(), false).collect(Collectors.toList());
return PageableExecutionUtils.getPage(content, pageable, () -> accessStrategy.count(query, entityClass)); return PageableExecutionUtils.getPage(content, pageable, () -> accessStrategy.count(query, entityClass));
} }
/*
* (non-Javadoc)
* @see org.springframework.data.jdbc.core.JdbcAggregateOperations#findAll(java.lang.Class)
*/
@Override @Override
public <T> Iterable<T> findAll(Class<T> domainType) { public <T> Iterable<T> findAll(Class<T> domainType) {

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

@ -15,6 +15,8 @@
*/ */
package org.springframework.data.jdbc.core.convert; package org.springframework.data.jdbc.core.convert;
import static java.lang.Boolean.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -29,10 +31,8 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentProp
import org.springframework.data.relational.core.query.Query; import org.springframework.data.relational.core.query.Query;
import org.springframework.data.relational.core.sql.LockMode; import org.springframework.data.relational.core.sql.LockMode;
import static java.lang.Boolean.*;
/** /**
* Delegates each methods to the {@link DataAccessStrategy}s passed to the constructor in turn until the first that does * Delegates each method to the {@link DataAccessStrategy}s passed to the constructor in turn until the first that does
* not throw an exception. * not throw an exception.
* *
* @author Jens Schauder * @author Jens Schauder

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

@ -114,8 +114,8 @@ public interface DataAccessStrategy extends RelationResolver {
* @param previousVersion The previous version assigned to the instance being saved. * @param previousVersion The previous version assigned to the instance being saved.
* @param <T> the type of the instance to save. * @param <T> the type of the instance to save.
* @return whether the update actually updated a row. * @return whether the update actually updated a row.
* @throws OptimisticLockingFailureException if the update fails to update at least one row assuming the * @throws OptimisticLockingFailureException if the update fails to update at least one row assuming the optimistic
* optimistic locking version check failed. * locking version check failed.
* @since 2.0 * @since 2.0
*/ */
<T> boolean updateWithVersion(T instance, Class<T> domainType, Number previousVersion); <T> boolean updateWithVersion(T instance, Class<T> domainType, Number previousVersion);
@ -155,8 +155,8 @@ public interface DataAccessStrategy extends RelationResolver {
* @param domainType the type of entity to be deleted. Implicitly determines the table to operate on. Must not be * @param domainType the type of entity to be deleted. Implicitly determines the table to operate on. Must not be
* {@code null}. * {@code null}.
* @param previousVersion The previous version assigned to the instance being saved. * @param previousVersion The previous version assigned to the instance being saved.
* @throws OptimisticLockingFailureException if the update fails to update at least one row assuming the * @throws OptimisticLockingFailureException if the update fails to update at least one row assuming the optimistic
* optimistic locking version check failed. * locking version check failed.
* @since 2.0 * @since 2.0
*/ */
<T> void deleteWithVersion(Object id, Class<T> domainType, Number previousVersion); <T> void deleteWithVersion(Object id, Class<T> domainType, Number previousVersion);
@ -292,6 +292,7 @@ public interface DataAccessStrategy extends RelationResolver {
* @param probeType the type of entities. Must not be {@code null}. * @param probeType the type of entities. Must not be {@code null}.
* @return exactly one result or {@link Optional#empty()} if no match found. * @return exactly one result or {@link Optional#empty()} if no match found.
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
* @since 3.0
*/ */
<T> Optional<T> selectOne(Query query, Class<T> probeType); <T> Optional<T> selectOne(Query query, Class<T> probeType);
@ -302,6 +303,7 @@ public interface DataAccessStrategy extends RelationResolver {
* @param probeType the type of entities. Must not be {@code null}. * @param probeType the type of entities. Must not be {@code null}.
* @return a non-null list with all the matching results. * @return a non-null list with all the matching results.
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
* @since 3.0
*/ */
<T> Iterable<T> select(Query query, Class<T> probeType); <T> Iterable<T> select(Query query, Class<T> probeType);
@ -314,6 +316,7 @@ public interface DataAccessStrategy extends RelationResolver {
* @param pageable the pagination that should be applied. Must not be {@literal null}. * @param pageable the pagination that should be applied. Must not be {@literal null}.
* @return a non-null list with all the matching results. * @return a non-null list with all the matching results.
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
* @since 3.0
*/ */
<T> Iterable<T> select(Query query, Class<T> probeType, Pageable pageable); <T> Iterable<T> select(Query query, Class<T> probeType, Pageable pageable);
@ -323,6 +326,7 @@ public interface DataAccessStrategy extends RelationResolver {
* @param query must not be {@literal null}. * @param query must not be {@literal null}.
* @param probeType the type of entities. Must not be {@code null}. * @param probeType the type of entities. Must not be {@code null}.
* @return {@literal true} if the object exists. * @return {@literal true} if the object exists.
* @since 3.0
*/ */
<T> boolean exists(Query query, Class<T> probeType); <T> boolean exists(Query query, Class<T> probeType);
@ -332,6 +336,7 @@ public interface DataAccessStrategy extends RelationResolver {
* @param probeType the probe type for which to count the elements. Must not be {@code null}. * @param probeType the probe type for which to count the elements. Must not be {@code null}.
* @param query the query which elements have to match. * @param query the query which elements have to match.
* @return the count. Guaranteed to be not {@code null}. * @return the count. Guaranteed to be not {@code null}.
* @since 3.0
*/ */
<T> long count(Query query, Class<T> probeType); <T> long count(Query query, Class<T> probeType);
} }

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

@ -20,9 +20,7 @@ import static org.springframework.data.jdbc.core.convert.SqlGenerator.*;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.function.Predicate;
import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.dao.OptimisticLockingFailureException;
@ -262,27 +260,24 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
} }
@Override @Override
@SuppressWarnings("unchecked")
public <T> T findById(Object id, Class<T> domainType) { public <T> T findById(Object id, Class<T> domainType) {
String findOneSql = sql(domainType).getFindOne(); String findOneSql = sql(domainType).getFindOne();
SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER); SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER);
try { try {
return operations.queryForObject(findOneSql, parameter, (RowMapper<T>) getEntityRowMapper(domainType)); return operations.queryForObject(findOneSql, parameter, getEntityRowMapper(domainType));
} catch (EmptyResultDataAccessException e) { } catch (EmptyResultDataAccessException e) {
return null; return null;
} }
} }
@Override @Override
@SuppressWarnings("unchecked")
public <T> Iterable<T> findAll(Class<T> domainType) { public <T> Iterable<T> findAll(Class<T> domainType) {
return operations.query(sql(domainType).getFindAll(), (RowMapper<T>) getEntityRowMapper(domainType)); return operations.query(sql(domainType).getFindAll(), getEntityRowMapper(domainType));
} }
@Override @Override
@SuppressWarnings("unchecked")
public <T> Iterable<T> findAllById(Iterable<?> ids, Class<T> domainType) { public <T> Iterable<T> findAllById(Iterable<?> ids, Class<T> domainType) {
if (!ids.iterator().hasNext()) { if (!ids.iterator().hasNext()) {
@ -293,7 +288,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
String findAllInListSql = sql(domainType).getFindAllInList(); String findAllInListSql = sql(domainType).getFindAllInList();
return operations.query(findAllInListSql, parameterSource, (RowMapper<T>) getEntityRowMapper(domainType)); return operations.query(findAllInListSql, parameterSource, getEntityRowMapper(domainType));
} }
@Override @Override
@ -330,73 +325,74 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
} }
@Override @Override
@SuppressWarnings("unchecked")
public <T> Iterable<T> findAll(Class<T> domainType, Sort sort) { public <T> Iterable<T> findAll(Class<T> domainType, Sort sort) {
return operations.query(sql(domainType).getFindAll(sort), (RowMapper<T>) getEntityRowMapper(domainType)); return operations.query(sql(domainType).getFindAll(sort), getEntityRowMapper(domainType));
} }
@Override @Override
@SuppressWarnings("unchecked")
public <T> Iterable<T> findAll(Class<T> domainType, Pageable pageable) { public <T> Iterable<T> findAll(Class<T> domainType, Pageable pageable) {
return operations.query(sql(domainType).getFindAll(pageable), (RowMapper<T>) getEntityRowMapper(domainType)); return operations.query(sql(domainType).getFindAll(pageable), getEntityRowMapper(domainType));
} }
@Override @Override
public <T> Optional<T> selectOne(Query query, Class<T> probeType) { public <T> Optional<T> selectOne(Query query, Class<T> probeType) {
MapSqlParameterSource parameterSource = new MapSqlParameterSource(); MapSqlParameterSource parameterSource = new MapSqlParameterSource();
String sqlQuery = sql(probeType).selectByQuery(query, parameterSource); String sqlQuery = sql(probeType).selectByQuery(query, parameterSource);
T foundObject;
try { try {
foundObject = operations.queryForObject(sqlQuery, parameterSource, (RowMapper<T>) getEntityRowMapper(probeType)); return Optional.ofNullable(
operations.queryForObject(sqlQuery, parameterSource, getEntityRowMapper(probeType)));
} catch (EmptyResultDataAccessException e) { } catch (EmptyResultDataAccessException e) {
foundObject = null; return Optional.empty();
} }
return Optional.ofNullable(foundObject);
} }
@Override @Override
public <T> Iterable<T> select(Query query, Class<T> probeType) { public <T> Iterable<T> select(Query query, Class<T> probeType) {
MapSqlParameterSource parameterSource = new MapSqlParameterSource(); MapSqlParameterSource parameterSource = new MapSqlParameterSource();
String sqlQuery = sql(probeType).selectByQuery(query, parameterSource); String sqlQuery = sql(probeType).selectByQuery(query, parameterSource);
return operations.query(sqlQuery, parameterSource, (RowMapper<T>) getEntityRowMapper(probeType)); return operations.query(sqlQuery, parameterSource, getEntityRowMapper(probeType));
} }
@Override @Override
public <T> Iterable<T> select(Query query, Class<T> probeType, Pageable pageable) { public <T> Iterable<T> select(Query query, Class<T> probeType, Pageable pageable) {
MapSqlParameterSource parameterSource = new MapSqlParameterSource(); MapSqlParameterSource parameterSource = new MapSqlParameterSource();
String sqlQuery = sql(probeType).selectByQuery(query, parameterSource, pageable); String sqlQuery = sql(probeType).selectByQuery(query, parameterSource, pageable);
return operations.query(sqlQuery, parameterSource, (RowMapper<T>) getEntityRowMapper(probeType)); return operations.query(sqlQuery, parameterSource, getEntityRowMapper(probeType));
} }
@Override @Override
public <T> boolean exists(Query query, Class<T> probeType) { public <T> boolean exists(Query query, Class<T> probeType) {
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
String sqlQuery = sql(probeType).existsByQuery(query, parameterSource); String sqlQuery = sql(probeType).existsByQuery(query, parameterSource);
Boolean result = operations.queryForObject(sqlQuery, parameterSource, Boolean.class); Boolean result = operations.queryForObject(sqlQuery, parameterSource, Boolean.class);
Assert.notNull(result, "The result of an exists query must not be null");
Assert.state(result != null, "The result of an exists query must not be null");
return result; return result;
} }
@Override @Override
public <T> long count(Query query, Class<T> probeType) { public <T> long count(Query query, Class<T> probeType) {
MapSqlParameterSource parameterSource = new MapSqlParameterSource(); MapSqlParameterSource parameterSource = new MapSqlParameterSource();
String sqlQuery = sql(probeType).countByQuery(query, parameterSource); String sqlQuery = sql(probeType).countByQuery(query, parameterSource);
Long result = operations.queryForObject(sqlQuery, parameterSource, Long.class); Long result = operations.queryForObject(sqlQuery, parameterSource, Long.class);
Assert.notNull(result, "The result of a count query must not be null."); Assert.state(result != null, "The result of a count query must not be null.");
return result; return result;
} }
private EntityRowMapper<?> getEntityRowMapper(Class<?> domainType) { private <T> EntityRowMapper<T> getEntityRowMapper(Class<T> domainType) {
return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), converter); return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), converter);
} }

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

@ -16,6 +16,7 @@
package org.springframework.data.jdbc.core.convert; package org.springframework.data.jdbc.core.convert;
import java.util.List; import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
@ -26,8 +27,6 @@ import org.springframework.data.relational.core.query.Query;
import org.springframework.data.relational.core.sql.LockMode; import org.springframework.data.relational.core.sql.LockMode;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import java.util.Optional;
/** /**
* Delegates all method calls to an instance set after construction. This is useful for {@link DataAccessStrategy}s with * Delegates all method calls to an instance set after construction. This is useful for {@link DataAccessStrategy}s with
* cyclic dependencies. * cyclic dependencies.

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

@ -55,7 +55,7 @@ import org.springframework.util.ClassUtils;
* *
* @author Mark Paluch * @author Mark Paluch
* @author Jens Schauder * @author Jens Schauder
* @since 2.0 * @since 3.0
*/ */
public class QueryMapper { public class QueryMapper {
@ -85,7 +85,7 @@ public class QueryMapper {
* *
* @param sort must not be {@literal null}. * @param sort must not be {@literal null}.
* @param entity related {@link RelationalPersistentEntity}, can be {@literal null}. * @param entity related {@link RelationalPersistentEntity}, can be {@literal null}.
* @return * @return a List of {@link OrderByField} objects guaranteed to be not {@literal null}.
*/ */
public List<OrderByField> getMappedSort(Table table, Sort sort, @Nullable RelationalPersistentEntity<?> entity) { public List<OrderByField> getMappedSort(Table table, Sort sort, @Nullable RelationalPersistentEntity<?> entity) {
@ -116,9 +116,8 @@ public class QueryMapper {
return expression; return expression;
} }
if (expression instanceof Column) { if (expression instanceof Column column) {
Column column = (Column) expression;
Field field = createPropertyField(entity, column.getName()); Field field = createPropertyField(entity, column.getName());
TableLike table = column.getTable(); TableLike table = column.getTable();
@ -128,9 +127,7 @@ public class QueryMapper {
return column instanceof Aliased ? columnFromTable.as(((Aliased) column).getAlias()) : columnFromTable; return column instanceof Aliased ? columnFromTable.as(((Aliased) column).getAlias()) : columnFromTable;
} }
if (expression instanceof SimpleFunction) { if (expression instanceof SimpleFunction function) {
SimpleFunction function = (SimpleFunction) expression;
List<Expression> arguments = function.getExpressions(); List<Expression> arguments = function.getExpressions();
List<Expression> mappedArguments = new ArrayList<>(arguments.size()); List<Expression> mappedArguments = new ArrayList<>(arguments.size());
@ -280,9 +277,7 @@ public class QueryMapper {
Object mappedValue; Object mappedValue;
SQLType sqlType; SQLType sqlType;
if (criteria.getValue() instanceof JdbcValue) { if (criteria.getValue() instanceof JdbcValue settableValue) {
JdbcValue settableValue = (JdbcValue) criteria.getValue();
mappedValue = convertValue(settableValue.getValue(), propertyField.getTypeHint()); mappedValue = convertValue(settableValue.getValue(), propertyField.getTypeHint());
sqlType = getTypeHint(mappedValue, actualType.getType(), settableValue); sqlType = getTypeHint(mappedValue, actualType.getType(), settableValue);
@ -554,40 +549,39 @@ public class QueryMapper {
String refName = column.getName().getReference(); String refName = column.getName().getReference();
switch (comparator) { switch (comparator) {
case EQ: { case EQ -> {
Expression expression = bind(mappedValue, sqlType, parameterSource, refName, ignoreCase); Expression expression = bind(mappedValue, sqlType, parameterSource, refName, ignoreCase);
return Conditions.isEqual(columnExpression, expression); return Conditions.isEqual(columnExpression, expression);
} }
case NEQ: { case NEQ -> {
Expression expression = bind(mappedValue, sqlType, parameterSource, refName, ignoreCase); Expression expression = bind(mappedValue, sqlType, parameterSource, refName, ignoreCase);
return Conditions.isEqual(columnExpression, expression).not(); return Conditions.isEqual(columnExpression, expression).not();
} }
case LT: { case LT -> {
Expression expression = bind(mappedValue, sqlType, parameterSource, refName); Expression expression = bind(mappedValue, sqlType, parameterSource, refName);
return column.isLess(expression); return column.isLess(expression);
} }
case LTE: { case LTE -> {
Expression expression = bind(mappedValue, sqlType, parameterSource, refName); Expression expression = bind(mappedValue, sqlType, parameterSource, refName);
return column.isLessOrEqualTo(expression); return column.isLessOrEqualTo(expression);
} }
case GT: { case GT -> {
Expression expression = bind(mappedValue, sqlType, parameterSource, refName); Expression expression = bind(mappedValue, sqlType, parameterSource, refName);
return column.isGreater(expression); return column.isGreater(expression);
} }
case GTE: { case GTE -> {
Expression expression = bind(mappedValue, sqlType, parameterSource, refName); Expression expression = bind(mappedValue, sqlType, parameterSource, refName);
return column.isGreaterOrEqualTo(expression); return column.isGreaterOrEqualTo(expression);
} }
case LIKE: { case LIKE -> {
Expression expression = bind(mappedValue, sqlType, parameterSource, refName, ignoreCase); Expression expression = bind(mappedValue, sqlType, parameterSource, refName, ignoreCase);
return Conditions.like(columnExpression, expression); return Conditions.like(columnExpression, expression);
} }
case NOT_LIKE: { case NOT_LIKE -> {
Expression expression = bind(mappedValue, sqlType, parameterSource, refName, ignoreCase); Expression expression = bind(mappedValue, sqlType, parameterSource, refName, ignoreCase);
return Conditions.notLike(columnExpression, expression); return Conditions.notLike(columnExpression, expression);
} }
default: default -> throw new UnsupportedOperationException("Comparator " + comparator + " not supported");
throw new UnsupportedOperationException("Comparator " + comparator + " not supported");
} }
} }
@ -677,7 +671,7 @@ public class QueryMapper {
/** /**
* Returns the key to be used in the mapped document eventually. * Returns the key to be used in the mapped document eventually.
* *
* @return * @return the key to be used in the mapped document eventually.
*/ */
public SqlIdentifier getMappedColumnName() { public SqlIdentifier getMappedColumnName() {
return this.name; return this.name;
@ -774,9 +768,6 @@ public class QueryMapper {
/** /**
* Returns the {@link PersistentPropertyPath} for the given {@code pathExpression}. * Returns the {@link PersistentPropertyPath} for the given {@code pathExpression}.
*
* @param pathExpression
* @return
*/ */
@Nullable @Nullable
private PersistentPropertyPath<RelationalPersistentProperty> getPath(String pathExpression) { private PersistentPropertyPath<RelationalPersistentProperty> getPath(String pathExpression) {

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

@ -88,6 +88,7 @@ class SqlGenerator {
private final Lazy<String> deleteByIdAndVersionSql = Lazy.of(this::createDeleteByIdAndVersionSql); private final Lazy<String> deleteByIdAndVersionSql = Lazy.of(this::createDeleteByIdAndVersionSql);
private final Lazy<String> deleteByListSql = Lazy.of(this::createDeleteByListSql); private final Lazy<String> deleteByListSql = Lazy.of(this::createDeleteByListSql);
private final QueryMapper queryMapper; private final QueryMapper queryMapper;
private final Dialect dialect;
/** /**
* Create a new {@link SqlGenerator} given {@link RelationalMappingContext} and {@link RelationalPersistentEntity}. * Create a new {@link SqlGenerator} given {@link RelationalMappingContext} and {@link RelationalPersistentEntity}.
@ -107,6 +108,7 @@ class SqlGenerator {
this.sqlRenderer = SqlRenderer.create(renderContext); this.sqlRenderer = SqlRenderer.create(renderContext);
this.columns = new Columns(entity, mappingContext, converter); this.columns = new Columns(entity, mappingContext, converter);
this.queryMapper = new QueryMapper(dialect, converter); this.queryMapper = new QueryMapper(dialect, converter);
this.dialect = dialect;
} }
/** /**
@ -668,15 +670,6 @@ class SqlGenerator {
return render(delete); return render(delete);
} }
private String createDeleteByIdInAndVersionSql() {
Delete delete = createBaseDeleteByIdIn(getTable()) //
.and(getVersionColumn().isEqualTo(SQL.bindMarker(":" + renderReference(VERSION_SQL_PARAMETER)))) //
.build();
return render(delete);
}
private DeleteBuilder.DeleteWhereAndOr createBaseDeleteById(Table table) { private DeleteBuilder.DeleteWhereAndOr createBaseDeleteById(Table table) {
return Delete.builder().from(table) return Delete.builder().from(table)
@ -827,8 +820,7 @@ class SqlGenerator {
*/ */
public String existsByQuery(Query query, MapSqlParameterSource parameterSource) { public String existsByQuery(Query query, MapSqlParameterSource parameterSource) {
Expression idColumn = getIdColumn(); SelectBuilder.SelectJoin baseSelect = getExistsSelect();
SelectBuilder.SelectJoin baseSelect = getSelectCountWithExpression(idColumn);
Select select = applyQueryOnSelect(query, parameterSource, (SelectBuilder.SelectWhere) baseSelect) // Select select = applyQueryOnSelect(query, parameterSource, (SelectBuilder.SelectWhere) baseSelect) //
.build(); .build();
@ -855,6 +847,36 @@ class SqlGenerator {
return render(select); return render(select);
} }
/**
* Generates a {@link org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin} with a
* <code>COUNT(...)</code> where the <code>countExpressions</code> are the parameters of the count.
*
* @return a non-null {@link org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin} that joins all the
* columns and has only a count in the projection of the select.
*/
private SelectBuilder.SelectJoin getExistsSelect() {
Table table = getTable();
SelectBuilder.SelectJoin baseSelect = StatementBuilder //
.select(dialect.getExistsFunction()) //
.from(table);
// add possible joins
for (PersistentPropertyPath<RelationalPersistentProperty> path : mappingContext
.findPersistentPropertyPaths(entity.getType(), p -> true)) {
PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(mappingContext, path);
// add a join if necessary
Join join = getJoin(extPath);
if (join != null) {
baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId);
}
}
return baseSelect;
}
/** /**
* Generates a {@link org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin} with a * Generates a {@link org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin} with a
* <code>COUNT(...)</code> where the <code>countExpressions</code> are the parameters of the count. * <code>COUNT(...)</code> where the <code>countExpressions</code> are the parameters of the count.
@ -869,11 +891,10 @@ class SqlGenerator {
Assert.state(countExpressions.length >= 1, "countExpressions must contain at least one expression"); Assert.state(countExpressions.length >= 1, "countExpressions must contain at least one expression");
Table table = getTable(); Table table = getTable();
SelectBuilder.SelectFromAndJoin selectBuilder = StatementBuilder //
.select(Functions.count(countExpressions)) //
.from(table);//
SelectBuilder.SelectJoin baseSelect = selectBuilder; SelectBuilder.SelectJoin baseSelect = StatementBuilder //
.select(Functions.count(countExpressions)) //
.from(table);
// add possible joins // add possible joins
for (PersistentPropertyPath<RelationalPersistentProperty> path : mappingContext for (PersistentPropertyPath<RelationalPersistentProperty> path : mappingContext

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

@ -22,10 +22,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSession;
import org.mybatis.spring.SqlSessionTemplate; import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.EmptyResultDataAccessException;
@ -352,10 +349,6 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy {
throw new UnsupportedOperationException("Not implemented"); throw new UnsupportedOperationException("Not implemented");
} }
/*
* (non-Javadoc)
* @see org.springframework.data.jdbc.core.DataAccessStrategy#count(java.lang.Class)
*/
@Override @Override
public long count(Class<?> domainType) { public long count(Class<?> domainType) {

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

@ -34,6 +34,7 @@ import org.springframework.data.relational.repository.query.RelationalExampleMap
* {@link org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery} using {@link Example}. * {@link org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery} using {@link Example}.
* *
* @author Diego Krupitza * @author Diego Krupitza
* @since 3.0
*/ */
class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<S, R> { class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<S, R> {
@ -47,40 +48,47 @@ class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<S, R> {
FetchableFluentQueryByExample(Example<S> example, Sort sort, Class<R> resultType, List<String> fieldsToInclude, FetchableFluentQueryByExample(Example<S> example, Sort sort, Class<R> resultType, List<String> fieldsToInclude,
RelationalExampleMapper exampleMapper, JdbcAggregateOperations entityOperations) { RelationalExampleMapper exampleMapper, JdbcAggregateOperations entityOperations) {
super(example, sort, resultType, fieldsToInclude); super(example, sort, resultType, fieldsToInclude);
this.exampleMapper = exampleMapper; this.exampleMapper = exampleMapper;
this.entityOperations = entityOperations; this.entityOperations = entityOperations;
} }
@Override @Override
public R oneValue() { public R oneValue() {
return this.entityOperations.selectOne(createQuery(), getExampleType()) return this.entityOperations.selectOne(createQuery(), getExampleType())
.map(item -> this.getConversionFunction().apply(item)).get(); .map(item -> this.getConversionFunction().apply(item)).get();
} }
@Override @Override
public R firstValue() { public R firstValue() {
return this.getConversionFunction() return this.getConversionFunction()
.apply(this.entityOperations.select(createQuery(), getExampleType(), getSort()).iterator().next()); .apply(this.entityOperations.select(createQuery().sort(getSort()), getExampleType()).iterator().next());
} }
@Override @Override
public List<R> all() { public List<R> all() {
return StreamSupport return StreamSupport
.stream(this.entityOperations.select(createQuery(), getExampleType(), getSort()).spliterator(), false) .stream(this.entityOperations.select(createQuery().sort(getSort()), getExampleType()).spliterator(), false)
.map(item -> this.getConversionFunction().apply(item)).collect(Collectors.toList()); .map(item -> this.getConversionFunction().apply(item)).collect(Collectors.toList());
} }
@Override @Override
public Page<R> page(Pageable pageable) { public Page<R> page(Pageable pageable) {
return this.entityOperations.select(createQuery(p -> p.with(pageable)), getExampleType(), pageable) return this.entityOperations.select(createQuery(p -> p.with(pageable)), getExampleType(), pageable)
.map(item -> this.getConversionFunction().apply(item)); .map(item -> this.getConversionFunction().apply(item));
} }
@Override @Override
public Stream<R> stream() { public Stream<R> stream() {
return StreamSupport return StreamSupport
.stream(this.entityOperations.select(createQuery(), getExampleType(), getSort()).spliterator(), false) .stream(this.entityOperations.select(createQuery().sort(getSort()), getExampleType()).spliterator(), false)
.map(item -> this.getConversionFunction().apply(item)); .map(item -> this.getConversionFunction().apply(item));
} }
@ -102,10 +110,6 @@ class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<S, R> {
Query query = exampleMapper.getMappedExample(getExample()); Query query = exampleMapper.getMappedExample(getExample());
if (getSort().isSorted()) {
query = query.sort(getSort());
}
if (!getFieldsToInclude().isEmpty()) { if (!getFieldsToInclude().isEmpty()) {
query = query.columns(getFieldsToInclude().toArray(new String[0])); query = query.columns(getFieldsToInclude().toArray(new String[0]));
} }
@ -118,6 +122,7 @@ class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<S, R> {
@Override @Override
protected <R> FluentQuerySupport<S, R> create(Example<S> example, Sort sort, Class<R> resultType, protected <R> FluentQuerySupport<S, R> create(Example<S> example, Sort sort, Class<R> resultType,
List<String> fieldsToInclude) { List<String> fieldsToInclude) {
return new FetchableFluentQueryByExample<>(example, sort, resultType, fieldsToInclude, this.exampleMapper, return new FetchableFluentQueryByExample<>(example, sort, resultType, fieldsToInclude, this.exampleMapper,
this.entityOperations); this.entityOperations);
} }

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

@ -31,6 +31,7 @@ import java.util.function.Function;
* Support class for {@link FluentQuery.FetchableFluentQuery} implementations. * Support class for {@link FluentQuery.FetchableFluentQuery} implementations.
* *
* @author Diego Krupitza * @author Diego Krupitza
* @since 3.0
*/ */
abstract class FluentQuerySupport<S, R> implements FluentQuery.FetchableFluentQuery<R> { abstract class FluentQuerySupport<S, R> implements FluentQuery.FetchableFluentQuery<R> {
@ -42,6 +43,7 @@ abstract class FluentQuerySupport<S, R> implements FluentQuery.FetchableFluentQu
private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
FluentQuerySupport(Example<S> example, Sort sort, Class<R> resultType, List<String> fieldsToInclude) { FluentQuerySupport(Example<S> example, Sort sort, Class<R> resultType, List<String> fieldsToInclude) {
this.example = example; this.example = example;
this.sort = sort; this.sort = sort;
this.resultType = resultType; this.resultType = resultType;
@ -123,5 +125,4 @@ abstract class FluentQuerySupport<S, R> implements FluentQuery.FetchableFluentQu
protected Function<Object, R> getConversionFunction() { protected Function<Object, R> getConversionFunction() {
return getConversionFunction(this.example.getProbeType(), getResultType()); return getConversionFunction(this.example.getProbeType(), getResultType());
} }
} }

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

@ -17,7 +17,6 @@ package org.springframework.data.jdbc.repository.support;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.data.domain.Example; import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
@ -26,11 +25,13 @@ import org.springframework.data.domain.Sort;
import org.springframework.data.jdbc.core.JdbcAggregateOperations; import org.springframework.data.jdbc.core.JdbcAggregateOperations;
import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.relational.core.query.Query;
import org.springframework.data.relational.repository.query.RelationalExampleMapper; import org.springframework.data.relational.repository.query.RelationalExampleMapper;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.FluentQuery; import org.springframework.data.repository.query.FluentQuery;
import org.springframework.data.repository.query.QueryByExampleExecutor; import org.springframework.data.repository.query.QueryByExampleExecutor;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.data.util.Streamable; import org.springframework.data.util.Streamable;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -141,50 +142,58 @@ public class SimpleJdbcRepository<T, ID>
@Override @Override
public <S extends T> Optional<S> findOne(Example<S> example) { public <S extends T> Optional<S> findOne(Example<S> example) {
Assert.notNull(example, "Example must not be null!");
Assert.notNull(example, "Example must not be null");
return this.entityOperations.selectOne(this.exampleMapper.getMappedExample(example), example.getProbeType()); return this.entityOperations.selectOne(this.exampleMapper.getMappedExample(example), example.getProbeType());
} }
@Override @Override
public <S extends T> Iterable<S> findAll(Example<S> example) { public <S extends T> Iterable<S> findAll(Example<S> example) {
Assert.notNull(example, "Example must not be null!");
Assert.notNull(example, "Example must not be null");
return findAll(example, Sort.unsorted()); return findAll(example, Sort.unsorted());
} }
@Override @Override
public <S extends T> Iterable<S> findAll(Example<S> example, Sort sort) { public <S extends T> Iterable<S> findAll(Example<S> example, Sort sort) {
Assert.notNull(example, "Example must not be null!");
Assert.notNull(sort, "Sort must not be null!");
return this.entityOperations.select(this.exampleMapper.getMappedExample(example), example.getProbeType(), sort); Assert.notNull(example, "Example must not be null");
Assert.notNull(sort, "Sort must not be null");
return this.entityOperations.select(this.exampleMapper.getMappedExample(example).sort(sort),
example.getProbeType());
} }
@Override @Override
public <S extends T> Page<S> findAll(Example<S> example, Pageable pageable) { public <S extends T> Page<S> findAll(Example<S> example, Pageable pageable) {
Assert.notNull(example, "Example must not be null!");
Assert.notNull(example, "Example must not be null");
return this.entityOperations.select(this.exampleMapper.getMappedExample(example), example.getProbeType(), pageable); return this.entityOperations.select(this.exampleMapper.getMappedExample(example), example.getProbeType(), pageable);
} }
@Override @Override
public <S extends T> long count(Example<S> example) { public <S extends T> long count(Example<S> example) {
Assert.notNull(example, "Example must not be null!");
Assert.notNull(example, "Example must not be null");
return this.entityOperations.count(this.exampleMapper.getMappedExample(example), example.getProbeType()); return this.entityOperations.count(this.exampleMapper.getMappedExample(example), example.getProbeType());
} }
@Override @Override
public <S extends T> boolean exists(Example<S> example) { public <S extends T> boolean exists(Example<S> example) {
Assert.notNull(example, "Example must not be null!"); Assert.notNull(example, "Example must not be null");
return this.entityOperations.exists(this.exampleMapper.getMappedExample(example), example.getProbeType()); return this.entityOperations.exists(this.exampleMapper.getMappedExample(example), example.getProbeType());
} }
@Override @Override
public <S extends T, R> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction) { public <S extends T, R> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction) {
Assert.notNull(example, "Sample must not be null!");
Assert.notNull(queryFunction, "Query function must not be null!"); Assert.notNull(example, "Sample must not be null");
Assert.notNull(queryFunction, "Query function must not be null");
FluentQuery.FetchableFluentQuery<S> fluentQuery = new FetchableFluentQueryByExample<>(example, FluentQuery.FetchableFluentQuery<S> fluentQuery = new FetchableFluentQueryByExample<>(example,
example.getProbeType(), this.exampleMapper, this.entityOperations); example.getProbeType(), this.exampleMapper, this.entityOperations);

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

@ -742,10 +742,10 @@ class SqlGeneratorUnitTests {
SqlIdentifier.quoted("child"), SqlIdentifier.quoted("CHILD_PARENT_OF_NO_ID_CHILD")); SqlIdentifier.quoted("child"), SqlIdentifier.quoted("CHILD_PARENT_OF_NO_ID_CHILD"));
} }
@Nullable @Test // GH-1192
@Test
void selectByQueryValidTest() { void selectByQueryValidTest() {
final SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class);
SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class);
DummyEntity probe = new DummyEntity(); DummyEntity probe = new DummyEntity();
probe.name = "Diego"; probe.name = "Diego";
@ -762,9 +762,10 @@ class SqlGeneratorUnitTests {
.containsOnly(entry("x_name", probe.name)); .containsOnly(entry("x_name", probe.name));
} }
@Test @Test // GH-1192
void existsByQuerySimpleValidTest() { void existsByQuerySimpleValidTest() {
final SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class);
SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class);
DummyEntity probe = new DummyEntity(); DummyEntity probe = new DummyEntity();
probe.name = "Diego"; probe.name = "Diego";
@ -781,9 +782,10 @@ class SqlGeneratorUnitTests {
.containsOnly(entry("x_name", probe.name)); .containsOnly(entry("x_name", probe.name));
} }
@Test @Test // GH-1192
void countByQuerySimpleValidTest() { void countByQuerySimpleValidTest() {
final SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class);
SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class);
DummyEntity probe = new DummyEntity(); DummyEntity probe = new DummyEntity();
probe.name = "Diego"; probe.name = "Diego";
@ -803,9 +805,10 @@ class SqlGeneratorUnitTests {
.containsOnly(entry("x_name", probe.name)); .containsOnly(entry("x_name", probe.name));
} }
@Test @Test // GH-1192
void selectByQueryPaginationValidTest() { void selectByQueryPaginationValidTest() {
final SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class);
SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class);
DummyEntity probe = new DummyEntity(); DummyEntity probe = new DummyEntity();
probe.name = "Diego"; probe.name = "Diego";
@ -829,6 +832,7 @@ class SqlGeneratorUnitTests {
.containsOnly(entry("x_name", probe.name)); .containsOnly(entry("x_name", probe.name));
} }
@Nullable
private SqlIdentifier getAlias(Object maybeAliased) { private SqlIdentifier getAlias(Object maybeAliased) {
if (maybeAliased instanceof Aliased) { if (maybeAliased instanceof Aliased) {

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

@ -31,6 +31,7 @@ import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@ -567,7 +568,7 @@ public class JdbcRepositoryIntegrationTests {
@Test // GH-987 @Test // GH-987
void queryBySimpleReference() { void queryBySimpleReference() {
final DummyEntity one = repository.save(createDummyEntity()); DummyEntity one = repository.save(createDummyEntity());
DummyEntity two = createDummyEntity(); DummyEntity two = createDummyEntity();
two.ref = AggregateReference.to(one.idProp); two.ref = AggregateReference.to(one.idProp);
two = repository.save(two); two = repository.save(two);
@ -580,7 +581,7 @@ public class JdbcRepositoryIntegrationTests {
@Test // GH-987 @Test // GH-987
void queryByAggregateReference() { void queryByAggregateReference() {
final DummyEntity one = repository.save(createDummyEntity()); DummyEntity one = repository.save(createDummyEntity());
DummyEntity two = createDummyEntity(); DummyEntity two = createDummyEntity();
two.ref = AggregateReference.to(one.idProp); two.ref = AggregateReference.to(one.idProp);
two = repository.save(two); two = repository.save(two);
@ -696,59 +697,28 @@ public class JdbcRepositoryIntegrationTests {
assertIsEqualToWithNonNullIds(reloadedRoots.get(1), root2); assertIsEqualToWithNonNullIds(reloadedRoots.get(1), root2);
} }
private Root createRoot(String namePrefix) { @Test // GH-1192
return new Root(null, namePrefix,
new Intermediate(null, namePrefix + "Intermediate", new Leaf(null, namePrefix + "Leaf"), emptyList()),
singletonList(new Intermediate(null, namePrefix + "QualifiedIntermediate", null,
singletonList(new Leaf(null, namePrefix + "QualifiedLeaf")))));
}
private void assertIsEqualToWithNonNullIds(Root reloadedRoot1, Root root1) {
assertThat(reloadedRoot1.id).isNotNull();
assertThat(reloadedRoot1.name).isEqualTo(root1.name);
assertThat(reloadedRoot1.intermediate.id).isNotNull();
assertThat(reloadedRoot1.intermediate.name).isEqualTo(root1.intermediate.name);
assertThat(reloadedRoot1.intermediates.get(0).id).isNotNull();
assertThat(reloadedRoot1.intermediates.get(0).name).isEqualTo(root1.intermediates.get(0).name);
assertThat(reloadedRoot1.intermediate.leaf.id).isNotNull();
assertThat(reloadedRoot1.intermediate.leaf.name).isEqualTo(root1.intermediate.leaf.name);
assertThat(reloadedRoot1.intermediates.get(0).leaves.get(0).id).isNotNull();
assertThat(reloadedRoot1.intermediates.get(0).leaves.get(0).name)
.isEqualTo(root1.intermediates.get(0).leaves.get(0).name);
}
@Test
void findOneByExampleShouldGetOne() { void findOneByExampleShouldGetOne() {
DummyEntity dummyEntity1 = createDummyEntity(); DummyEntity dummyEntity1 = createDummyEntity();
dummyEntity1.setFlag(true); dummyEntity1.setFlag(true);
repository.save(dummyEntity1); repository.save(dummyEntity1);
DummyEntity dummyEntity2 = createDummyEntity(); DummyEntity dummyEntity2 = createDummyEntity();
dummyEntity2.setName("Diego"); dummyEntity2.setName("Diego");
repository.save(dummyEntity2); repository.save(dummyEntity2);
Example<DummyEntity> diegoExample = Example.of(new DummyEntity("Diego")); Example<DummyEntity> diegoExample = Example.of(new DummyEntity("Diego"));
Optional<DummyEntity> foundExampleDiego = repository.findOne(diegoExample); Optional<DummyEntity> foundExampleDiego = repository.findOne(diegoExample);
assertThat(foundExampleDiego).isPresent();
assertThat(foundExampleDiego.get()).isNotNull();
assertThat(foundExampleDiego.get().getName()).isEqualTo("Diego"); assertThat(foundExampleDiego.get().getName()).isEqualTo("Diego");
} }
@Test @Test // GH-1192
void findOneByExampleMultipleMatchShouldGetOne() { void findOneByExampleMultipleMatchShouldGetOne() {
DummyEntity dummyEntity1 = createDummyEntity(); repository.save(createDummyEntity());
repository.save(dummyEntity1); repository.save(createDummyEntity());
DummyEntity dummyEntity2 = createDummyEntity();
repository.save(dummyEntity2);
Example<DummyEntity> example = Example.of(createDummyEntity()); Example<DummyEntity> example = Example.of(createDummyEntity());
@ -756,12 +726,11 @@ public class JdbcRepositoryIntegrationTests {
.hasMessageContaining("expected 1, actual 2"); .hasMessageContaining("expected 1, actual 2");
} }
@Test @Test // GH-1192
void findOneByExampleShouldGetNone() { void findOneByExampleShouldGetNone() {
DummyEntity dummyEntity1 = createDummyEntity(); DummyEntity dummyEntity1 = createDummyEntity();
dummyEntity1.setFlag(true); dummyEntity1.setFlag(true);
repository.save(dummyEntity1); repository.save(dummyEntity1);
Example<DummyEntity> diegoExample = Example.of(new DummyEntity("NotExisting")); Example<DummyEntity> diegoExample = Example.of(new DummyEntity("NotExisting"));
@ -771,51 +740,42 @@ public class JdbcRepositoryIntegrationTests {
assertThat(foundExampleDiego).isNotPresent(); assertThat(foundExampleDiego).isNotPresent();
} }
@Test @Test // GH-1192
void findAllByExampleShouldGetOne() { void findAllByExampleShouldGetOne() {
DummyEntity dummyEntity1 = createDummyEntity(); DummyEntity dummyEntity1 = createDummyEntity();
dummyEntity1.setFlag(true); dummyEntity1.setFlag(true);
repository.save(dummyEntity1); repository.save(dummyEntity1);
DummyEntity dummyEntity2 = createDummyEntity(); DummyEntity dummyEntity2 = createDummyEntity();
dummyEntity2.setName("Diego"); dummyEntity2.setName("Diego");
repository.save(dummyEntity2); repository.save(dummyEntity2);
Example<DummyEntity> example = Example.of(new DummyEntity("Diego")); Example<DummyEntity> example = Example.of(new DummyEntity("Diego"));
Iterable<DummyEntity> allFound = repository.findAll(example); Iterable<DummyEntity> allFound = repository.findAll(example);
assertThat(allFound) // assertThat(allFound).extracting(DummyEntity::getName) //
.isNotNull() //
.hasSize(1) //
.extracting(DummyEntity::getName) //
.containsExactly(example.getProbe().getName()); .containsExactly(example.getProbe().getName());
} }
@Test @Test // GH-1192
void findAllByExampleMultipleMatchShouldGetOne() { void findAllByExampleMultipleMatchShouldGetOne() {
DummyEntity dummyEntity1 = createDummyEntity(); repository.save(createDummyEntity());
repository.save(dummyEntity1); repository.save(createDummyEntity());
DummyEntity dummyEntity2 = createDummyEntity();
repository.save(dummyEntity2);
Example<DummyEntity> example = Example.of(createDummyEntity()); Example<DummyEntity> example = Example.of(createDummyEntity());
Iterable<DummyEntity> allFound = repository.findAll(example); Iterable<DummyEntity> allFound = repository.findAll(example);
assertThat(allFound) // assertThat(allFound) //
.isNotNull() //
.hasSize(2) // .hasSize(2) //
.extracting(DummyEntity::getName) // .extracting(DummyEntity::getName) //
.containsOnly(example.getProbe().getName()); .containsOnly(example.getProbe().getName());
} }
@Test @Test // GH-1192
void findAllByExampleShouldGetNone() { void findAllByExampleShouldGetNone() {
DummyEntity dummyEntity1 = createDummyEntity(); DummyEntity dummyEntity1 = createDummyEntity();
@ -827,12 +787,10 @@ public class JdbcRepositoryIntegrationTests {
Iterable<DummyEntity> allFound = repository.findAll(example); Iterable<DummyEntity> allFound = repository.findAll(example);
assertThat(allFound) // assertThat(allFound).isEmpty();
.isNotNull() //
.isEmpty();
} }
@Test @Test // GH-1192
void findAllByExamplePageableShouldGetOne() { void findAllByExamplePageableShouldGetOne() {
DummyEntity dummyEntity1 = createDummyEntity(); DummyEntity dummyEntity1 = createDummyEntity();
@ -850,21 +808,15 @@ public class JdbcRepositoryIntegrationTests {
Iterable<DummyEntity> allFound = repository.findAll(example, pageRequest); Iterable<DummyEntity> allFound = repository.findAll(example, pageRequest);
assertThat(allFound) // assertThat(allFound).extracting(DummyEntity::getName) //
.isNotNull() //
.hasSize(1) //
.extracting(DummyEntity::getName) //
.containsExactly(example.getProbe().getName()); .containsExactly(example.getProbe().getName());
} }
@Test @Test // GH-1192
void findAllByExamplePageableMultipleMatchShouldGetOne() { void findAllByExamplePageableMultipleMatchShouldGetOne() {
DummyEntity dummyEntity1 = createDummyEntity(); repository.save(createDummyEntity());
repository.save(dummyEntity1); repository.save(createDummyEntity());
DummyEntity dummyEntity2 = createDummyEntity();
repository.save(dummyEntity2);
Example<DummyEntity> example = Example.of(createDummyEntity()); Example<DummyEntity> example = Example.of(createDummyEntity());
Pageable pageRequest = PageRequest.of(0, 10); Pageable pageRequest = PageRequest.of(0, 10);
@ -872,13 +824,12 @@ public class JdbcRepositoryIntegrationTests {
Iterable<DummyEntity> allFound = repository.findAll(example, pageRequest); Iterable<DummyEntity> allFound = repository.findAll(example, pageRequest);
assertThat(allFound) // assertThat(allFound) //
.isNotNull() //
.hasSize(2) // .hasSize(2) //
.extracting(DummyEntity::getName) // .extracting(DummyEntity::getName) //
.containsOnly(example.getProbe().getName()); .containsOnly(example.getProbe().getName());
} }
@Test @Test // GH-1192
void findAllByExamplePageableShouldGetNone() { void findAllByExamplePageableShouldGetNone() {
DummyEntity dummyEntity1 = createDummyEntity(); DummyEntity dummyEntity1 = createDummyEntity();
@ -891,19 +842,14 @@ public class JdbcRepositoryIntegrationTests {
Iterable<DummyEntity> allFound = repository.findAll(example, pageRequest); Iterable<DummyEntity> allFound = repository.findAll(example, pageRequest);
assertThat(allFound) // assertThat(allFound).isEmpty();
.isNotNull() //
.isEmpty();
} }
@Test @Test // GH-1192
void findAllByExamplePageableOutsidePageShouldGetNone() { void findAllByExamplePageableOutsidePageShouldGetNone() {
DummyEntity dummyEntity1 = createDummyEntity(); repository.save(createDummyEntity());
repository.save(dummyEntity1); repository.save(createDummyEntity());
DummyEntity dummyEntity2 = createDummyEntity();
repository.save(dummyEntity2);
Example<DummyEntity> example = Example.of(createDummyEntity()); Example<DummyEntity> example = Example.of(createDummyEntity());
Pageable pageRequest = PageRequest.of(10, 10); Pageable pageRequest = PageRequest.of(10, 10);
@ -915,7 +861,7 @@ public class JdbcRepositoryIntegrationTests {
.isEmpty(); .isEmpty();
} }
@ParameterizedTest @ParameterizedTest // GH-1192
@MethodSource("findAllByExamplePageableSource") @MethodSource("findAllByExamplePageableSource")
void findAllByExamplePageable(Pageable pageRequest, int size, int totalPages, List<String> notContains) { void findAllByExamplePageable(Pageable pageRequest, int size, int totalPages, List<String> notContains) {
@ -964,17 +910,15 @@ public class JdbcRepositoryIntegrationTests {
); );
} }
@Test @Test // GH-1192
void existsByExampleShouldGetOne() { void existsByExampleShouldGetOne() {
DummyEntity dummyEntity1 = createDummyEntity(); DummyEntity dummyEntity1 = createDummyEntity();
dummyEntity1.setFlag(true); dummyEntity1.setFlag(true);
repository.save(dummyEntity1); repository.save(dummyEntity1);
DummyEntity dummyEntity2 = createDummyEntity(); DummyEntity dummyEntity2 = createDummyEntity();
dummyEntity2.setName("Diego"); dummyEntity2.setName("Diego");
repository.save(dummyEntity2); repository.save(dummyEntity2);
Example<DummyEntity> example = Example.of(new DummyEntity("Diego")); Example<DummyEntity> example = Example.of(new DummyEntity("Diego"));
@ -984,7 +928,7 @@ public class JdbcRepositoryIntegrationTests {
assertThat(exists).isTrue(); assertThat(exists).isTrue();
} }
@Test @Test // GH-1192
void existsByExampleMultipleMatchShouldGetOne() { void existsByExampleMultipleMatchShouldGetOne() {
DummyEntity dummyEntity1 = createDummyEntity(); DummyEntity dummyEntity1 = createDummyEntity();
@ -999,7 +943,7 @@ public class JdbcRepositoryIntegrationTests {
assertThat(exists).isTrue(); assertThat(exists).isTrue();
} }
@Test @Test // GH-1192
void existsByExampleShouldGetNone() { void existsByExampleShouldGetNone() {
DummyEntity dummyEntity1 = createDummyEntity(); DummyEntity dummyEntity1 = createDummyEntity();
@ -1014,17 +958,17 @@ public class JdbcRepositoryIntegrationTests {
assertThat(exists).isFalse(); assertThat(exists).isFalse();
} }
@Test @Test // GH-1192
void existsByExampleComplex() { void existsByExampleComplex() {
final Instant pointInTime = Instant.now().minusSeconds(10000); Instant pointInTime = Instant.now().truncatedTo(ChronoUnit.MILLIS).minusSeconds(10000);
final DummyEntity one = repository.save(createDummyEntity()); repository.save(createDummyEntity());
DummyEntity two = createDummyEntity(); DummyEntity two = createDummyEntity();
two.setName("Diego"); two.setName("Diego");
two.setPointInTime(pointInTime); two.setPointInTime(pointInTime);
two = repository.save(two); repository.save(two);
DummyEntity exampleEntitiy = createDummyEntity(); DummyEntity exampleEntitiy = createDummyEntity();
exampleEntitiy.setName("Diego"); exampleEntitiy.setName("Diego");
@ -1036,7 +980,7 @@ public class JdbcRepositoryIntegrationTests {
assertThat(exists).isTrue(); assertThat(exists).isTrue();
} }
@Test @Test // GH-1192
void countByExampleShouldGetOne() { void countByExampleShouldGetOne() {
DummyEntity dummyEntity1 = createDummyEntity(); DummyEntity dummyEntity1 = createDummyEntity();
@ -1056,7 +1000,7 @@ public class JdbcRepositoryIntegrationTests {
assertThat(count).isOne(); assertThat(count).isOne();
} }
@Test @Test // GH-1192
void countByExampleMultipleMatchShouldGetOne() { void countByExampleMultipleMatchShouldGetOne() {
DummyEntity dummyEntity1 = createDummyEntity(); DummyEntity dummyEntity1 = createDummyEntity();
@ -1071,7 +1015,7 @@ public class JdbcRepositoryIntegrationTests {
assertThat(count).isEqualTo(2); assertThat(count).isEqualTo(2);
} }
@Test @Test // GH-1192
void countByExampleShouldGetNone() { void countByExampleShouldGetNone() {
DummyEntity dummyEntity1 = createDummyEntity(); DummyEntity dummyEntity1 = createDummyEntity();
@ -1086,17 +1030,16 @@ public class JdbcRepositoryIntegrationTests {
assertThat(count).isNotNull().isZero(); assertThat(count).isNotNull().isZero();
} }
@Test @Test // GH-1192
void countByExampleComplex() { void countByExampleComplex() {
final Instant pointInTime = Instant.now().minusSeconds(10000); Instant pointInTime = Instant.now().minusSeconds(10000).truncatedTo(ChronoUnit.MILLIS);
repository.save(createDummyEntity());
final DummyEntity one = repository.save(createDummyEntity());
DummyEntity two = createDummyEntity(); DummyEntity two = createDummyEntity();
two.setName("Diego"); two.setName("Diego");
two.setPointInTime(pointInTime); two.setPointInTime(pointInTime);
two = repository.save(two); repository.save(two);
DummyEntity exampleEntitiy = createDummyEntity(); DummyEntity exampleEntitiy = createDummyEntity();
exampleEntitiy.setName("Diego"); exampleEntitiy.setName("Diego");
@ -1108,11 +1051,11 @@ public class JdbcRepositoryIntegrationTests {
assertThat(count).isOne(); assertThat(count).isOne();
} }
@Test @Test // GH-1192
void fetchByExampleFluentAllSimple() { void fetchByExampleFluentAllSimple() {
String searchName = "Diego";
Instant now = Instant.now(); String searchName = "Diego";
Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
final DummyEntity one = repository.save(createDummyEntity()); final DummyEntity one = repository.save(createDummyEntity());
@ -1121,11 +1064,17 @@ public class JdbcRepositoryIntegrationTests {
two.setName(searchName); two.setName(searchName);
two.setPointInTime(now.minusSeconds(10000)); two.setPointInTime(now.minusSeconds(10000));
two = repository.save(two); two = repository.save(two);
// certain databases consider it a great idea to assign default values to timestamp fields.
// I'm looking at you MariaDb.
two = repository.findById(two.idProp).orElseThrow();
DummyEntity third = createDummyEntity(); DummyEntity third = createDummyEntity();
third.setName(searchName); third.setName(searchName);
third.setPointInTime(now.minusSeconds(200000)); third.setPointInTime(now.minusSeconds(200000));
third = repository.save(third); third = repository.save(third);
// certain databases consider it a great idea to assign default values to timestamp fields.
// I'm looking at you MariaDb.
third = repository.findById(third.idProp).orElseThrow();
DummyEntity exampleEntitiy = createDummyEntity(); DummyEntity exampleEntitiy = createDummyEntity();
exampleEntitiy.setName(searchName); exampleEntitiy.setName(searchName);
@ -1133,28 +1082,27 @@ public class JdbcRepositoryIntegrationTests {
Example<DummyEntity> example = Example.of(exampleEntitiy); Example<DummyEntity> example = Example.of(exampleEntitiy);
List<DummyEntity> matches = repository.findBy(example, p -> p.sortBy(Sort.by("pointInTime").descending()).all()); List<DummyEntity> matches = repository.findBy(example, p -> p.sortBy(Sort.by("pointInTime").descending()).all());
assertThat(matches).hasSize(2).contains(two, third); assertThat(matches).containsExactly(two, third);
assertThat(matches.get(0)).isEqualTo(two);
} }
@Test @Test // GH-1192
void fetchByExampleFluentCountSimple() { void fetchByExampleFluentCountSimple() {
String searchName = "Diego";
String searchName = "Diego";
Instant now = Instant.now(); Instant now = Instant.now();
final DummyEntity one = repository.save(createDummyEntity()); repository.save(createDummyEntity());
DummyEntity two = createDummyEntity(); DummyEntity two = createDummyEntity();
two.setName(searchName); two.setName(searchName);
two.setPointInTime(now.minusSeconds(10000)); two.setPointInTime(now.minusSeconds(10000));
two = repository.save(two); repository.save(two);
DummyEntity third = createDummyEntity(); DummyEntity third = createDummyEntity();
third.setName(searchName); third.setName(searchName);
third.setPointInTime(now.minusSeconds(200000)); third.setPointInTime(now.minusSeconds(200000));
third = repository.save(third); repository.save(third);
DummyEntity exampleEntitiy = createDummyEntity(); DummyEntity exampleEntitiy = createDummyEntity();
exampleEntitiy.setName(searchName); exampleEntitiy.setName(searchName);
@ -1165,53 +1113,56 @@ public class JdbcRepositoryIntegrationTests {
assertThat(matches).isEqualTo(2); assertThat(matches).isEqualTo(2);
} }
@Test @Test // GH-1192
void fetchByExampleFluentOnlyInstantFirstSimple() { void fetchByExampleFluentOnlyInstantFirstSimple() {
String searchName = "Diego";
Instant now = Instant.now(); String searchName = "Diego";
Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
final DummyEntity one = repository.save(createDummyEntity()); repository.save(createDummyEntity());
DummyEntity two = createDummyEntity(); DummyEntity two = createDummyEntity();
two.setName(searchName); two.setName(searchName);
two.setPointInTime(now.minusSeconds(10000)); two.setPointInTime(now.minusSeconds(10000));
two = repository.save(two); two = repository.save(two);
// certain databases consider it a great idea to assign default values to timestamp fields.
// I'm looking at you MariaDb.
two = repository.findById(two.idProp).orElseThrow();
DummyEntity third = createDummyEntity(); DummyEntity third = createDummyEntity();
third.setName(searchName); third.setName(searchName);
third.setPointInTime(now.minusSeconds(200000)); third.setPointInTime(now.minusSeconds(200000));
third = repository.save(third); repository.save(third);
DummyEntity exampleEntitiy = createDummyEntity(); DummyEntity exampleEntity = createDummyEntity();
exampleEntitiy.setName(searchName); exampleEntity.setName(searchName);
Example<DummyEntity> example = Example.of(exampleEntitiy); Example<DummyEntity> example = Example.of(exampleEntity);
Optional<DummyEntity> matches = repository.findBy(example, Optional<DummyEntity> matches = repository.findBy(example,
p -> p.sortBy(Sort.by("pointInTime").descending()).first()); p -> p.sortBy(Sort.by("pointInTime").descending()).first());
assertThat(matches).contains(two); assertThat(matches).contains(two);
} }
@Test @Test // GH-1192
void fetchByExampleFluentOnlyInstantOneValueError() { void fetchByExampleFluentOnlyInstantOneValueError() {
String searchName = "Diego";
String searchName = "Diego";
Instant now = Instant.now(); Instant now = Instant.now();
final DummyEntity one = repository.save(createDummyEntity()); repository.save(createDummyEntity());
DummyEntity two = createDummyEntity(); DummyEntity two = createDummyEntity();
two.setName(searchName); two.setName(searchName);
two.setPointInTime(now.minusSeconds(10000)); two.setPointInTime(now.minusSeconds(10000));
two = repository.save(two); repository.save(two);
DummyEntity third = createDummyEntity(); DummyEntity third = createDummyEntity();
third.setName(searchName); third.setName(searchName);
third.setPointInTime(now.minusSeconds(200000)); third.setPointInTime(now.minusSeconds(200000));
third = repository.save(third); repository.save(third);
DummyEntity exampleEntitiy = createDummyEntity(); DummyEntity exampleEntitiy = createDummyEntity();
exampleEntitiy.setName(searchName); exampleEntitiy.setName(searchName);
@ -1222,19 +1173,21 @@ public class JdbcRepositoryIntegrationTests {
.isInstanceOf(IncorrectResultSizeDataAccessException.class).hasMessageContaining("expected 1, actual 2"); .isInstanceOf(IncorrectResultSizeDataAccessException.class).hasMessageContaining("expected 1, actual 2");
} }
@Test @Test // GH-1192
void fetchByExampleFluentOnlyInstantOneValueSimple() { void fetchByExampleFluentOnlyInstantOneValueSimple() {
String searchName = "Diego";
Instant now = Instant.now(); String searchName = "Diego";
Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
final DummyEntity one = repository.save(createDummyEntity()); repository.save(createDummyEntity());
DummyEntity two = createDummyEntity(); DummyEntity two = createDummyEntity();
two.setName(searchName); two.setName(searchName);
two.setPointInTime(now.minusSeconds(10000)); two.setPointInTime(now.minusSeconds(10000));
two = repository.save(two); two = repository.save(two);
// certain databases consider it a great idea to assign default values to timestamp fields.
// I'm looking at you MariaDb.
two = repository.findById(two.idProp).orElseThrow();
DummyEntity exampleEntitiy = createDummyEntity(); DummyEntity exampleEntitiy = createDummyEntity();
exampleEntitiy.setName(searchName); exampleEntitiy.setName(searchName);
@ -1246,30 +1199,52 @@ public class JdbcRepositoryIntegrationTests {
assertThat(match).contains(two); assertThat(match).contains(two);
} }
@Test @Test // GH-1192
void fetchByExampleFluentOnlyInstantOneValueAsSimple() { void fetchByExampleFluentOnlyInstantOneValueAsSimple() {
String searchName = "Diego";
String searchName = "Diego";
Instant now = Instant.now(); Instant now = Instant.now();
final DummyEntity one = repository.save(createDummyEntity()); repository.save(createDummyEntity());
DummyEntity two = createDummyEntity(); DummyEntity two = createDummyEntity();
two.setName(searchName); two.setName(searchName);
two.setPointInTime(now.minusSeconds(10000)); two.setPointInTime(now.minusSeconds(10000));
two = repository.save(two); two = repository.save(two);
DummyEntity exampleEntitiy = createDummyEntity(); DummyEntity exampleEntity = createDummyEntity();
exampleEntitiy.setName(searchName); exampleEntity.setName(searchName);
Example<DummyEntity> example = Example.of(exampleEntitiy); Example<DummyEntity> example = Example.of(exampleEntity);
Optional<DummyProjectExample> match = repository.findBy(example, p -> p.as(DummyProjectExample.class).one()); Optional<DummyProjectExample> match = repository.findBy(example, p -> p.as(DummyProjectExample.class).one());
assertThat(match.get().getName()).contains(two.getName()); assertThat(match.get().getName()).contains(two.getName());
} }
private Root createRoot(String namePrefix) {
return new Root(null, namePrefix,
new Intermediate(null, namePrefix + "Intermediate", new Leaf(null, namePrefix + "Leaf"), emptyList()),
singletonList(new Intermediate(null, namePrefix + "QualifiedIntermediate", null,
singletonList(new Leaf(null, namePrefix + "QualifiedLeaf")))));
}
private void assertIsEqualToWithNonNullIds(Root reloadedRoot1, Root root1) {
assertThat(reloadedRoot1.id).isNotNull();
assertThat(reloadedRoot1.name).isEqualTo(root1.name);
assertThat(reloadedRoot1.intermediate.id).isNotNull();
assertThat(reloadedRoot1.intermediate.name).isEqualTo(root1.intermediate.name);
assertThat(reloadedRoot1.intermediates.get(0).id).isNotNull();
assertThat(reloadedRoot1.intermediates.get(0).name).isEqualTo(root1.intermediates.get(0).name);
assertThat(reloadedRoot1.intermediate.leaf.id).isNotNull();
assertThat(reloadedRoot1.intermediate.leaf.name).isEqualTo(root1.intermediate.leaf.name);
assertThat(reloadedRoot1.intermediates.get(0).leaves.get(0).id).isNotNull();
assertThat(reloadedRoot1.intermediates.get(0).leaves.get(0).name)
.isEqualTo(root1.intermediates.get(0).leaves.get(0).name);
}
private Instant createDummyBeforeAndAfterNow() { private Instant createDummyBeforeAndAfterNow() {
Instant now = Instant.now(); Instant now = Instant.now();

2
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql

@ -7,7 +7,7 @@ CREATE TABLE dummy_entity
( (
id_Prop BIGINT IDENTITY PRIMARY KEY, id_Prop BIGINT IDENTITY PRIMARY KEY,
NAME VARCHAR(100), NAME VARCHAR(100),
POINT_IN_TIME DATETIME, POINT_IN_TIME DATETIME2,
OFFSET_DATE_TIME DATETIMEOFFSET, OFFSET_DATE_TIME DATETIMEOFFSET,
FLAG BIT, FLAG BIT,
REF BIGINT, REF BIGINT,

13
spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java

@ -19,7 +19,10 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Set; import java.util.Set;
import org.springframework.data.relational.core.sql.Functions;
import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.IdentifierProcessing;
import org.springframework.data.relational.core.sql.SQL;
import org.springframework.data.relational.core.sql.SimpleFunction;
import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.data.relational.core.sql.render.SelectRenderContext; import org.springframework.data.relational.core.sql.render.SelectRenderContext;
@ -131,4 +134,14 @@ public interface Dialect {
default OrderByNullPrecedence orderByNullHandling() { default OrderByNullPrecedence orderByNullHandling() {
return OrderByNullPrecedence.SQL_STANDARD; return OrderByNullPrecedence.SQL_STANDARD;
} }
/**
* Provide a SQL function that is suitable for implementing an exists-query.
* The default is `COUNT(1)`, but for some database a `LEAST(COUNT(1), 1)` might be required, which doesn't get accepted by other databases.
*
* @since 3.0
*/
default SimpleFunction getExistsFunction(){
return Functions.count(SQL.literalOf(1));
}
} }

8
spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java

@ -23,10 +23,13 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.springframework.data.relational.core.sql.Functions;
import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.IdentifierProcessing;
import org.springframework.data.relational.core.sql.IdentifierProcessing.LetterCasing; import org.springframework.data.relational.core.sql.IdentifierProcessing.LetterCasing;
import org.springframework.data.relational.core.sql.IdentifierProcessing.Quoting; import org.springframework.data.relational.core.sql.IdentifierProcessing.Quoting;
import org.springframework.data.relational.core.sql.LockOptions; import org.springframework.data.relational.core.sql.LockOptions;
import org.springframework.data.relational.core.sql.SQL;
import org.springframework.data.relational.core.sql.SimpleFunction;
import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.data.relational.core.sql.TableLike; import org.springframework.data.relational.core.sql.TableLike;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -193,4 +196,9 @@ public class PostgresDialect extends AbstractDialect {
action.accept(ClassUtils.resolveClassName(className, PostgresDialect.class.getClassLoader())); action.accept(ClassUtils.resolveClassName(className, PostgresDialect.class.getClassLoader()));
} }
} }
@Override
public SimpleFunction getExistsFunction() {
return Functions.least(Functions.count(SQL.literalOf(1)), SQL.literalOf(1));
}
} }

5
spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java

@ -48,6 +48,11 @@ public class Functions {
return SimpleFunction.create("COUNT", Arrays.asList(columns)); return SimpleFunction.create("COUNT", Arrays.asList(columns));
} }
public static SimpleFunction least(Expression... expressions) {
return SimpleFunction.create("LEAST", Arrays.asList(expressions));
}
/** /**
* Creates a new {@code COUNT} function. * Creates a new {@code COUNT} function.
* *

Loading…
Cancel
Save