Add support for arbitrary where clauses in Single Query Loading.

Closes #1601
Original pull request: #1617
This commit is contained in:
Jens Schauder
2023-09-04 14:15:33 +02:00
committed by Mark Paluch
parent 6fb6110ca0
commit 0fdeaebbee
21 changed files with 313 additions and 46 deletions

View File

@@ -21,15 +21,22 @@ import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.relational.core.dialect.Dialect;
import org.springframework.data.relational.core.mapping.AggregatePath;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.query.CriteriaDefinition;
import org.springframework.data.relational.core.query.Query;
import org.springframework.data.relational.core.sql.Condition;
import org.springframework.data.relational.core.sql.Table;
import org.springframework.data.relational.core.sqlgeneration.AliasFactory;
import org.springframework.data.relational.core.sqlgeneration.SingleQuerySqlGenerator;
import org.springframework.data.relational.core.sqlgeneration.SqlGenerator;
import org.springframework.data.relational.domain.RowDocument;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -89,6 +96,35 @@ class AggregateReader<T> {
return jdbcTemplate.query(sqlGenerator.findAllById(), Map.of("ids", convertedIds), this::extractAll);
}
public Iterable<T> findAllBy(Query query) {
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
BiFunction<Table, RelationalPersistentEntity, Condition> condition = createConditionSource(query, parameterSource);
return jdbcTemplate.query(sqlGenerator.findAllByCondition(condition), parameterSource, this::extractAll);
}
public Optional<T> findOneByQuery(Query query) {
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
BiFunction<Table, RelationalPersistentEntity, Condition> condition = createConditionSource(query, parameterSource);
return Optional.ofNullable(
jdbcTemplate.query(sqlGenerator.findAllByCondition(condition), parameterSource, this::extractZeroOrOne));
}
private BiFunction<Table, RelationalPersistentEntity, Condition> createConditionSource(Query query, MapSqlParameterSource parameterSource) {
QueryMapper queryMapper = new QueryMapper(converter);
BiFunction<Table, RelationalPersistentEntity, Condition> condition = (table, aggregate) -> {
Optional<CriteriaDefinition> criteria = query.getCriteria();
return criteria
.map(criteriaDefinition -> queryMapper.getMappedObject(parameterSource, criteriaDefinition, table, aggregate))
.orElse(null);
};
return condition;
}
/**
* Extracts a list of aggregates from the given {@link ResultSet} by utilizing the
* {@link RowDocumentResultSetExtractor} and the {@link JdbcConverter}. When used as a method reference this conforms
@@ -115,7 +151,8 @@ class AggregateReader<T> {
* to the {@link org.springframework.jdbc.core.ResultSetExtractor} contract.
*
* @param @param rs the {@link ResultSet} from which to extract the data. Must not be {(}@literal null}.
* @return The single instance when the conversion results in exactly one instance. If the {@literal ResultSet} is empty, null is returned.
* @return The single instance when the conversion results in exactly one instance. If the {@literal ResultSet} is
* empty, null is returned.
* @throws SQLException
* @throws IncorrectResultSizeDataAccessException when the conversion yields more than one instance.
*/
@@ -190,9 +227,15 @@ class AggregateReader<T> {
return findAllById;
}
@Override
public String findAllByCondition(BiFunction<Table, RelationalPersistentEntity, Condition> conditionSource) {
return delegate.findAllByCondition(conditionSource);
}
@Override
public AliasFactory getAliasFactory() {
return delegate.getAliasFactory();
}
}
}

View File

