From aad40a32b0dcb12efab1d700b554118e039472cb Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Mon, 11 Jul 2022 12:48:45 +0200 Subject: [PATCH] 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 --- .../jdbc/core/JdbcAggregateOperations.java | 8 +- .../data/jdbc/core/JdbcAggregateTemplate.java | 8 +- .../convert/CascadingDataAccessStrategy.java | 6 +- .../jdbc/core/convert/DataAccessStrategy.java | 13 +- .../convert/DefaultDataAccessStrategy.java | 42 ++- .../convert/DelegatingDataAccessStrategy.java | 3 +- .../data/jdbc/core/convert/QueryMapper.java | 39 ++- .../data/jdbc/core/convert/SqlGenerator.java | 51 ++-- .../mybatis/MyBatisDataAccessStrategy.java | 7 - .../FetchableFluentQueryByExample.java | 19 +- .../support/FluentQuerySupport.java | 3 +- .../support/SimpleJdbcRepository.java | 31 ++- .../core/convert/SqlGeneratorUnitTests.java | 22 +- .../JdbcRepositoryIntegrationTests.java | 241 ++++++++---------- .../JdbcRepositoryIntegrationTests-mssql.sql | 2 +- .../data/relational/core/dialect/Dialect.java | 13 + .../core/dialect/PostgresDialect.java | 8 + .../data/relational/core/sql/Functions.java | 5 + 18 files changed, 273 insertions(+), 248 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java index d20dffd3a..1b6c2b512 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java +++ b/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}. * @return exactly one result or {@link Optional#empty()} if no match found. * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. + * @since 3.0 */ Optional selectOne(Query query, Class entityClass); @@ -204,11 +205,11 @@ public interface JdbcAggregateOperations { * * @param query 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. * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. + * @since 3.0 */ - Iterable select(Query query, Class entityClass, Sort sort); + Iterable select(Query query, Class entityClass); /** * 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 entityClass the entity type must not be {@literal null}. * @return {@literal true} if the object exists. + * @since 3.0 */ boolean exists(Query query, Class entityClass); @@ -225,6 +227,7 @@ public interface JdbcAggregateOperations { * @param query 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}. + * @since 3.0 */ long count(Query query, Class entityClass); @@ -236,6 +239,7 @@ public interface JdbcAggregateOperations { * @param entityClass the entity type must not be {@literal null}. * @param pageable can be null. * @return a {@link Page} of entities matching the given {@link Example}. + * @since 3.0 */ Page select(Query query, Class entityClass, Pageable pageable); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java index b07542a5b..8a402a2de 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java +++ b/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 AggregateChangeExecutor executor; - private final JdbcConverter converter; private EntityCallbacks entityCallbacks = EntityCallbacks.create(); @@ -248,7 +247,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { } @Override - public Iterable select(Query query, Class entityClass, Sort sort) { + public Iterable select(Query query, Class entityClass) { return accessStrategy.select(query, entityClass); } @@ -264,16 +263,13 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { @Override public Page select(Query query, Class entityClass, Pageable pageable) { + Iterable items = triggerAfterConvert(accessStrategy.select(query, entityClass, pageable)); List content = StreamSupport.stream(items.spliterator(), false).collect(Collectors.toList()); return PageableExecutionUtils.getPage(content, pageable, () -> accessStrategy.count(query, entityClass)); } - /* - * (non-Javadoc) - * @see org.springframework.data.jdbc.core.JdbcAggregateOperations#findAll(java.lang.Class) - */ @Override public Iterable findAll(Class domainType) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CascadingDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CascadingDataAccessStrategy.java index 94309f875..6fd9c8b9f 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CascadingDataAccessStrategy.java +++ b/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; +import static java.lang.Boolean.*; + import java.util.ArrayList; import java.util.List; 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.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. * * @author Jens Schauder diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java index 5bfc9140a..9f534e6fa 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java +++ b/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 the type of the instance to save. * @return whether the update actually updated a row. - * @throws OptimisticLockingFailureException if the update fails to update at least one row assuming the - * optimistic locking version check failed. + * @throws OptimisticLockingFailureException if the update fails to update at least one row assuming the optimistic + * locking version check failed. * @since 2.0 */ boolean updateWithVersion(T instance, Class 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 * {@code null}. * @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 - * optimistic locking version check failed. + * @throws OptimisticLockingFailureException if the update fails to update at least one row assuming the optimistic + * locking version check failed. * @since 2.0 */ void deleteWithVersion(Object id, Class domainType, Number previousVersion); @@ -292,6 +292,7 @@ public interface DataAccessStrategy extends RelationResolver { * @param probeType the type of entities. Must not be {@code null}. * @return exactly one result or {@link Optional#empty()} if no match found. * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. + * @since 3.0 */ Optional selectOne(Query query, Class probeType); @@ -302,6 +303,7 @@ public interface DataAccessStrategy extends RelationResolver { * @param probeType the type of entities. Must not be {@code null}. * @return a non-null list with all the matching results. * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. + * @since 3.0 */ Iterable select(Query query, Class probeType); @@ -314,6 +316,7 @@ public interface DataAccessStrategy extends RelationResolver { * @param pageable the pagination that should be applied. Must not be {@literal null}. * @return a non-null list with all the matching results. * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. + * @since 3.0 */ Iterable select(Query query, Class probeType, Pageable pageable); @@ -323,6 +326,7 @@ public interface DataAccessStrategy extends RelationResolver { * @param query must not be {@literal null}. * @param probeType the type of entities. Must not be {@code null}. * @return {@literal true} if the object exists. + * @since 3.0 */ boolean exists(Query query, Class 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 query the query which elements have to match. * @return the count. Guaranteed to be not {@code null}. + * @since 3.0 */ long count(Query query, Class probeType); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java index 3dcfc550a..6101ccf6f 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java +++ b/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.util.Collections; import java.util.List; -import java.util.Map; import java.util.Optional; -import java.util.function.Predicate; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.OptimisticLockingFailureException; @@ -262,27 +260,24 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { } @Override - @SuppressWarnings("unchecked") public T findById(Object id, Class domainType) { String findOneSql = sql(domainType).getFindOne(); SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER); try { - return operations.queryForObject(findOneSql, parameter, (RowMapper) getEntityRowMapper(domainType)); + return operations.queryForObject(findOneSql, parameter, getEntityRowMapper(domainType)); } catch (EmptyResultDataAccessException e) { return null; } } @Override - @SuppressWarnings("unchecked") public Iterable findAll(Class domainType) { - return operations.query(sql(domainType).getFindAll(), (RowMapper) getEntityRowMapper(domainType)); + return operations.query(sql(domainType).getFindAll(), getEntityRowMapper(domainType)); } @Override - @SuppressWarnings("unchecked") public Iterable findAllById(Iterable ids, Class domainType) { if (!ids.iterator().hasNext()) { @@ -293,7 +288,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { String findAllInListSql = sql(domainType).getFindAllInList(); - return operations.query(findAllInListSql, parameterSource, (RowMapper) getEntityRowMapper(domainType)); + return operations.query(findAllInListSql, parameterSource, getEntityRowMapper(domainType)); } @Override @@ -330,73 +325,74 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { } @Override - @SuppressWarnings("unchecked") public Iterable findAll(Class domainType, Sort sort) { - return operations.query(sql(domainType).getFindAll(sort), (RowMapper) getEntityRowMapper(domainType)); + return operations.query(sql(domainType).getFindAll(sort), getEntityRowMapper(domainType)); } @Override - @SuppressWarnings("unchecked") public Iterable findAll(Class domainType, Pageable pageable) { - return operations.query(sql(domainType).getFindAll(pageable), (RowMapper) getEntityRowMapper(domainType)); + return operations.query(sql(domainType).getFindAll(pageable), getEntityRowMapper(domainType)); } @Override public Optional selectOne(Query query, Class probeType) { + MapSqlParameterSource parameterSource = new MapSqlParameterSource(); String sqlQuery = sql(probeType).selectByQuery(query, parameterSource); - T foundObject; try { - foundObject = operations.queryForObject(sqlQuery, parameterSource, (RowMapper) getEntityRowMapper(probeType)); + return Optional.ofNullable( + operations.queryForObject(sqlQuery, parameterSource, getEntityRowMapper(probeType))); } catch (EmptyResultDataAccessException e) { - foundObject = null; + return Optional.empty(); } - - return Optional.ofNullable(foundObject); } @Override public Iterable select(Query query, Class probeType) { + MapSqlParameterSource parameterSource = new MapSqlParameterSource(); String sqlQuery = sql(probeType).selectByQuery(query, parameterSource); - return operations.query(sqlQuery, parameterSource, (RowMapper) getEntityRowMapper(probeType)); + return operations.query(sqlQuery, parameterSource, getEntityRowMapper(probeType)); } @Override public Iterable select(Query query, Class probeType, Pageable pageable) { + MapSqlParameterSource parameterSource = new MapSqlParameterSource(); String sqlQuery = sql(probeType).selectByQuery(query, parameterSource, pageable); - return operations.query(sqlQuery, parameterSource, (RowMapper) getEntityRowMapper(probeType)); + return operations.query(sqlQuery, parameterSource, getEntityRowMapper(probeType)); } @Override public boolean exists(Query query, Class probeType) { - MapSqlParameterSource parameterSource = new MapSqlParameterSource(); + MapSqlParameterSource parameterSource = new MapSqlParameterSource(); String sqlQuery = sql(probeType).existsByQuery(query, parameterSource); 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; } @Override public long count(Query query, Class probeType) { + MapSqlParameterSource parameterSource = new MapSqlParameterSource(); String sqlQuery = sql(probeType).countByQuery(query, parameterSource); 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; } - private EntityRowMapper getEntityRowMapper(Class domainType) { + private EntityRowMapper getEntityRowMapper(Class domainType) { return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), converter); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DelegatingDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DelegatingDataAccessStrategy.java index 0866c54b7..bdbba665a 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DelegatingDataAccessStrategy.java +++ b/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; import java.util.List; +import java.util.Optional; import org.springframework.data.domain.Pageable; 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.util.Assert; -import java.util.Optional; - /** * Delegates all method calls to an instance set after construction. This is useful for {@link DataAccessStrategy}s with * cyclic dependencies. diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/QueryMapper.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/QueryMapper.java index fd33c061f..c32c37482 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/QueryMapper.java +++ b/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 Jens Schauder - * @since 2.0 + * @since 3.0 */ public class QueryMapper { @@ -85,7 +85,7 @@ public class QueryMapper { * * @param sort must not 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 getMappedSort(Table table, Sort sort, @Nullable RelationalPersistentEntity entity) { @@ -116,9 +116,8 @@ public class QueryMapper { return expression; } - if (expression instanceof Column) { + if (expression instanceof Column column) { - Column column = (Column) expression; Field field = createPropertyField(entity, column.getName()); TableLike table = column.getTable(); @@ -128,9 +127,7 @@ public class QueryMapper { return column instanceof Aliased ? columnFromTable.as(((Aliased) column).getAlias()) : columnFromTable; } - if (expression instanceof SimpleFunction) { - - SimpleFunction function = (SimpleFunction) expression; + if (expression instanceof SimpleFunction function) { List arguments = function.getExpressions(); List mappedArguments = new ArrayList<>(arguments.size()); @@ -280,9 +277,7 @@ public class QueryMapper { Object mappedValue; SQLType sqlType; - if (criteria.getValue() instanceof JdbcValue) { - - JdbcValue settableValue = (JdbcValue) criteria.getValue(); + if (criteria.getValue() instanceof JdbcValue settableValue) { mappedValue = convertValue(settableValue.getValue(), propertyField.getTypeHint()); sqlType = getTypeHint(mappedValue, actualType.getType(), settableValue); @@ -554,40 +549,39 @@ public class QueryMapper { String refName = column.getName().getReference(); switch (comparator) { - case EQ: { + case EQ -> { Expression expression = bind(mappedValue, sqlType, parameterSource, refName, ignoreCase); return Conditions.isEqual(columnExpression, expression); } - case NEQ: { + case NEQ -> { Expression expression = bind(mappedValue, sqlType, parameterSource, refName, ignoreCase); return Conditions.isEqual(columnExpression, expression).not(); } - case LT: { + case LT -> { Expression expression = bind(mappedValue, sqlType, parameterSource, refName); return column.isLess(expression); } - case LTE: { + case LTE -> { Expression expression = bind(mappedValue, sqlType, parameterSource, refName); return column.isLessOrEqualTo(expression); } - case GT: { + case GT -> { Expression expression = bind(mappedValue, sqlType, parameterSource, refName); return column.isGreater(expression); } - case GTE: { + case GTE -> { Expression expression = bind(mappedValue, sqlType, parameterSource, refName); return column.isGreaterOrEqualTo(expression); } - case LIKE: { + case LIKE -> { Expression expression = bind(mappedValue, sqlType, parameterSource, refName, ignoreCase); return Conditions.like(columnExpression, expression); } - case NOT_LIKE: { + case NOT_LIKE -> { Expression expression = bind(mappedValue, sqlType, parameterSource, refName, ignoreCase); return Conditions.notLike(columnExpression, expression); } - default: - throw new UnsupportedOperationException("Comparator " + comparator + " not supported"); + default -> 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. * - * @return + * @return the key to be used in the mapped document eventually. */ public SqlIdentifier getMappedColumnName() { return this.name; @@ -774,9 +768,6 @@ public class QueryMapper { /** * Returns the {@link PersistentPropertyPath} for the given {@code pathExpression}. - * - * @param pathExpression - * @return */ @Nullable private PersistentPropertyPath getPath(String pathExpression) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java index c3110c0fa..6a13c5038 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java @@ -88,6 +88,7 @@ class SqlGenerator { private final Lazy deleteByIdAndVersionSql = Lazy.of(this::createDeleteByIdAndVersionSql); private final Lazy deleteByListSql = Lazy.of(this::createDeleteByListSql); private final QueryMapper queryMapper; + private final Dialect dialect; /** * Create a new {@link SqlGenerator} given {@link RelationalMappingContext} and {@link RelationalPersistentEntity}. @@ -107,6 +108,7 @@ class SqlGenerator { this.sqlRenderer = SqlRenderer.create(renderContext); this.columns = new Columns(entity, mappingContext, converter); this.queryMapper = new QueryMapper(dialect, converter); + this.dialect = dialect; } /** @@ -668,15 +670,6 @@ class SqlGenerator { 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) { return Delete.builder().from(table) @@ -827,8 +820,7 @@ class SqlGenerator { */ public String existsByQuery(Query query, MapSqlParameterSource parameterSource) { - Expression idColumn = getIdColumn(); - SelectBuilder.SelectJoin baseSelect = getSelectCountWithExpression(idColumn); + SelectBuilder.SelectJoin baseSelect = getExistsSelect(); Select select = applyQueryOnSelect(query, parameterSource, (SelectBuilder.SelectWhere) baseSelect) // .build(); @@ -855,6 +847,36 @@ class SqlGenerator { return render(select); } + /** + * Generates a {@link org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin} with a + * COUNT(...) where the countExpressions 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 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 * COUNT(...) where the countExpressions are the parameters of the count. @@ -869,11 +891,10 @@ class SqlGenerator { Assert.state(countExpressions.length >= 1, "countExpressions must contain at least one expression"); 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 for (PersistentPropertyPath path : mappingContext diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java index b4256cb4b..ec1c3ba1c 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java +++ b/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.Map; 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.mybatis.spring.SqlSessionTemplate; import org.springframework.dao.EmptyResultDataAccessException; @@ -352,10 +349,6 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { throw new UnsupportedOperationException("Not implemented"); } - /* - * (non-Javadoc) - * @see org.springframework.data.jdbc.core.DataAccessStrategy#count(java.lang.Class) - */ @Override public long count(Class domainType) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FetchableFluentQueryByExample.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FetchableFluentQueryByExample.java index 9c6aebddc..803d2b246 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FetchableFluentQueryByExample.java +++ b/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}. * * @author Diego Krupitza + * @since 3.0 */ class FetchableFluentQueryByExample extends FluentQuerySupport { @@ -47,40 +48,47 @@ class FetchableFluentQueryByExample extends FluentQuerySupport { FetchableFluentQueryByExample(Example example, Sort sort, Class resultType, List fieldsToInclude, RelationalExampleMapper exampleMapper, JdbcAggregateOperations entityOperations) { + super(example, sort, resultType, fieldsToInclude); + this.exampleMapper = exampleMapper; this.entityOperations = entityOperations; } @Override public R oneValue() { + return this.entityOperations.selectOne(createQuery(), getExampleType()) .map(item -> this.getConversionFunction().apply(item)).get(); } @Override public R firstValue() { + return this.getConversionFunction() - .apply(this.entityOperations.select(createQuery(), getExampleType(), getSort()).iterator().next()); + .apply(this.entityOperations.select(createQuery().sort(getSort()), getExampleType()).iterator().next()); } @Override public List all() { + 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()); } @Override public Page page(Pageable pageable) { + return this.entityOperations.select(createQuery(p -> p.with(pageable)), getExampleType(), pageable) .map(item -> this.getConversionFunction().apply(item)); } @Override public Stream stream() { + 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)); } @@ -102,10 +110,6 @@ class FetchableFluentQueryByExample extends FluentQuerySupport { Query query = exampleMapper.getMappedExample(getExample()); - if (getSort().isSorted()) { - query = query.sort(getSort()); - } - if (!getFieldsToInclude().isEmpty()) { query = query.columns(getFieldsToInclude().toArray(new String[0])); } @@ -118,6 +122,7 @@ class FetchableFluentQueryByExample extends FluentQuerySupport { @Override protected FluentQuerySupport create(Example example, Sort sort, Class resultType, List fieldsToInclude) { + return new FetchableFluentQueryByExample<>(example, sort, resultType, fieldsToInclude, this.exampleMapper, this.entityOperations); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FluentQuerySupport.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FluentQuerySupport.java index 50506b505..2603e129b 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FluentQuerySupport.java +++ b/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. * * @author Diego Krupitza + * @since 3.0 */ abstract class FluentQuerySupport implements FluentQuery.FetchableFluentQuery { @@ -42,6 +43,7 @@ abstract class FluentQuerySupport implements FluentQuery.FetchableFluentQu private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); FluentQuerySupport(Example example, Sort sort, Class resultType, List fieldsToInclude) { + this.example = example; this.sort = sort; this.resultType = resultType; @@ -123,5 +125,4 @@ abstract class FluentQuerySupport implements FluentQuery.FetchableFluentQu protected Function getConversionFunction() { return getConversionFunction(this.example.getProbeType(), getResultType()); } - } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java index 85de944ce..98fe6a865 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java +++ b/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.function.Function; -import java.util.stream.Collectors; import org.springframework.data.domain.Example; 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.convert.JdbcConverter; 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.repository.CrudRepository; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.FluentQuery; import org.springframework.data.repository.query.QueryByExampleExecutor; +import org.springframework.data.support.PageableExecutionUtils; import org.springframework.data.util.Streamable; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; @@ -141,50 +142,58 @@ public class SimpleJdbcRepository @Override public Optional findOne(Example 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()); } @Override public Iterable findAll(Example example) { - Assert.notNull(example, "Example must not be null!"); + + Assert.notNull(example, "Example must not be null"); return findAll(example, Sort.unsorted()); } @Override public Iterable findAll(Example 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 public Page findAll(Example 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); } @Override public long count(Example 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()); } @Override public boolean exists(Example 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()); } @Override public R findBy(Example example, Function, 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 fluentQuery = new FetchableFluentQueryByExample<>(example, example.getProbeType(), this.exampleMapper, this.entityOperations); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java index 9721ccd3c..c34eddbe1 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java +++ b/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")); } - @Nullable - @Test + @Test // GH-1192 void selectByQueryValidTest() { - final SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class); + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class); DummyEntity probe = new DummyEntity(); probe.name = "Diego"; @@ -762,9 +762,10 @@ class SqlGeneratorUnitTests { .containsOnly(entry("x_name", probe.name)); } - @Test + @Test // GH-1192 void existsByQuerySimpleValidTest() { - final SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class); + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class); DummyEntity probe = new DummyEntity(); probe.name = "Diego"; @@ -781,9 +782,10 @@ class SqlGeneratorUnitTests { .containsOnly(entry("x_name", probe.name)); } - @Test + @Test // GH-1192 void countByQuerySimpleValidTest() { - final SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class); + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class); DummyEntity probe = new DummyEntity(); probe.name = "Diego"; @@ -803,9 +805,10 @@ class SqlGeneratorUnitTests { .containsOnly(entry("x_name", probe.name)); } - @Test + @Test // GH-1192 void selectByQueryPaginationValidTest() { - final SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class); + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class); DummyEntity probe = new DummyEntity(); probe.name = "Diego"; @@ -829,6 +832,7 @@ class SqlGeneratorUnitTests { .containsOnly(entry("x_name", probe.name)); } + @Nullable private SqlIdentifier getAlias(Object maybeAliased) { if (maybeAliased instanceof Aliased) { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java index 6ec974111..5430d4b0a 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java +++ b/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.OffsetDateTime; import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -567,7 +568,7 @@ public class JdbcRepositoryIntegrationTests { @Test // GH-987 void queryBySimpleReference() { - final DummyEntity one = repository.save(createDummyEntity()); + DummyEntity one = repository.save(createDummyEntity()); DummyEntity two = createDummyEntity(); two.ref = AggregateReference.to(one.idProp); two = repository.save(two); @@ -580,7 +581,7 @@ public class JdbcRepositoryIntegrationTests { @Test // GH-987 void queryByAggregateReference() { - final DummyEntity one = repository.save(createDummyEntity()); + DummyEntity one = repository.save(createDummyEntity()); DummyEntity two = createDummyEntity(); two.ref = AggregateReference.to(one.idProp); two = repository.save(two); @@ -696,59 +697,28 @@ public class JdbcRepositoryIntegrationTests { assertIsEqualToWithNonNullIds(reloadedRoots.get(1), root2); } - 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); - } - - @Test + @Test // GH-1192 void findOneByExampleShouldGetOne() { DummyEntity dummyEntity1 = createDummyEntity(); dummyEntity1.setFlag(true); - repository.save(dummyEntity1); DummyEntity dummyEntity2 = createDummyEntity(); dummyEntity2.setName("Diego"); - repository.save(dummyEntity2); Example diegoExample = Example.of(new DummyEntity("Diego")); - Optional foundExampleDiego = repository.findOne(diegoExample); - assertThat(foundExampleDiego).isPresent(); - assertThat(foundExampleDiego.get()).isNotNull(); assertThat(foundExampleDiego.get().getName()).isEqualTo("Diego"); } - @Test + @Test // GH-1192 void findOneByExampleMultipleMatchShouldGetOne() { - DummyEntity dummyEntity1 = createDummyEntity(); - repository.save(dummyEntity1); - - DummyEntity dummyEntity2 = createDummyEntity(); - repository.save(dummyEntity2); + repository.save(createDummyEntity()); + repository.save(createDummyEntity()); Example example = Example.of(createDummyEntity()); @@ -756,12 +726,11 @@ public class JdbcRepositoryIntegrationTests { .hasMessageContaining("expected 1, actual 2"); } - @Test + @Test // GH-1192 void findOneByExampleShouldGetNone() { DummyEntity dummyEntity1 = createDummyEntity(); dummyEntity1.setFlag(true); - repository.save(dummyEntity1); Example diegoExample = Example.of(new DummyEntity("NotExisting")); @@ -771,51 +740,42 @@ public class JdbcRepositoryIntegrationTests { assertThat(foundExampleDiego).isNotPresent(); } - @Test + @Test // GH-1192 void findAllByExampleShouldGetOne() { DummyEntity dummyEntity1 = createDummyEntity(); dummyEntity1.setFlag(true); - repository.save(dummyEntity1); DummyEntity dummyEntity2 = createDummyEntity(); dummyEntity2.setName("Diego"); - repository.save(dummyEntity2); Example example = Example.of(new DummyEntity("Diego")); Iterable allFound = repository.findAll(example); - assertThat(allFound) // - .isNotNull() // - .hasSize(1) // - .extracting(DummyEntity::getName) // + assertThat(allFound).extracting(DummyEntity::getName) // .containsExactly(example.getProbe().getName()); } - @Test + @Test // GH-1192 void findAllByExampleMultipleMatchShouldGetOne() { - DummyEntity dummyEntity1 = createDummyEntity(); - repository.save(dummyEntity1); - - DummyEntity dummyEntity2 = createDummyEntity(); - repository.save(dummyEntity2); + repository.save(createDummyEntity()); + repository.save(createDummyEntity()); Example example = Example.of(createDummyEntity()); Iterable allFound = repository.findAll(example); assertThat(allFound) // - .isNotNull() // .hasSize(2) // .extracting(DummyEntity::getName) // .containsOnly(example.getProbe().getName()); } - @Test + @Test // GH-1192 void findAllByExampleShouldGetNone() { DummyEntity dummyEntity1 = createDummyEntity(); @@ -827,12 +787,10 @@ public class JdbcRepositoryIntegrationTests { Iterable allFound = repository.findAll(example); - assertThat(allFound) // - .isNotNull() // - .isEmpty(); + assertThat(allFound).isEmpty(); } - @Test + @Test // GH-1192 void findAllByExamplePageableShouldGetOne() { DummyEntity dummyEntity1 = createDummyEntity(); @@ -850,21 +808,15 @@ public class JdbcRepositoryIntegrationTests { Iterable allFound = repository.findAll(example, pageRequest); - assertThat(allFound) // - .isNotNull() // - .hasSize(1) // - .extracting(DummyEntity::getName) // + assertThat(allFound).extracting(DummyEntity::getName) // .containsExactly(example.getProbe().getName()); } - @Test + @Test // GH-1192 void findAllByExamplePageableMultipleMatchShouldGetOne() { - DummyEntity dummyEntity1 = createDummyEntity(); - repository.save(dummyEntity1); - - DummyEntity dummyEntity2 = createDummyEntity(); - repository.save(dummyEntity2); + repository.save(createDummyEntity()); + repository.save(createDummyEntity()); Example example = Example.of(createDummyEntity()); Pageable pageRequest = PageRequest.of(0, 10); @@ -872,13 +824,12 @@ public class JdbcRepositoryIntegrationTests { Iterable allFound = repository.findAll(example, pageRequest); assertThat(allFound) // - .isNotNull() // .hasSize(2) // .extracting(DummyEntity::getName) // .containsOnly(example.getProbe().getName()); } - @Test + @Test // GH-1192 void findAllByExamplePageableShouldGetNone() { DummyEntity dummyEntity1 = createDummyEntity(); @@ -891,19 +842,14 @@ public class JdbcRepositoryIntegrationTests { Iterable allFound = repository.findAll(example, pageRequest); - assertThat(allFound) // - .isNotNull() // - .isEmpty(); + assertThat(allFound).isEmpty(); } - @Test + @Test // GH-1192 void findAllByExamplePageableOutsidePageShouldGetNone() { - DummyEntity dummyEntity1 = createDummyEntity(); - repository.save(dummyEntity1); - - DummyEntity dummyEntity2 = createDummyEntity(); - repository.save(dummyEntity2); + repository.save(createDummyEntity()); + repository.save(createDummyEntity()); Example example = Example.of(createDummyEntity()); Pageable pageRequest = PageRequest.of(10, 10); @@ -915,7 +861,7 @@ public class JdbcRepositoryIntegrationTests { .isEmpty(); } - @ParameterizedTest + @ParameterizedTest // GH-1192 @MethodSource("findAllByExamplePageableSource") void findAllByExamplePageable(Pageable pageRequest, int size, int totalPages, List notContains) { @@ -964,17 +910,15 @@ public class JdbcRepositoryIntegrationTests { ); } - @Test + @Test // GH-1192 void existsByExampleShouldGetOne() { DummyEntity dummyEntity1 = createDummyEntity(); dummyEntity1.setFlag(true); - repository.save(dummyEntity1); DummyEntity dummyEntity2 = createDummyEntity(); dummyEntity2.setName("Diego"); - repository.save(dummyEntity2); Example example = Example.of(new DummyEntity("Diego")); @@ -984,7 +928,7 @@ public class JdbcRepositoryIntegrationTests { assertThat(exists).isTrue(); } - @Test + @Test // GH-1192 void existsByExampleMultipleMatchShouldGetOne() { DummyEntity dummyEntity1 = createDummyEntity(); @@ -999,7 +943,7 @@ public class JdbcRepositoryIntegrationTests { assertThat(exists).isTrue(); } - @Test + @Test // GH-1192 void existsByExampleShouldGetNone() { DummyEntity dummyEntity1 = createDummyEntity(); @@ -1014,17 +958,17 @@ public class JdbcRepositoryIntegrationTests { assertThat(exists).isFalse(); } - @Test + @Test // GH-1192 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(); two.setName("Diego"); two.setPointInTime(pointInTime); - two = repository.save(two); + repository.save(two); DummyEntity exampleEntitiy = createDummyEntity(); exampleEntitiy.setName("Diego"); @@ -1036,7 +980,7 @@ public class JdbcRepositoryIntegrationTests { assertThat(exists).isTrue(); } - @Test + @Test // GH-1192 void countByExampleShouldGetOne() { DummyEntity dummyEntity1 = createDummyEntity(); @@ -1056,7 +1000,7 @@ public class JdbcRepositoryIntegrationTests { assertThat(count).isOne(); } - @Test + @Test // GH-1192 void countByExampleMultipleMatchShouldGetOne() { DummyEntity dummyEntity1 = createDummyEntity(); @@ -1071,7 +1015,7 @@ public class JdbcRepositoryIntegrationTests { assertThat(count).isEqualTo(2); } - @Test + @Test // GH-1192 void countByExampleShouldGetNone() { DummyEntity dummyEntity1 = createDummyEntity(); @@ -1086,17 +1030,16 @@ public class JdbcRepositoryIntegrationTests { assertThat(count).isNotNull().isZero(); } - @Test + @Test // GH-1192 void countByExampleComplex() { - final Instant pointInTime = Instant.now().minusSeconds(10000); - - final DummyEntity one = repository.save(createDummyEntity()); + Instant pointInTime = Instant.now().minusSeconds(10000).truncatedTo(ChronoUnit.MILLIS); + repository.save(createDummyEntity()); DummyEntity two = createDummyEntity(); two.setName("Diego"); two.setPointInTime(pointInTime); - two = repository.save(two); + repository.save(two); DummyEntity exampleEntitiy = createDummyEntity(); exampleEntitiy.setName("Diego"); @@ -1108,11 +1051,11 @@ public class JdbcRepositoryIntegrationTests { assertThat(count).isOne(); } - @Test + @Test // GH-1192 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()); @@ -1121,11 +1064,17 @@ public class JdbcRepositoryIntegrationTests { two.setName(searchName); two.setPointInTime(now.minusSeconds(10000)); 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(); third.setName(searchName); third.setPointInTime(now.minusSeconds(200000)); 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(); exampleEntitiy.setName(searchName); @@ -1133,28 +1082,27 @@ public class JdbcRepositoryIntegrationTests { Example example = Example.of(exampleEntitiy); List matches = repository.findBy(example, p -> p.sortBy(Sort.by("pointInTime").descending()).all()); - assertThat(matches).hasSize(2).contains(two, third); - assertThat(matches.get(0)).isEqualTo(two); + assertThat(matches).containsExactly(two, third); } - @Test + @Test // GH-1192 void fetchByExampleFluentCountSimple() { - String searchName = "Diego"; + String searchName = "Diego"; Instant now = Instant.now(); - final DummyEntity one = repository.save(createDummyEntity()); + repository.save(createDummyEntity()); DummyEntity two = createDummyEntity(); two.setName(searchName); two.setPointInTime(now.minusSeconds(10000)); - two = repository.save(two); + repository.save(two); DummyEntity third = createDummyEntity(); third.setName(searchName); third.setPointInTime(now.minusSeconds(200000)); - third = repository.save(third); + repository.save(third); DummyEntity exampleEntitiy = createDummyEntity(); exampleEntitiy.setName(searchName); @@ -1165,53 +1113,56 @@ public class JdbcRepositoryIntegrationTests { assertThat(matches).isEqualTo(2); } - @Test + @Test // GH-1192 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(); two.setName(searchName); two.setPointInTime(now.minusSeconds(10000)); 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(); third.setName(searchName); third.setPointInTime(now.minusSeconds(200000)); - third = repository.save(third); + repository.save(third); - DummyEntity exampleEntitiy = createDummyEntity(); - exampleEntitiy.setName(searchName); + DummyEntity exampleEntity = createDummyEntity(); + exampleEntity.setName(searchName); - Example example = Example.of(exampleEntitiy); + Example example = Example.of(exampleEntity); Optional matches = repository.findBy(example, p -> p.sortBy(Sort.by("pointInTime").descending()).first()); + assertThat(matches).contains(two); } - @Test + @Test // GH-1192 void fetchByExampleFluentOnlyInstantOneValueError() { - String searchName = "Diego"; + String searchName = "Diego"; Instant now = Instant.now(); - final DummyEntity one = repository.save(createDummyEntity()); + repository.save(createDummyEntity()); DummyEntity two = createDummyEntity(); - two.setName(searchName); two.setPointInTime(now.minusSeconds(10000)); - two = repository.save(two); + repository.save(two); DummyEntity third = createDummyEntity(); third.setName(searchName); third.setPointInTime(now.minusSeconds(200000)); - third = repository.save(third); + repository.save(third); DummyEntity exampleEntitiy = createDummyEntity(); exampleEntitiy.setName(searchName); @@ -1222,19 +1173,21 @@ public class JdbcRepositoryIntegrationTests { .isInstanceOf(IncorrectResultSizeDataAccessException.class).hasMessageContaining("expected 1, actual 2"); } - @Test + @Test // GH-1192 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(); - two.setName(searchName); two.setPointInTime(now.minusSeconds(10000)); 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(); exampleEntitiy.setName(searchName); @@ -1246,30 +1199,52 @@ public class JdbcRepositoryIntegrationTests { assertThat(match).contains(two); } - @Test + @Test // GH-1192 void fetchByExampleFluentOnlyInstantOneValueAsSimple() { - String searchName = "Diego"; + String searchName = "Diego"; Instant now = Instant.now(); - final DummyEntity one = repository.save(createDummyEntity()); + repository.save(createDummyEntity()); DummyEntity two = createDummyEntity(); - two.setName(searchName); two.setPointInTime(now.minusSeconds(10000)); two = repository.save(two); - DummyEntity exampleEntitiy = createDummyEntity(); - exampleEntitiy.setName(searchName); + DummyEntity exampleEntity = createDummyEntity(); + exampleEntity.setName(searchName); - Example example = Example.of(exampleEntitiy); + Example example = Example.of(exampleEntity); Optional match = repository.findBy(example, p -> p.as(DummyProjectExample.class).one()); 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() { Instant now = Instant.now(); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql index 5f2069c61..458bbd0b5 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql +++ b/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, NAME VARCHAR(100), - POINT_IN_TIME DATETIME, + POINT_IN_TIME DATETIME2, OFFSET_DATE_TIME DATETIMEOFFSET, FLAG BIT, REF BIGINT, diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java index 1b47eb7c7..7b8e9a8f8 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java +++ b/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.Set; +import org.springframework.data.relational.core.sql.Functions; 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.render.SelectRenderContext; @@ -131,4 +134,14 @@ public interface Dialect { default OrderByNullPrecedence orderByNullHandling() { 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)); + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java index 7703e3e8a..33a73e263 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java +++ b/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.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.LetterCasing; import org.springframework.data.relational.core.sql.IdentifierProcessing.Quoting; 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.TableLike; import org.springframework.util.Assert; @@ -193,4 +196,9 @@ public class PostgresDialect extends AbstractDialect { action.accept(ClassUtils.resolveClassName(className, PostgresDialect.class.getClassLoader())); } } + + @Override + public SimpleFunction getExistsFunction() { + return Functions.least(Functions.count(SQL.literalOf(1)), SQL.literalOf(1)); + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java index b5e366ba3..da2266f0d 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java +++ b/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)); } + public static SimpleFunction least(Expression... expressions) { + + return SimpleFunction.create("LEAST", Arrays.asList(expressions)); + } + /** * Creates a new {@code COUNT} function. *