diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 84fe0f8868..4af182c4df 100755 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,2 +1,2 @@ -#Mon Oct 11 14:30:22 CEST 2021 -distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.3/apache-maven-3.8.3-bin.zip +#Tue Feb 22 13:59:08 CET 2022 +distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip 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 73cc729525..60f8f68a6a 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 @@ -15,9 +15,13 @@ */ package org.springframework.data.jdbc.core; +import java.util.Optional; + +import org.springframework.data.domain.Example; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.relational.core.query.Query; import org.springframework.lang.Nullable; /** @@ -26,6 +30,7 @@ * @author Jens Schauder * @author Thomas Lang * @author Milan Milanov + * @author Diego Krupitza */ public interface JdbcAggregateOperations { @@ -154,4 +159,54 @@ public interface JdbcAggregateOperations { * @since 2.0 */ Page findAll(Class domainType, Pageable pageable); + + /** + * Execute a {@code SELECT} query and convert the resulting item to an entity ensuring exactly one result. + * + * @param query 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. + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. + */ + Optional selectOne(Query query, Class entityClass); + + /** + * Execute a {@code SELECT} query and convert the resulting items to a {@link Iterable} that is sorted. + * + * @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. + */ + Iterable select(Query query, Class entityClass, Sort sort); + + /** + * Determine whether there are aggregates that match the {@link Query} + * + * @param query must not be {@literal null}. + * @param entityClass the entity type must not be {@literal null}. + * @return {@literal true} if the object exists. + */ + boolean exists(Query query, Class entityClass); + + /** + * Counts the number of aggregates of a given type that match the given query. + * + * @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}. + */ + long count(Query query, Class entityClass); + + /** + * Returns a {@link Page} of entities matching the given {@link Query}. In case no match could be found, an empty + * {@link Page} is returned. + * + * @param query must not be {@literal null}. + * @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}. + */ + 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 24c2de8689..f42eb69427 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 @@ -17,6 +17,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -41,6 +42,7 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.mapping.event.*; +import org.springframework.data.relational.core.query.Query; import org.springframework.data.support.PageableExecutionUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -54,6 +56,7 @@ * @author Christoph Strobl * @author Milan Milanov * @author Myeonghyeon Lee + * @author Diego Krupitza * @author Chirag Tailor */ public class JdbcAggregateTemplate implements JdbcAggregateOperations { @@ -65,6 +68,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { private final DataAccessStrategy accessStrategy; private final AggregateChangeExecutor executor; + private final JdbcConverter converter; private EntityCallbacks entityCallbacks = EntityCallbacks.create(); @@ -232,6 +236,38 @@ public Page findAll(Class domainType, Pageable pageable) { return PageableExecutionUtils.getPage(content, pageable, () -> accessStrategy.count(domainType)); } + @Override + public Optional selectOne(Query query, Class entityClass) { + return accessStrategy.selectOne(query, entityClass); + } + + @Override + public Iterable select(Query query, Class entityClass, Sort sort) { + return accessStrategy.select(query, entityClass); + } + + @Override + public boolean exists(Query query, Class entityClass) { + return accessStrategy.exists(query, entityClass); + } + + @Override + public long count(Query query, Class entityClass) { + return accessStrategy.count(query, entityClass); + } + + @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 7987fabf67..365af7eca4 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 @@ -17,6 +17,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.function.Consumer; import java.util.function.Function; @@ -25,6 +26,7 @@ import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.conversion.IdValueSource; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.core.query.Query; import org.springframework.data.relational.core.sql.LockMode; /** @@ -36,6 +38,7 @@ * @author Tyler Van Gorder * @author Milan Milanov * @author Myeonghyeon Lee + * @author Diego Krupitza * @author Chirag Tailor * @since 1.1 */ @@ -148,6 +151,31 @@ public Iterable findAll(Class domainType, Pageable pageable) { return collect(das -> das.findAll(domainType, pageable)); } + @Override + public Optional selectOne(Query query, Class probeType) { + return collect(das -> das.selectOne(query, probeType)); + } + + @Override + public Iterable select(Query query, Class probeType) { + return collect(das -> das.select(query, probeType)); + } + + @Override + public Iterable select(Query query, Class probeType, Pageable pageable) { + return collect(das -> das.select(query, probeType, pageable)); + } + + @Override + public boolean exists(Query query, Class probeType) { + return collect(das -> das.exists(query, probeType)); + } + + @Override + public long count(Query query, Class probeType) { + return collect(das -> das.count(query, probeType)); + } + private T collect(Function function) { // Keep as Eclipse fails to compile if <> is used. 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 297925887f..67109f8b49 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 @@ -17,6 +17,7 @@ import java.util.List; import java.util.Map; +import java.util.Optional; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.domain.Pageable; @@ -25,6 +26,7 @@ import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.conversion.IdValueSource; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.core.query.Query; import org.springframework.data.relational.core.sql.LockMode; import org.springframework.lang.Nullable; @@ -37,6 +39,7 @@ * @author Tyler Van Gorder * @author Milan Milanov * @author Myeonghyeon Lee + * @author Diego Krupitza * @author Chirag Tailor */ public interface DataAccessStrategy extends RelationResolver { @@ -258,4 +261,54 @@ Iterable findAllByPath(Identifier identifier, * @since 2.0 */ Iterable findAll(Class domainType, Pageable pageable); + + /** + * Execute a {@code SELECT} query and convert the resulting item to an entity ensuring exactly one result. + * + * @param query must not be {@literal null}. + * @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. + */ + Optional selectOne(Query query, Class probeType); + + /** + * Execute a {@code SELECT} query and convert the resulting items to a {@link Iterable}. + * + * @param query must not be {@literal null}. + * @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. + */ + Iterable select(Query query, Class probeType); + + /** + * Execute a {@code SELECT} query and convert the resulting items to a {@link Iterable}. Applies the {@link Pageable} + * to the result. + * + * @param query must not be {@literal null}. + * @param probeType the type of entities. 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. + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. + */ + Iterable select(Query query, Class probeType, Pageable pageable); + + /** + * Determine whether there is an aggregate of type probeType that matches the provided {@link Query}. + * + * @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. + */ + boolean exists(Query query, Class probeType); + + /** + * Counts the rows in the table representing the given probe type, that match the given query. + * + * @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}. + */ + 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 2521aef521..3cde57a6a1 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 @@ -32,10 +32,12 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.core.query.Query; import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.LockMode; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.lang.Nullable; @@ -55,6 +57,7 @@ * @author Myeonghyeon Lee * @author Yunyoung LEE * @author Radim Tlusty + * @author Diego Krupitza * @author Chirag Tailor * @since 1.1 */ @@ -314,6 +317,61 @@ public Iterable findAll(Class domainType, Pageable pageable) { return operations.query(sql(domainType).getFindAll(pageable), (RowMapper) 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)); + } catch (EmptyResultDataAccessException e) { + foundObject = null; + } + + 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)); + } + + @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)); + } + + @Override + public boolean exists(Query query, Class probeType) { + 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"); + + 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."); + + return result; + } + private EntityRowMapper getEntityRowMapper(Class domainType) { return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), converter); } @@ -349,4 +407,4 @@ private SqlIdentifier getIdColumn(Class domainType) { return Optional.ofNullable(context.getRequiredPersistentEntity(domainType).getIdProperty()) .map(RelationalPersistentProperty::getColumnName).orElse(null); } -} +} \ No newline at end of file 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 3a8f9c308c..91388a5c29 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 @@ -22,9 +22,12 @@ import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.conversion.IdValueSource; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +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. @@ -33,6 +36,7 @@ * @author Tyler Van Gorder * @author Milan Milanov * @author Myeonghyeon Lee + * @author Diego Krupitza * @author Chirag Tailor * @since 1.1 */ @@ -145,6 +149,31 @@ public Iterable findAll(Class domainType, Pageable pageable) { return delegate.findAll(domainType, pageable); } + @Override + public Optional selectOne(Query query, Class probeType) { + return delegate.selectOne(query, probeType); + } + + @Override + public Iterable select(Query query, Class probeType) { + return delegate.select(query, probeType); + } + + @Override + public Iterable select(Query query, Class probeType, Pageable pageable) { + return delegate.select(query, probeType, pageable); + } + + @Override + public boolean exists(Query query, Class probeType) { + return delegate.exists(query, probeType); + } + + @Override + public long count(Query query, Class probeType) { + return delegate.count(query, probeType); + } + /** * Must be called exactly once before calling any of the other methods. * 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 c9e7c25ac3..e91739b23a 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 @@ -22,6 +22,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.jdbc.repository.query.QueryMapper; import org.springframework.data.jdbc.repository.support.SimpleJdbcRepository; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.context.MappingContext; @@ -31,10 +32,13 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.core.query.CriteriaDefinition; +import org.springframework.data.relational.core.query.Query; import org.springframework.data.relational.core.sql.*; import org.springframework.data.relational.core.sql.render.RenderContext; import org.springframework.data.relational.core.sql.render.SqlRenderer; import org.springframework.data.util.Lazy; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -52,6 +56,7 @@ * @author Myeonghyeon Lee * @author Mikhail Polivakha * @author Chirag Tailor + * @author Diego Krupitza */ class SqlGenerator { @@ -82,6 +87,7 @@ class SqlGenerator { private final Lazy deleteByIdSql = Lazy.of(this::createDeleteSql); private final Lazy deleteByIdAndVersionSql = Lazy.of(this::createDeleteByIdAndVersionSql); private final Lazy deleteByListSql = Lazy.of(this::createDeleteByListSql); + private final QueryMapper queryMapper; /** * Create a new {@link SqlGenerator} given {@link RelationalMappingContext} and {@link RelationalPersistentEntity}. @@ -100,6 +106,7 @@ class SqlGenerator { this.renderContext = new RenderContextFactory(dialect).createRenderContext(); this.sqlRenderer = SqlRenderer.create(renderContext); this.columns = new Columns(entity, mappingContext, converter); + this.queryMapper = new QueryMapper(dialect, converter); } /** @@ -722,6 +729,159 @@ private OrderByField orderToOrderByField(Sort.Order order) { return OrderByField.from(column, order.getDirection()).withNullHandling(order.getNullHandling()); } + /** + * Constructs a single sql query that performs select based on the provided query. Additional the bindings for the + * where clause are stored after execution into the parameterSource + * + * @param query the query to base the select on. Must not be null + * @param parameterSource the source for holding the bindings + * @return a non null query string. + */ + public String selectByQuery(Query query, MapSqlParameterSource parameterSource) { + + Assert.notNull(parameterSource, "parameterSource must not be null"); + + SelectBuilder.SelectWhere selectBuilder = selectBuilder(); + + Select select = applyQueryOnSelect(query, parameterSource, selectBuilder) // + .build(); + + return render(select); + } + + /** + * Constructs a single sql query that performs select based on the provided query and pagination information. + * Additional the bindings for the where clause are stored after execution into the parameterSource + * + * @param query the query to base the select on. Must not be null. + * @param pageable the pageable to perform on the select. + * @param parameterSource the source for holding the bindings. + * @return a non null query string. + */ + public String selectByQuery(Query query, MapSqlParameterSource parameterSource, Pageable pageable) { + + Assert.notNull(parameterSource, "parameterSource must not be null"); + + SelectBuilder.SelectWhere selectBuilder = selectBuilder(); + + // first apply query and then pagination. This means possible query sorting and limiting might be overwritten by the + // pagination. This is desired. + SelectBuilder.SelectOrdered selectOrdered = applyQueryOnSelect(query, parameterSource, selectBuilder); + selectOrdered = applyPagination(pageable, selectOrdered); + selectOrdered = selectOrdered.orderBy(extractOrderByFields(pageable.getSort())); + + Select select = selectOrdered.build(); + return render(select); + } + + /** + * Constructs a single sql query that performs select count based on the provided query for checking existence. + * Additional the bindings for the where clause are stored after execution into the parameterSource + * + * @param query the query to base the select on. Must not be null + * @param parameterSource the source for holding the bindings + * @return a non null query string. + */ + public String existsByQuery(Query query, MapSqlParameterSource parameterSource) { + + Expression idColumn = getIdColumn(); + SelectBuilder.SelectJoin baseSelect = getSelectCountWithExpression(idColumn); + + Select select = applyQueryOnSelect(query, parameterSource, (SelectBuilder.SelectWhere) baseSelect) // + .build(); + + return render(select); + } + + /** + * Constructs a single sql query that performs select count based on the provided query. Additional the bindings for + * the where clause are stored after execution into the parameterSource + * + * @param query the query to base the select on. Must not be null + * @param parameterSource the source for holding the bindings + * @return a non null query string. + */ + public String countByQuery(Query query, MapSqlParameterSource parameterSource) { + + Expression countExpression = Expressions.just("1"); + SelectBuilder.SelectJoin baseSelect = getSelectCountWithExpression(countExpression); + + Select select = applyQueryOnSelect(query, parameterSource, (SelectBuilder.SelectWhere) baseSelect) // + .build(); + + 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. + * + * @param countExpressions the expression to use as count parameter. + * @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 getSelectCountWithExpression(Expression... countExpressions) { + + Assert.notNull(countExpressions, "countExpressions must not be null"); + 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; + + // 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; + } + + private SelectBuilder.SelectOrdered applyQueryOnSelect(Query query, MapSqlParameterSource parameterSource, + SelectBuilder.SelectWhere selectBuilder) { + + Table table = Table.create(this.entity.getTableName()); + + SelectBuilder.SelectOrdered selectOrdered = query // + .getCriteria() // + .map(item -> this.applyCriteria(item, selectBuilder, parameterSource, table)) // + .orElse(selectBuilder); + + if (query.isSorted()) { + List sort = extractOrderByFields(query.getSort()); + selectOrdered = selectBuilder.orderBy(sort); + } + + SelectBuilder.SelectLimitOffset limitable = (SelectBuilder.SelectLimitOffset) selectOrdered; + + if (query.getLimit() > 0) { + limitable = limitable.limit(query.getLimit()); + } + + if (query.getOffset() > 0) { + limitable = limitable.offset(query.getOffset()); + } + return (SelectBuilder.SelectOrdered) limitable; + } + + SelectBuilder.SelectOrdered applyCriteria(@Nullable CriteriaDefinition criteria, + SelectBuilder.SelectWhere whereBuilder, MapSqlParameterSource parameterSource, Table table) { + + return criteria != null // + ? whereBuilder.where(queryMapper.getMappedObject(parameterSource, criteria, table, entity)) // + : whereBuilder; + } + /** * Value object representing a {@code JOIN} association. */ 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 bd66945c6a..224ffb5c6a 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 @@ -21,6 +21,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; @@ -37,6 +38,7 @@ import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.core.query.Query; import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.LockMode; import org.springframework.data.relational.core.sql.SqlIdentifier; @@ -320,6 +322,35 @@ public Iterable findAll(Class domainType, Pageable pageable) { new MyBatisContext(null, null, domainType, additionalContext)); } + @Override + public Optional selectOne(Query query, Class probeType) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public Iterable select(Query query, Class probeType) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public Iterable select(Query query, Class probeType, Pageable pageable) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public boolean exists(Query query, Class probeType) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public long count(Query query, Class probeType) { + 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/query/QueryMapper.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java index 97626cb5f8..7d2e8b0dd9 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java @@ -58,7 +58,7 @@ * @author Jens Schauder * @since 2.0 */ -class QueryMapper { +public class QueryMapper { private final JdbcConverter converter; private final Dialect dialect; @@ -71,7 +71,7 @@ class QueryMapper { * @param converter must not be {@literal null}. */ @SuppressWarnings({ "unchecked", "rawtypes" }) - QueryMapper(Dialect dialect, JdbcConverter converter) { + public QueryMapper(Dialect dialect, JdbcConverter converter) { Assert.notNull(dialect, "Dialect must not be null!"); Assert.notNull(converter, "JdbcConverter must not be null!"); @@ -88,7 +88,7 @@ class QueryMapper { * @param entity related {@link RelationalPersistentEntity}, can be {@literal null}. * @return */ - List getMappedSort(Table table, Sort sort, @Nullable RelationalPersistentEntity entity) { + public List getMappedSort(Table table, Sort sort, @Nullable RelationalPersistentEntity entity) { List mappedOrder = new ArrayList<>(); @@ -157,7 +157,7 @@ Expression getMappedObject(Expression expression, @Nullable RelationalPersistent * @param entity related {@link RelationalPersistentEntity}, can be {@literal null}. * @return the mapped {@link Condition}. */ - Condition getMappedObject(MapSqlParameterSource parameterSource, CriteriaDefinition criteria, Table table, + public Condition getMappedObject(MapSqlParameterSource parameterSource, CriteriaDefinition criteria, Table table, @Nullable RelationalPersistentEntity entity) { Assert.notNull(parameterSource, "MapSqlParameterSource must not be null!"); 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 new file mode 100644 index 0000000000..9c6aebddcb --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FetchableFluentQueryByExample.java @@ -0,0 +1,124 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.support; + +import java.util.Collections; +import java.util.List; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jdbc.core.JdbcAggregateOperations; +import org.springframework.data.relational.core.query.Query; +import org.springframework.data.relational.repository.query.RelationalExampleMapper; + +/** + * {@link org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery} using {@link Example}. + * + * @author Diego Krupitza + */ +class FetchableFluentQueryByExample extends FluentQuerySupport { + + private final RelationalExampleMapper exampleMapper; + private final JdbcAggregateOperations entityOperations; + + FetchableFluentQueryByExample(Example example, Class resultType, RelationalExampleMapper exampleMapper, + JdbcAggregateOperations entityOperations) { + this(example, Sort.unsorted(), resultType, Collections.emptyList(), exampleMapper, entityOperations); + } + + 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()); + } + + @Override + public List all() { + return StreamSupport + .stream(this.entityOperations.select(createQuery(), getExampleType(), getSort()).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) + .map(item -> this.getConversionFunction().apply(item)); + } + + @Override + public long count() { + return this.entityOperations.count(createQuery(), getExampleType()); + } + + @Override + public boolean exists() { + return this.entityOperations.exists(createQuery(), getExampleType()); + } + + private Query createQuery() { + return createQuery(UnaryOperator.identity()); + } + + private Query createQuery(UnaryOperator queryCustomizer) { + + Query query = exampleMapper.getMappedExample(getExample()); + + if (getSort().isSorted()) { + query = query.sort(getSort()); + } + + if (!getFieldsToInclude().isEmpty()) { + query = query.columns(getFieldsToInclude().toArray(new String[0])); + } + + query = queryCustomizer.apply(query); + + return query; + } + + @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 new file mode 100644 index 0000000000..50506b505a --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FluentQuerySupport.java @@ -0,0 +1,127 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.support; + +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Sort; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.query.FluentQuery; +import org.springframework.util.Assert; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; + +/** + * Support class for {@link FluentQuery.FetchableFluentQuery} implementations. + * + * @author Diego Krupitza + */ +abstract class FluentQuerySupport implements FluentQuery.FetchableFluentQuery { + + private final Example example; + private final Sort sort; + private final Class resultType; + private final List fieldsToInclude; + + private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); + + FluentQuerySupport(Example example, Sort sort, Class resultType, List fieldsToInclude) { + this.example = example; + this.sort = sort; + this.resultType = resultType; + this.fieldsToInclude = fieldsToInclude; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery#sortBy(org.springframework.data.domain.Sort) + */ + @Override + public FetchableFluentQuery sortBy(Sort sort) { + + Assert.notNull(sort, "Sort must not be null!"); + + return create(example, sort, resultType, fieldsToInclude); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery#as(java.lang.Class) + */ + @Override + public FetchableFluentQuery as(Class projection) { + + Assert.notNull(projection, "Projection target type must not be null!"); + + return create(example, sort, projection, fieldsToInclude); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery#project(java.util.Collection) + */ + @Override + public FetchableFluentQuery project(Collection properties) { + + Assert.notNull(properties, "Projection properties must not be null!"); + + return create(example, sort, resultType, new ArrayList<>(properties)); + } + + protected abstract FluentQuerySupport create(Example example, Sort sort, Class resultType, + List fieldsToInclude); + + Class getExampleType() { + return this.example.getProbeType(); + } + + Example getExample() { + return this.example; + } + + Sort getSort() { + return sort; + } + + Class getResultType() { + return resultType; + } + + List getFieldsToInclude() { + return fieldsToInclude; + } + + private Function getConversionFunction(Class inputType, Class targetType) { + + if (targetType.isAssignableFrom(inputType)) { + return (Function) Function.identity(); + } + + if (targetType.isInterface()) { + return o -> projectionFactory.createProjection(targetType, o); + } + + return o -> DefaultConversionService.getSharedInstance().convert(o, targetType); + } + + protected Function getConversionFunction() { + return getConversionFunction(this.example.getProbeType(), getResultType()); + } + +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java index b6bebae247..f68ecc2dc8 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java @@ -122,7 +122,8 @@ protected Object getTargetRepository(RepositoryInformation repositoryInformation RelationalPersistentEntity persistentEntity = context .getRequiredPersistentEntity(repositoryInformation.getDomainType()); - return getTargetRepositoryViaReflection(repositoryInformation.getRepositoryBaseClass(), template, persistentEntity); + return getTargetRepositoryViaReflection(repositoryInformation, template, persistentEntity, + converter); } @Override 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 f67679d249..6d9ed6bebe 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 @@ -16,15 +16,21 @@ 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; import org.springframework.data.domain.Pageable; 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.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.util.Streamable; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; @@ -35,20 +41,25 @@ * @author Jens Schauder * @author Oliver Gierke * @author Milan Milanov + * @author Diego Krupitza */ @Transactional(readOnly = true) -public class SimpleJdbcRepository implements CrudRepository, PagingAndSortingRepository { +public class SimpleJdbcRepository + implements CrudRepository, PagingAndSortingRepository, QueryByExampleExecutor { private final JdbcAggregateOperations entityOperations; private final PersistentEntity entity; + private final RelationalExampleMapper exampleMapper; - public SimpleJdbcRepository(JdbcAggregateOperations entityOperations,PersistentEntity entity) { + public SimpleJdbcRepository(JdbcAggregateOperations entityOperations, PersistentEntity entity, + JdbcConverter converter) { Assert.notNull(entityOperations, "EntityOperations must not be null."); Assert.notNull(entity, "Entity must not be null."); this.entityOperations = entityOperations; this.entity = entity; + this.exampleMapper = new RelationalExampleMapper(converter.getMappingContext()); } @Transactional @@ -131,4 +142,56 @@ public Page findAll(Pageable pageable) { return entityOperations.findAll(entity.getType(), pageable); } + @Override + public Optional findOne(Example example) { + 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!"); + + 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); + } + + @Override + public Page findAll(Example example, Pageable pageable) { + 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!"); + + return this.entityOperations.count(this.exampleMapper.getMappedExample(example), example.getProbeType()); + } + + @Override + public boolean exists(Example example) { + 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!"); + + FluentQuery.FetchableFluentQuery fluentQuery = new FetchableFluentQueryByExample<>(example, + example.getProbeType(), this.exampleMapper, this.entityOperations); + + return queryFunction.apply(fluentQuery); + } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/DependencyTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/DependencyTests.java index e5e0c56252..c315051f80 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/DependencyTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/DependencyTests.java @@ -45,8 +45,8 @@ void cycleFree() { .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS) // we just analyze the code of this module. .importPackages("org.springframework.data.jdbc") .that( // - onlySpringData() // - ); + onlySpringData() // + ); ArchRule rule = SlicesRuleDefinition.slices() // .matching("org.springframework.data.jdbc.(**)") // @@ -168,4 +168,4 @@ public String getDescription() { }; } -} +} \ No newline at end of file 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 7ea24dfebe..07fd25a620 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 @@ -46,10 +46,13 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.core.query.Criteria; +import org.springframework.data.relational.core.query.Query; import org.springframework.data.relational.core.sql.Aliased; import org.springframework.data.relational.core.sql.LockMode; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.data.relational.core.sql.Table; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; /** * Unit tests for the {@link SqlGenerator}. @@ -64,6 +67,7 @@ * @author Myeonghyeon Lee * @author Mikhail Polivakha * @author Chirag Tailor + * @author Diego Krupitza */ class SqlGeneratorUnitTests { @@ -716,6 +720,92 @@ void columnForReferencedEntityWithoutId() { SqlIdentifier.quoted("child"), SqlIdentifier.quoted("CHILD_PARENT_OF_NO_ID_CHILD")); } + @Test + void selectByQueryValidTest() { + final SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class); + + DummyEntity probe = new DummyEntity(); + probe.name = "Diego"; + + Criteria criteria = Criteria.where("name").is(probe.name); + Query query = Query.query(criteria); + + MapSqlParameterSource parameterSource = new MapSqlParameterSource(); + + String generatedSQL = sqlGenerator.selectByQuery(query, parameterSource); + assertThat(generatedSQL).isNotNull().contains(":x_name"); + + assertThat(parameterSource.getValues()) // + .containsOnly(entry("x_name", probe.name)); + } + + @Test + void existsByQuerySimpleValidTest() { + final SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class); + + DummyEntity probe = new DummyEntity(); + probe.name = "Diego"; + + Criteria criteria = Criteria.where("name").is(probe.name); + Query query = Query.query(criteria); + + MapSqlParameterSource parameterSource = new MapSqlParameterSource(); + + String generatedSQL = sqlGenerator.existsByQuery(query, parameterSource); + assertThat(generatedSQL).isNotNull().contains(":x_name"); + + assertThat(parameterSource.getValues()) // + .containsOnly(entry("x_name", probe.name)); + } + + @Test + void countByQuerySimpleValidTest() { + final SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class); + + DummyEntity probe = new DummyEntity(); + probe.name = "Diego"; + + Criteria criteria = Criteria.where("name").is(probe.name); + Query query = Query.query(criteria); + + MapSqlParameterSource parameterSource = new MapSqlParameterSource(); + + String generatedSQL = sqlGenerator.countByQuery(query, parameterSource); + assertThat(generatedSQL) // + .isNotNull() // + .containsIgnoringCase("COUNT(1)") // + .contains(":x_name"); + + assertThat(parameterSource.getValues()) // + .containsOnly(entry("x_name", probe.name)); + } + + @Test + void selectByQueryPaginationValidTest() { + final SqlGenerator sqlGenerator = createSqlGenerator(DummyEntity.class); + + DummyEntity probe = new DummyEntity(); + probe.name = "Diego"; + + Criteria criteria = Criteria.where("name").is(probe.name); + Query query = Query.query(criteria); + + PageRequest pageRequest = PageRequest.of(2, 1, Sort.by(Sort.Order.asc("name"))); + + MapSqlParameterSource parameterSource = new MapSqlParameterSource(); + + String generatedSQL = sqlGenerator.selectByQuery(query, parameterSource, pageRequest); + assertThat(generatedSQL) // + .isNotNull() // + .contains(":x_name") // + .containsIgnoringCase("ORDER BY dummy_entity.x_name ASC") // + .containsIgnoringCase("LIMIT 1") // + .containsIgnoringCase("OFFSET 2 LIMIT 1"); + + assertThat(parameterSource.getValues()) // + .containsOnly(entry("x_name", probe.name)); + } + private SqlIdentifier getAlias(Object maybeAliased) { if (maybeAliased instanceof Aliased) { @@ -737,7 +827,8 @@ private PersistentPropertyPath getPath(String path @SuppressWarnings("unused") static class DummyEntity { - @Column("id1") @Id Long id; + @Column("id1") + @Id Long id; String name; ReferencedEntity ref; Set elements; @@ -810,7 +901,8 @@ static class EntityWithQuotedColumnName { // these column names behave like single double quote in the name since the get quoted and then doubling the double // quote escapes it. - @Id @Column("test\"\"_@id") Long id; + @Id + @Column("test\"\"_@id") Long id; @Column("test\"\"_@123") String name; } 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 85f9e1e1f7..210789aa04 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 @@ -32,11 +32,17 @@ import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.PropertiesFactoryBean; import org.springframework.context.ApplicationListener; @@ -44,13 +50,15 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.io.ClassPathResource; +import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.annotation.Id; +import org.springframework.data.domain.Example; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; import org.springframework.data.jdbc.core.mapping.AggregateReference; -import org.springframework.data.relational.repository.Lock; import org.springframework.data.jdbc.repository.query.Modifying; import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; @@ -62,10 +70,13 @@ import org.springframework.data.relational.core.mapping.event.AfterConvertEvent; import org.springframework.data.relational.core.mapping.event.AfterLoadEvent; import org.springframework.data.relational.core.sql.LockMode; +import org.springframework.data.relational.repository.Lock; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; +import org.springframework.data.repository.query.FluentQuery; import org.springframework.data.repository.query.Param; +import org.springframework.data.repository.query.QueryByExampleExecutor; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; @@ -80,6 +91,7 @@ * * @author Jens Schauder * @author Mark Paluch + * @author Diego Krupitza */ @Transactional @TestExecutionListeners(value = AssumeFeatureTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS) @@ -575,6 +587,557 @@ void nullStringResult() { assertThat(repository.returnInput(null)).isNull(); } + @Test + 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 + void findOneByExampleMultipleMatchShouldGetOne() { + + DummyEntity dummyEntity1 = createDummyEntity(); + repository.save(dummyEntity1); + + DummyEntity dummyEntity2 = createDummyEntity(); + repository.save(dummyEntity2); + + Example example = Example.of(createDummyEntity()); + + assertThatThrownBy(() -> repository.findOne(example)).isInstanceOf(IncorrectResultSizeDataAccessException.class) + .hasMessageContaining("expected 1, actual 2"); + } + + @Test + void findOneByExampleShouldGetNone() { + + DummyEntity dummyEntity1 = createDummyEntity(); + dummyEntity1.setFlag(true); + + repository.save(dummyEntity1); + + Example diegoExample = Example.of(new DummyEntity("NotExisting")); + + Optional foundExampleDiego = repository.findOne(diegoExample); + + assertThat(foundExampleDiego).isNotPresent(); + } + + @Test + 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) // + .containsExactly(example.getProbe().getName()); + } + + @Test + void findAllByExampleMultipleMatchShouldGetOne() { + + DummyEntity dummyEntity1 = createDummyEntity(); + repository.save(dummyEntity1); + + DummyEntity dummyEntity2 = createDummyEntity(); + repository.save(dummyEntity2); + + Example example = Example.of(createDummyEntity()); + + Iterable allFound = repository.findAll(example); + + assertThat(allFound) // + .isNotNull() // + .hasSize(2) // + .extracting(DummyEntity::getName) // + .containsOnly(example.getProbe().getName()); + } + + @Test + void findAllByExampleShouldGetNone() { + + DummyEntity dummyEntity1 = createDummyEntity(); + dummyEntity1.setFlag(true); + + repository.save(dummyEntity1); + + Example example = Example.of(new DummyEntity("NotExisting")); + + Iterable allFound = repository.findAll(example); + + assertThat(allFound) // + .isNotNull() // + .isEmpty(); + } + + @Test + void findAllByExamplePageableShouldGetOne() { + + 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")); + Pageable pageRequest = PageRequest.of(0, 10); + + Iterable allFound = repository.findAll(example, pageRequest); + + assertThat(allFound) // + .isNotNull() // + .hasSize(1) // + .extracting(DummyEntity::getName) // + .containsExactly(example.getProbe().getName()); + } + + @Test + void findAllByExamplePageableMultipleMatchShouldGetOne() { + + DummyEntity dummyEntity1 = createDummyEntity(); + repository.save(dummyEntity1); + + DummyEntity dummyEntity2 = createDummyEntity(); + repository.save(dummyEntity2); + + Example example = Example.of(createDummyEntity()); + Pageable pageRequest = PageRequest.of(0, 10); + + Iterable allFound = repository.findAll(example, pageRequest); + + assertThat(allFound) // + .isNotNull() // + .hasSize(2) // + .extracting(DummyEntity::getName) // + .containsOnly(example.getProbe().getName()); + } + + @Test + void findAllByExamplePageableShouldGetNone() { + + DummyEntity dummyEntity1 = createDummyEntity(); + dummyEntity1.setFlag(true); + + repository.save(dummyEntity1); + + Example example = Example.of(new DummyEntity("NotExisting")); + Pageable pageRequest = PageRequest.of(0, 10); + + Iterable allFound = repository.findAll(example, pageRequest); + + assertThat(allFound) // + .isNotNull() // + .isEmpty(); + } + + @Test + void findAllByExamplePageableOutsidePageShouldGetNone() { + + DummyEntity dummyEntity1 = createDummyEntity(); + repository.save(dummyEntity1); + + DummyEntity dummyEntity2 = createDummyEntity(); + repository.save(dummyEntity2); + + Example example = Example.of(createDummyEntity()); + Pageable pageRequest = PageRequest.of(10, 10); + + Iterable allFound = repository.findAll(example, pageRequest); + + assertThat(allFound) // + .isNotNull() // + .isEmpty(); + } + + @ParameterizedTest + @MethodSource("findAllByExamplePageableSource") + void findAllByExamplePageable(Pageable pageRequest, int size, int totalPages, List notContains) { + + for (int i = 0; i < 100; i++) { + DummyEntity dummyEntity = createDummyEntity(); + dummyEntity.setFlag(true); + dummyEntity.setName("" + i); + + repository.save(dummyEntity); + } + + DummyEntity dummyEntityExample = createDummyEntity(); + dummyEntityExample.setName(null); + dummyEntityExample.setFlag(true); + + Example example = Example.of(dummyEntityExample); + + Page allFound = repository.findAll(example, pageRequest); + + // page has correct size + assertThat(allFound) // + .isNotNull() // + .hasSize(size); + + // correct number of total + assertThat(allFound.getTotalElements()).isEqualTo(100); + + assertThat(allFound.getTotalPages()).isEqualTo(totalPages); + + if (!notContains.isEmpty()) { + assertThat(allFound) // + .extracting(DummyEntity::getName) // + .doesNotContain(notContains.toArray(new String[0])); + } + } + + public static Stream findAllByExamplePageableSource() { + return Stream.of( // + Arguments.of(PageRequest.of(0, 3), 3, 34, Arrays.asList("3", "4", "100")), // + Arguments.of(PageRequest.of(1, 10), 10, 10, Arrays.asList("9", "20", "30")), // + Arguments.of(PageRequest.of(2, 10), 10, 10, Arrays.asList("1", "2", "3")), // + Arguments.of(PageRequest.of(33, 3), 1, 34, Collections.emptyList()), // + Arguments.of(PageRequest.of(36, 3), 0, 34, Collections.emptyList()), // + Arguments.of(PageRequest.of(0, 10000), 100, 1, Collections.emptyList()), // + Arguments.of(PageRequest.of(100, 10000), 0, 1, Collections.emptyList()) // + ); + } + + @Test + 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")); + + boolean exists = repository.exists(example); + + assertThat(exists).isTrue(); + } + + @Test + void existsByExampleMultipleMatchShouldGetOne() { + + DummyEntity dummyEntity1 = createDummyEntity(); + repository.save(dummyEntity1); + + DummyEntity dummyEntity2 = createDummyEntity(); + repository.save(dummyEntity2); + + Example example = Example.of(createDummyEntity()); + + boolean exists = repository.exists(example); + assertThat(exists).isTrue(); + } + + @Test + void existsByExampleShouldGetNone() { + + DummyEntity dummyEntity1 = createDummyEntity(); + dummyEntity1.setFlag(true); + + repository.save(dummyEntity1); + + Example example = Example.of(new DummyEntity("NotExisting")); + + boolean exists = repository.exists(example); + + assertThat(exists).isFalse(); + } + + @Test + void existsByExampleComplex() { + + final Instant pointInTime = Instant.now().minusSeconds(10000); + + final DummyEntity one = repository.save(createDummyEntity()); + + DummyEntity two = createDummyEntity(); + two.setName("Diego"); + two.setPointInTime(pointInTime); + two = repository.save(two); + + DummyEntity exampleEntitiy = createDummyEntity(); + exampleEntitiy.setName("Diego"); + exampleEntitiy.setPointInTime(pointInTime); + + Example example = Example.of(exampleEntitiy); + + boolean exists = repository.exists(example); + assertThat(exists).isTrue(); + } + + @Test + void countByExampleShouldGetOne() { + + 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")); + + long count = repository.count(example); + + assertThat(count).isOne(); + } + + @Test + void countByExampleMultipleMatchShouldGetOne() { + + DummyEntity dummyEntity1 = createDummyEntity(); + repository.save(dummyEntity1); + + DummyEntity dummyEntity2 = createDummyEntity(); + repository.save(dummyEntity2); + + Example example = Example.of(createDummyEntity()); + + long count = repository.count(example); + assertThat(count).isEqualTo(2); + } + + @Test + void countByExampleShouldGetNone() { + + DummyEntity dummyEntity1 = createDummyEntity(); + dummyEntity1.setFlag(true); + + repository.save(dummyEntity1); + + Example example = Example.of(new DummyEntity("NotExisting")); + + long count = repository.count(example); + + assertThat(count).isNotNull().isZero(); + } + + @Test + void countByExampleComplex() { + + final Instant pointInTime = Instant.now().minusSeconds(10000); + + final DummyEntity one = repository.save(createDummyEntity()); + + DummyEntity two = createDummyEntity(); + two.setName("Diego"); + two.setPointInTime(pointInTime); + two = repository.save(two); + + DummyEntity exampleEntitiy = createDummyEntity(); + exampleEntitiy.setName("Diego"); + exampleEntitiy.setPointInTime(pointInTime); + + Example example = Example.of(exampleEntitiy); + + long count = repository.count(example); + assertThat(count).isOne(); + } + + @Test + void fetchByExampleFluentAllSimple() { + String searchName = "Diego"; + + Instant now = Instant.now(); + + final DummyEntity one = repository.save(createDummyEntity()); + + DummyEntity two = createDummyEntity(); + + two.setName(searchName); + two.setPointInTime(now.minusSeconds(10000)); + two = repository.save(two); + + DummyEntity third = createDummyEntity(); + third.setName(searchName); + third.setPointInTime(now.minusSeconds(200000)); + third = repository.save(third); + + DummyEntity exampleEntitiy = createDummyEntity(); + exampleEntitiy.setName(searchName); + + 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); + } + + @Test + void fetchByExampleFluentCountSimple() { + String searchName = "Diego"; + + Instant now = Instant.now(); + + final DummyEntity one = repository.save(createDummyEntity()); + + DummyEntity two = createDummyEntity(); + + two.setName(searchName); + two.setPointInTime(now.minusSeconds(10000)); + two = repository.save(two); + + DummyEntity third = createDummyEntity(); + third.setName(searchName); + third.setPointInTime(now.minusSeconds(200000)); + third = repository.save(third); + + DummyEntity exampleEntitiy = createDummyEntity(); + exampleEntitiy.setName(searchName); + + Example example = Example.of(exampleEntitiy); + + Long matches = repository.findBy(example, FluentQuery.FetchableFluentQuery::count); + assertThat(matches).isEqualTo(2); + } + + @Test + void fetchByExampleFluentOnlyInstantFirstSimple() { + String searchName = "Diego"; + + Instant now = Instant.now(); + + final DummyEntity one = repository.save(createDummyEntity()); + + DummyEntity two = createDummyEntity(); + + two.setName(searchName); + two.setPointInTime(now.minusSeconds(10000)); + two = repository.save(two); + + DummyEntity third = createDummyEntity(); + third.setName(searchName); + third.setPointInTime(now.minusSeconds(200000)); + third = repository.save(third); + + DummyEntity exampleEntitiy = createDummyEntity(); + exampleEntitiy.setName(searchName); + + Example example = Example.of(exampleEntitiy); + + Optional matches = repository.findBy(example, + p -> p.sortBy(Sort.by("pointInTime").descending()).first()); + assertThat(matches).contains(two); + } + + @Test + void fetchByExampleFluentOnlyInstantOneValueError() { + String searchName = "Diego"; + + Instant now = Instant.now(); + + final DummyEntity one = repository.save(createDummyEntity()); + + DummyEntity two = createDummyEntity(); + + two.setName(searchName); + two.setPointInTime(now.minusSeconds(10000)); + two = repository.save(two); + + DummyEntity third = createDummyEntity(); + third.setName(searchName); + third.setPointInTime(now.minusSeconds(200000)); + third = repository.save(third); + + DummyEntity exampleEntitiy = createDummyEntity(); + exampleEntitiy.setName(searchName); + + Example example = Example.of(exampleEntitiy); + + assertThatThrownBy(() -> repository.findBy(example, p -> p.sortBy(Sort.by("pointInTime").descending()).one())) + .isInstanceOf(IncorrectResultSizeDataAccessException.class).hasMessageContaining("expected 1, actual 2"); + } + + @Test + void fetchByExampleFluentOnlyInstantOneValueSimple() { + String searchName = "Diego"; + + Instant now = Instant.now(); + + final DummyEntity one = repository.save(createDummyEntity()); + + DummyEntity two = createDummyEntity(); + + two.setName(searchName); + two.setPointInTime(now.minusSeconds(10000)); + two = repository.save(two); + + DummyEntity exampleEntitiy = createDummyEntity(); + exampleEntitiy.setName(searchName); + + Example example = Example.of(exampleEntitiy); + + Optional match = repository.findBy(example, p -> p.sortBy(Sort.by("pointInTime").descending()).one()); + + assertThat(match).contains(two); + } + + @Test + void fetchByExampleFluentOnlyInstantOneValueAsSimple() { + String searchName = "Diego"; + + Instant now = Instant.now(); + + final DummyEntity one = repository.save(createDummyEntity()); + + DummyEntity two = createDummyEntity(); + + two.setName(searchName); + two.setPointInTime(now.minusSeconds(10000)); + two = repository.save(two); + + DummyEntity exampleEntitiy = createDummyEntity(); + exampleEntitiy.setName(searchName); + + Example example = Example.of(exampleEntitiy); + + Optional match = repository.findBy(example, p -> p.as(DummyProjectExample.class).one()); + + assertThat(match.get().getName()).contains(two.getName()); + } + private Instant createDummyBeforeAndAfterNow() { Instant now = Instant.now(); @@ -598,7 +1161,11 @@ private Instant createDummyBeforeAndAfterNow() { return now; } - interface DummyEntityRepository extends CrudRepository { + interface DummyProjectExample { + String getName(); + } + + interface DummyEntityRepository extends CrudRepository, QueryByExampleExecutor { @Lock(LockMode.PESSIMISTIC_WRITE) List findAllByName(String name); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java index a94b535b5c..cd901b3df9 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java @@ -64,6 +64,7 @@ * @author Evgeni Dimitrov * @author Fei Dong * @author Chirag Tailor + * @author Diego Krupitza */ @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = TestConfiguration.class) @@ -175,7 +176,8 @@ Dialect jdbcDialect(@Qualifier("qualifierJdbcOperations") NamedParameterJdbcOper private static class DummyRepositoryBaseClass implements CrudRepository { - DummyRepositoryBaseClass(JdbcAggregateTemplate template, PersistentEntity persistentEntity) { + DummyRepositoryBaseClass(JdbcAggregateTemplate template, PersistentEntity persistentEntity, + JdbcConverter converter) { } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepositoryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepositoryUnitTests.java index e5836503b3..47bd009b8e 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepositoryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepositoryUnitTests.java @@ -24,6 +24,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.jdbc.core.JdbcAggregateOperations; +import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; /** @@ -36,11 +37,12 @@ public class SimpleJdbcRepositoryUnitTests { @Mock JdbcAggregateOperations operations; @Mock RelationalPersistentEntity entity; + @Mock JdbcConverter converter; @Test // DATAJDBC-252 public void saveReturnsEntityProducedByOperations() { - SimpleJdbcRepository repository = new SimpleJdbcRepository<>(operations, entity); + SimpleJdbcRepository repository = new SimpleJdbcRepository<>(operations, entity,converter); Sample expected = new Sample(); doReturn(expected).when(operations).save(any());