@@ -310,7 +310,7 @@ public class QueryMapper {
sqlType = getTypeHint(mappedValue, actualType.getType(), settableValue);
} else if (criteria.getValue() instanceof ValueFunction valueFunction) {
mappedValue = valueFunction;
mappedValue = valueFunction.transform(v -> convertValue(comparator, v, propertyField.getTypeHint()));
sqlType = propertyField.getSqlType();
} else if (propertyField instanceof MetadataBackedField metadataBackedField //

View File

@@ -77,12 +77,13 @@ class SingleQueryDataAccessStrategy implements ReadingDataAccessStrategy {
@Override
public <T> Optional<T> findOne(Query query, Class<T> domainType) {
return Optional.empty();
return getReader(domainType).findOneByQuery(query);
}
@Override
public <T> Iterable<T> findAll(Query query, Class<T> domainType) {
throw new UnsupportedOperationException();
return getReader(domainType).findAllBy(query);
}
@Override

View File

@@ -16,9 +16,11 @@
package org.springframework.data.jdbc.core.convert;
import java.util.Collections;
import java.util.Optional;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.data.relational.core.query.Query;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.util.Assert;
@@ -85,13 +87,37 @@ class SingleQueryFallbackDataAccessStrategy extends DelegatingDataAccessStrategy
return super.findAllById(ids, domainType);
}
private boolean isSingleSelectQuerySupported(Class<?> entityType) {
public <T> Optional<T> findOne(Query query, Class<T> domainType) {
return sqlGeneratorSource.getDialect().supportsSingleQueryLoading()//
&& entityQualifiesForSingleSelectQuery(entityType);
if (isSingleSelectQuerySupported(domainType) && isSingleSelectQuerySupported(query)) {
return singleSelectDelegate.findOne(query, domainType);
}
return super.findOne(query, domainType);
}
private boolean entityQualifiesForSingleSelectQuery(Class<?> entityType) {
@Override
public <T> Iterable<T> findAll(Query query, Class<T> domainType) {
if (isSingleSelectQuerySupported(domainType) && isSingleSelectQuerySupported(query)) {
return singleSelectDelegate.findAll(query, domainType);
}
return super.findAll(query, domainType);
}
private static boolean isSingleSelectQuerySupported(Query query) {
return !query.isSorted() && !query.isLimited();
}
private boolean isSingleSelectQuerySupported(Class<?> entityType) {
return converter.getMappingContext().isSingleQueryLoadingEnabled()
&& sqlGeneratorSource.getDialect().supportsSingleQueryLoading()//
&& entityQualifiesForSingleQueryLoading(entityType);
}
private boolean entityQualifiesForSingleQueryLoading(Class<?> entityType) {
boolean referenceFound = false;
for (PersistentPropertyPath<RelationalPersistentProperty> path : converter.getMappingContext()
@@ -113,9 +139,9 @@ class SingleQueryFallbackDataAccessStrategy extends DelegatingDataAccessStrategy
}
// AggregateReferences aren't supported yet
if (property.isAssociation()) {
return false;
}
// if (property.isAssociation()) {
// return false;
// }
}
return true;

View File

@@ -45,8 +45,8 @@ public class EscapingParameterSource implements SqlParameterSource {
public Object getValue(String paramName) throws IllegalArgumentException {
Object value = parameterSource.getValue(paramName);
if (value instanceof ValueFunction<?>) {
return ((ValueFunction<?>) value).apply(escaper);
if (value instanceof ValueFunction<?> valueFunction) {
return valueFunction.apply(escaper);
}
return value;
}

View File

@@ -23,15 +23,8 @@ import static org.springframework.data.jdbc.testing.TestConfiguration.*;
import static org.springframework.data.jdbc.testing.TestDatabaseFeatures.Feature.*;
import java.time.LocalDateTime;
import java.util.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.IntStream;
@@ -42,6 +35,7 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.annotation.Id;
@@ -64,6 +58,9 @@ import org.springframework.data.relational.core.mapping.InsertOnlyProperty;
import org.springframework.data.relational.core.mapping.MappedCollection;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.Table;
import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.relational.core.query.CriteriaDefinition;
import org.springframework.data.relational.core.query.Query;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.test.context.ActiveProfiles;
@@ -223,6 +220,62 @@ abstract class AbstractJdbcAggregateTemplateIntegrationTests {
.containsExactlyInAnyOrder(tuple(entity.id, "entity"), tuple(yetAnother.id, "yetAnother"));
}
@Test // GH-1601
void findAllByQuery() {
template.save(SimpleListParent.of("one", "one_1"));
SimpleListParent two = template.save(SimpleListParent.of("two", "two_1", "two_2"));
template.save(SimpleListParent.of("three", "three_1", "three_2", "three_3"));
CriteriaDefinition criteria = CriteriaDefinition.from(Criteria.where("id").is(two.id));
Query query = Query.query(criteria);
Iterable<SimpleListParent> reloadedById = template.findAll(query, SimpleListParent.class);
assertThat(reloadedById).extracting(e -> e.id, e -> e.content.size()).containsExactly(tuple(two.id, 2));
}
@Test // GH-1601
void findOneByQuery() {
template.save(SimpleListParent.of("one", "one_1"));
SimpleListParent two = template.save(SimpleListParent.of("two", "two_1", "two_2"));
template.save(SimpleListParent.of("three", "three_1", "three_2", "three_3"));
CriteriaDefinition criteria = CriteriaDefinition.from(Criteria.where("id").is(two.id));
Query query = Query.query(criteria);
Optional<SimpleListParent> reloadedById = template.findOne(query, SimpleListParent.class);
assertThat(reloadedById).get().extracting(e -> e.id, e -> e.content.size()).containsExactly(two.id, 2);
}
@Test // GH-1601
void findOneByQueryNothingFound() {
template.save(SimpleListParent.of("one", "one_1"));
SimpleListParent two = template.save(SimpleListParent.of("two", "two_1", "two_2"));
template.save(SimpleListParent.of("three", "three_1", "three_2", "three_3"));
CriteriaDefinition criteria = CriteriaDefinition.from(Criteria.where("id").is(4711));
Query query = Query.query(criteria);
Optional<SimpleListParent> reloadedById = template.findOne(query, SimpleListParent.class);
assertThat(reloadedById).isEmpty();
}
@Test // GH-1601
void findOneByQueryToManyResults() {
template.save(SimpleListParent.of("one", "one_1"));
SimpleListParent two = template.save(SimpleListParent.of("two", "two_1", "two_2"));
template.save(SimpleListParent.of("three", "three_1", "three_2", "three_3"));
CriteriaDefinition criteria = CriteriaDefinition.from(Criteria.where("id").not(two.id));
Query query = Query.query(criteria);
assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class)
.isThrownBy(() -> template.findOne(query, SimpleListParent.class));
}
@Test // DATAJDBC-112
@EnabledOnFeature(SUPPORTS_QUOTED_IDS)
void saveAndLoadAnEntityWithReferencedEntityById() {
@@ -1266,6 +1319,29 @@ abstract class AbstractJdbcAggregateTemplateIntegrationTests {
private String content;
}
@SuppressWarnings("unused")
static class SimpleListParent {
@Id private Long id;
String name;
List<ElementNoId> content = new ArrayList<>();
static SimpleListParent of(String name, String... contents) {
SimpleListParent parent = new SimpleListParent();
parent.name = name;
for (String content : contents) {
ElementNoId element = new ElementNoId();
element.content = content;
parent.content.add(element);
}
return parent;
}
}
@Table("LIST_PARENT")
@SuppressWarnings("unused")
static class ListParent {

View File

@@ -6,6 +6,7 @@ DROP TABLE ONE_TO_ONE_PARENT;
DROP TABLE ELEMENT_NO_ID;
DROP TABLE LIST_PARENT;
DROP TABLE SIMPLE_LIST_PARENT;
DROP TABLE BYTE_ARRAY_OWNER;
@@ -74,11 +75,18 @@ CREATE TABLE LIST_PARENT
"id4" BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY,
NAME VARCHAR(100)
);
CREATE TABLE SIMPLE_LIST_PARENT
(
ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY,
NAME VARCHAR(100)
);
CREATE TABLE ELEMENT_NO_ID
(
CONTENT VARCHAR(100),
LIST_PARENT_KEY BIGINT,
LIST_PARENT BIGINT
SIMPLE_LIST_PARENT_KEY BIGINT,
LIST_PARENT BIGINT,
SIMPLE_LIST_PARENT BIGINT
);
ALTER TABLE ELEMENT_NO_ID
ADD FOREIGN KEY (LIST_PARENT)

View File

@@ -32,9 +32,17 @@ CREATE TABLE LIST_PARENT
NAME VARCHAR(100)
);
CREATE TABLE SIMPLE_LIST_PARENT
(
ID SERIAL PRIMARY KEY,
NAME VARCHAR(100)
);
CREATE TABLE element_no_id
(
content VARCHAR(100),
SIMPLE_LIST_PARENT_key BIGINT,
SIMPLE_LIST_PARENT INTEGER,
LIST_PARENT_key BIGINT,
LIST_PARENT INTEGER
);

View File

@@ -26,6 +26,11 @@ CREATE TABLE Child_No_Id
content VARCHAR(30)
);
CREATE TABLE SIMPLE_LIST_PARENT
(
ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY,
NAME VARCHAR(100)
);
CREATE TABLE LIST_PARENT
(
"id4" BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY,
@@ -34,6 +39,8 @@ CREATE TABLE LIST_PARENT
CREATE TABLE ELEMENT_NO_ID
(
CONTENT VARCHAR(100),
SIMPLE_LIST_PARENT_KEY BIGINT,
SIMPLE_LIST_PARENT BIGINT,
LIST_PARENT_KEY BIGINT,
LIST_PARENT BIGINT
);

View File

@@ -31,9 +31,16 @@ CREATE TABLE LIST_PARENT
`id4` BIGINT AUTO_INCREMENT PRIMARY KEY,
NAME VARCHAR(100)
);
CREATE TABLE SIMPLE_LIST_PARENT
(
ID BIGINT AUTO_INCREMENT PRIMARY KEY,
NAME VARCHAR(100)
);
CREATE TABLE element_no_id
(
CONTENT VARCHAR(100),
SIMPLE_LIST_PARENT_key BIGINT,
SIMPLE_LIST_PARENT BIGINT,
LIST_PARENT_key BIGINT,
LIST_PARENT BIGINT
);

View File

@@ -30,14 +30,22 @@ CREATE TABLE Child_No_Id
DROP TABLE IF EXISTS element_no_id;
DROP TABLE IF EXISTS LIST_PARENT;
DROP TABLE IF EXISTS SIMPLE_LIST_PARENT;
CREATE TABLE LIST_PARENT
(
[id4] BIGINT IDENTITY PRIMARY KEY,
NAME VARCHAR(100)
);
CREATE TABLE SIMPLE_LIST_PARENT
(
ID BIGINT IDENTITY PRIMARY KEY,
NAME VARCHAR(100)
);
CREATE TABLE element_no_id
(
CONTENT VARCHAR(100),
SIMPLE_LIST_PARENT_key BIGINT,
SIMPLE_LIST_PARENT BIGINT,
LIST_PARENT_key BIGINT,
LIST_PARENT BIGINT
);

View File

@@ -26,6 +26,11 @@ CREATE TABLE Child_No_Id
`content` VARCHAR(30)
);
CREATE TABLE SIMPLE_LIST_PARENT
(
ID BIGINT AUTO_INCREMENT PRIMARY KEY,
NAME VARCHAR(100)
);
CREATE TABLE LIST_PARENT
(
`id4` BIGINT AUTO_INCREMENT PRIMARY KEY,
@@ -35,7 +40,9 @@ CREATE TABLE element_no_id
(
CONTENT VARCHAR(100),
LIST_PARENT_key BIGINT,
LIST_PARENT BIGINT
SIMPLE_LIST_PARENT_key BIGINT,
LIST_PARENT BIGINT,
SIMPLE_LIST_PARENT BIGINT
);
CREATE TABLE BYTE_ARRAY_OWNER

View File

@@ -4,6 +4,7 @@ DROP TABLE CHILD_NO_ID CASCADE CONSTRAINTS PURGE;
DROP TABLE ONE_TO_ONE_PARENT CASCADE CONSTRAINTS PURGE;
DROP TABLE ELEMENT_NO_ID CASCADE CONSTRAINTS PURGE;
DROP TABLE LIST_PARENT CASCADE CONSTRAINTS PURGE;
DROP TABLE SIMPLE_LIST_PARENT CASCADE CONSTRAINTS PURGE;
DROP TABLE BYTE_ARRAY_OWNER CASCADE CONSTRAINTS PURGE;
DROP TABLE CHAIN0 CASCADE CONSTRAINTS PURGE;
DROP TABLE CHAIN1 CASCADE CONSTRAINTS PURGE;
@@ -64,9 +65,16 @@ CREATE TABLE LIST_PARENT
"id4" NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY,
NAME VARCHAR(100)
);
CREATE TABLE SIMPLE_LIST_PARENT
(
ID NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY,
NAME VARCHAR(100)
);
CREATE TABLE element_no_id
(
CONTENT VARCHAR(100),
SIMPLE_LIST_PARENT_key NUMBER,
SIMPLE_LIST_PARENT NUMBER,
LIST_PARENT_key NUMBER,
LIST_PARENT NUMBER
);

View File

@@ -4,6 +4,7 @@ DROP TABLE ONE_TO_ONE_PARENT;
DROP TABLE Child_No_Id;
DROP TABLE element_no_id;
DROP TABLE "LIST_PARENT";
DROP TABLE SIMPLE_LIST_PARENT;
DROP TABLE "ARRAY_OWNER";
DROP TABLE DOUBLE_LIST_OWNER;
DROP TABLE FLOAT_LIST_OWNER;
@@ -68,11 +69,19 @@ CREATE TABLE "LIST_PARENT"
NAME VARCHAR(100)
);
CREATE TABLE SIMPLE_LIST_PARENT
(
id SERIAL PRIMARY KEY,
NAME VARCHAR(100)
);
CREATE TABLE element_no_id
(
content VARCHAR(100),
LIST_PARENT_key BIGINT,
"LIST_PARENT" INTEGER
SIMPLE_LIST_PARENT_key BIGINT,
"LIST_PARENT" INTEGER,
SIMPLE_LIST_PARENT INTEGER
);
CREATE TABLE "ARRAY_OWNER"