Enable Single Query Loading for simple aggregates.
Single Query Loading loads as the name suggests complete aggregates using a single select. While the ultimate goal is to support this for all aggregates, this commit enables it only for simple aggregate and also only for some operations. A simple aggregate is an aggregate that only reference up to one other entity and does not have embedded entities. The supported operations are those available via `CrudRepository`: `findAll`, `findById`, and `findAllByIds`. Single Query Loading does NOT work with the supported in memory databases H2 and HSQLDB, since these do not properly support windowing functions, which are essential for Single Query Loading. To turn on Single Query Loading call `RelationalMappingContext.setSingleQueryLoadingEnabled(true)`. Closes #1446 See #1450 See #1445 Original pull request: #1572
This commit is contained in:
committed by
Mark Paluch
parent
dffde372f9
commit
93821f5f42
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
{
|
||||
echo "mcr.microsoft.com/mssql/server:2019-CU16-ubuntu-20.04"
|
||||
echo "mcr.microsoft.com/mssql/server:2022-CU5-ubuntu-20.04"
|
||||
echo "ibmcom/db2:11.5.7.0a"
|
||||
echo "harbor-repo.vmware.com/mcr-proxy-cache/mssql/server:2019-CU16-ubuntu-20.04"
|
||||
echo "harbor-repo.vmware.com/dockerhub-proxy-cache/ibmcom/db2:11.5.7.0a"
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2023 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.core.convert;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
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.RelationalMappingContext;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.data.relational.core.sqlgeneration.AliasFactory;
|
||||
import org.springframework.data.relational.core.sqlgeneration.CachingSqlGenerator;
|
||||
import org.springframework.data.relational.core.sqlgeneration.SingleQuerySqlGenerator;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Reads complete Aggregates from the database, by generating appropriate SQL using a {@link SingleQuerySqlGenerator}
|
||||
* and a matching {@link AggregateResultSetExtractor} and invoking a
|
||||
* {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate}
|
||||
*
|
||||
* @param <T> the type of aggregate produced by this reader.
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class AggregateReader<T> {
|
||||
|
||||
private final RelationalMappingContext mappingContext;
|
||||
private final RelationalPersistentEntity<T> aggregate;
|
||||
private final AliasFactory aliasFactory;
|
||||
private final org.springframework.data.relational.core.sqlgeneration.SqlGenerator sqlGenerator;
|
||||
private final JdbcConverter converter;
|
||||
private final NamedParameterJdbcOperations jdbcTemplate;
|
||||
|
||||
AggregateReader(RelationalMappingContext mappingContext, Dialect dialect, JdbcConverter converter,
|
||||
NamedParameterJdbcOperations jdbcTemplate, RelationalPersistentEntity<T> aggregate) {
|
||||
|
||||
this.mappingContext = mappingContext;
|
||||
|
||||
this.aggregate = aggregate;
|
||||
this.converter = converter;
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
|
||||
this.sqlGenerator = new CachingSqlGenerator(new SingleQuerySqlGenerator(mappingContext, dialect, aggregate));
|
||||
this.aliasFactory = sqlGenerator.getAliasFactory();
|
||||
}
|
||||
|
||||
public List<T> findAll() {
|
||||
|
||||
String sql = sqlGenerator.findAll();
|
||||
|
||||
PathToColumnMapping pathToColumn = createPathToColumnMapping(aliasFactory);
|
||||
AggregateResultSetExtractor<T> extractor = new AggregateResultSetExtractor<>(mappingContext, aggregate, converter,
|
||||
pathToColumn);
|
||||
|
||||
Iterable<T> result = jdbcTemplate.query(sql, extractor);
|
||||
|
||||
Assert.state(result != null, "result is null");
|
||||
|
||||
return (List<T>) result;
|
||||
}
|
||||
|
||||
public T findById(Object id) {
|
||||
|
||||
PathToColumnMapping pathToColumn = createPathToColumnMapping(aliasFactory);
|
||||
AggregateResultSetExtractor<T> extractor = new AggregateResultSetExtractor<>(mappingContext, aggregate, converter,
|
||||
pathToColumn);
|
||||
|
||||
String sql = sqlGenerator.findById();
|
||||
|
||||
id = converter.writeValue(id, aggregate.getRequiredIdProperty().getTypeInformation());
|
||||
|
||||
Iterator<T> result = jdbcTemplate.query(sql, Map.of("id", id), extractor).iterator();
|
||||
|
||||
T returnValue = result.hasNext() ? result.next() : null;
|
||||
|
||||
if (result.hasNext()) {
|
||||
throw new IncorrectResultSizeDataAccessException(1);
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
public Iterable<T> findAllById(Iterable<?> ids) {
|
||||
|
||||
PathToColumnMapping pathToColumn = createPathToColumnMapping(aliasFactory);
|
||||
AggregateResultSetExtractor<T> extractor = new AggregateResultSetExtractor<>(mappingContext, aggregate, converter,
|
||||
pathToColumn);
|
||||
|
||||
String sql = sqlGenerator.findAllById();
|
||||
|
||||
List<Object> convertedIds = new ArrayList<>();
|
||||
for (Object id : ids) {
|
||||
convertedIds.add(converter.writeValue(id, aggregate.getRequiredIdProperty().getTypeInformation()));
|
||||
}
|
||||
|
||||
return jdbcTemplate.query(sql, Map.of("ids", convertedIds), extractor);
|
||||
}
|
||||
|
||||
private PathToColumnMapping createPathToColumnMapping(AliasFactory aliasFactory) {
|
||||
return new PathToColumnMapping() {
|
||||
@Override
|
||||
public String column(AggregatePath path) {
|
||||
|
||||
String alias = aliasFactory.getColumnAlias(path);
|
||||
Assert.notNull(alias, () -> "alias for >" + path + "<must not be null");
|
||||
return alias;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String keyColumn(AggregatePath path) {
|
||||
return aliasFactory.getKeyAlias(path);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2023 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.core.convert;
|
||||
|
||||
import org.springframework.data.relational.core.dialect.Dialect;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
|
||||
|
||||
/**
|
||||
* Creates {@link AggregateReader} instances.
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class AggregateReaderFactory {
|
||||
|
||||
private final RelationalMappingContext mappingContext;
|
||||
private final Dialect dialect;
|
||||
private final JdbcConverter converter;
|
||||
private final NamedParameterJdbcOperations jdbcTemplate;
|
||||
|
||||
public AggregateReaderFactory(RelationalMappingContext mappingContext, Dialect dialect, JdbcConverter converter,
|
||||
NamedParameterJdbcOperations jdbcTemplate) {
|
||||
|
||||
this.mappingContext = mappingContext;
|
||||
this.dialect = dialect;
|
||||
this.converter = converter;
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
<T> AggregateReader<T> createAggregateReaderFor(RelationalPersistentEntity<T> entity) {
|
||||
return new AggregateReader<>(mappingContext, dialect, converter, jdbcTemplate, entity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,596 @@
|
||||
/*
|
||||
* Copyright 2023 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.core.convert;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.util.AbstractCollection;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.data.mapping.Parameter;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.PropertyHandler;
|
||||
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
|
||||
import org.springframework.data.mapping.model.EntityInstantiator;
|
||||
import org.springframework.data.mapping.model.ParameterValueProvider;
|
||||
import org.springframework.data.relational.core.mapping.AggregatePath;
|
||||
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.util.TypeInformation;
|
||||
import org.springframework.jdbc.core.ResultSetExtractor;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Extracts complete aggregates from a {@link ResultSet}. The {@literal ResultSet} must have a very special structure
|
||||
* which looks somewhat how one would represent an aggregate in a single excel table. The first row contains data of the
|
||||
* aggregate root, any single valued reference and the first element of any collection. Following rows do NOT repeat the
|
||||
* aggregate root data but contain data of second elements of any collections. For details see accompanying unit tests.
|
||||
*
|
||||
* @param <T> the type of aggregates to extract
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class AggregateResultSetExtractor<T> implements ResultSetExtractor<Iterable<T>> {
|
||||
|
||||
private final RelationalMappingContext context;
|
||||
private final RelationalPersistentEntity<T> rootEntity;
|
||||
private final JdbcConverter converter;
|
||||
private final PathToColumnMapping propertyToColumn;
|
||||
|
||||
/**
|
||||
* @param context the {@link org.springframework.data.mapping.context.MappingContext} providing the metadata for the
|
||||
* aggregate and its entity. Must not be {@literal null}.
|
||||
* @param rootEntity the aggregate root. Must not be {@literal null}.
|
||||
* @param converter Used for converting objects from the database to whatever is required by the aggregate. Must not
|
||||
* be {@literal null}.
|
||||
* @param pathToColumn a mapping from {@link org.springframework.data.relational.core.mapping.AggregatePath} to the
|
||||
* column of the {@link ResultSet} that holds the data for that
|
||||
* {@link org.springframework.data.relational.core.mapping.AggregatePath}.
|
||||
*/
|
||||
AggregateResultSetExtractor(RelationalMappingContext context, RelationalPersistentEntity<T> rootEntity,
|
||||
JdbcConverter converter, PathToColumnMapping pathToColumn) {
|
||||
|
||||
Assert.notNull(context, "context must not be null");
|
||||
Assert.notNull(rootEntity, "rootEntity must not be null");
|
||||
Assert.notNull(converter, "converter must not be null");
|
||||
Assert.notNull(pathToColumn, "propertyToColumn must not be null");
|
||||
|
||||
this.context = context;
|
||||
this.rootEntity = rootEntity;
|
||||
this.converter = converter;
|
||||
this.propertyToColumn = pathToColumn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<T> extractData(ResultSet resultSet) throws DataAccessException {
|
||||
|
||||
CachingResultSet crs = new CachingResultSet(resultSet);
|
||||
|
||||
CollectionReader reader = new CollectionReader(crs);
|
||||
|
||||
while (crs.next()) {
|
||||
reader.read();
|
||||
}
|
||||
|
||||
return (Iterable<T>) reader.getResultAndReset();
|
||||
}
|
||||
|
||||
/**
|
||||
* create an instance and populate all its properties
|
||||
*/
|
||||
@Nullable
|
||||
private Object hydrateInstance(EntityInstantiator instantiator, ResultSetParameterValueProvider valueProvider,
|
||||
RelationalPersistentEntity<?> entity) {
|
||||
|
||||
if (!valueProvider.basePath.isRoot() && // this is a nested ValueProvider
|
||||
valueProvider.basePath.getRequiredLeafProperty().isEmbedded() && // it's an embedded
|
||||
!valueProvider.basePath.getRequiredLeafProperty().shouldCreateEmptyEmbedded() && // it's embedded
|
||||
!valueProvider.hasValue()) { // all values have been null
|
||||
return null;
|
||||
}
|
||||
|
||||
Object instance = instantiator.createInstance(entity, valueProvider);
|
||||
|
||||
PersistentPropertyAccessor<?> accessor = new ConvertingPropertyAccessor<>(entity.getPropertyAccessor(instance),
|
||||
converter.getConversionService());
|
||||
|
||||
if (entity.requiresPropertyPopulation()) {
|
||||
|
||||
entity.doWithProperties((PropertyHandler<RelationalPersistentProperty>) p -> {
|
||||
|
||||
if (!entity.isCreatorArgument(p)) {
|
||||
accessor.setProperty(p, valueProvider.getValue(p));
|
||||
}
|
||||
});
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Reader} is responsible for reading a single entity or collection of entities from a set of columns
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
private interface Reader {
|
||||
|
||||
/**
|
||||
* read the data needed for creating the result of this {@literal Reader}
|
||||
*/
|
||||
void read();
|
||||
|
||||
/**
|
||||
* Checks if this {@literal Reader} has all the data needed for a complete result, or if it needs to read further
|
||||
* rows.
|
||||
*
|
||||
* @return the result of the check.
|
||||
*/
|
||||
boolean hasResult();
|
||||
|
||||
/**
|
||||
* Constructs the result, returns it and resets the state of the reader to read the next instance.
|
||||
*
|
||||
* @return an instance of whatever this {@literal Reader} is supposed to read.
|
||||
*/
|
||||
@Nullable
|
||||
Object getResultAndReset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts a {@link Map} to the interface of a {@literal Collection<Map.Entry<Object, Object>>}.
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
private static class MapAdapter extends AbstractCollection<Map.Entry<Object, Object>> {
|
||||
|
||||
private final Map<Object, Object> map = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Iterator<Map.Entry<Object, Object>> iterator() {
|
||||
return map.entrySet().iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return map.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean add(Map.Entry<Object, Object> entry) {
|
||||
|
||||
map.put(entry.getKey(), entry.getValue());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts a {@link List} to the interface of a {@literal Collection<Map.Entry<Object, Object>>}.
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
private static class ListAdapter extends AbstractCollection<Map.Entry<Object, Object>> {
|
||||
|
||||
private final List<Object> list = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public Iterator<Map.Entry<Object, Object>> iterator() {
|
||||
throw new UnsupportedOperationException("Do we need this?");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return list.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean add(Map.Entry<Object, Object> entry) {
|
||||
|
||||
Integer index = (Integer) entry.getKey();
|
||||
while (index >= list.size()) {
|
||||
list.add(null);
|
||||
}
|
||||
list.set(index, entry.getValue());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Reader} for reading entities.
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
private class EntityReader implements Reader {
|
||||
|
||||
/**
|
||||
* Debugging the recursive structure of {@link Reader} instances can become a little mind bending. Giving each
|
||||
* {@literal Reader} a descriptive name helps with that.
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
private final AggregatePath basePath;
|
||||
private final CachingResultSet crs;
|
||||
|
||||
private final EntityInstantiator instantiator;
|
||||
@Nullable private final String idColumn;
|
||||
|
||||
private ResultSetParameterValueProvider valueProvider;
|
||||
private boolean result;
|
||||
|
||||
Object oldId = null;
|
||||
|
||||
private EntityReader(AggregatePath basePath, CachingResultSet crs) {
|
||||
this(basePath, crs, null);
|
||||
}
|
||||
|
||||
private EntityReader(AggregatePath basePath, CachingResultSet crs, @Nullable String keyColumn) {
|
||||
|
||||
this.basePath = basePath;
|
||||
this.crs = crs;
|
||||
|
||||
RelationalPersistentEntity<?> entity = basePath.isRoot() ? rootEntity : basePath.getRequiredLeafEntity();
|
||||
instantiator = converter.getEntityInstantiators().getInstantiatorFor(entity);
|
||||
|
||||
idColumn = entity.hasIdProperty() ? propertyToColumn.column(basePath.append(entity.getRequiredIdProperty()))
|
||||
: keyColumn;
|
||||
|
||||
reset();
|
||||
|
||||
name = "EntityReader for " + (basePath.isRoot() ? "<root>" : basePath.toDotPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read() {
|
||||
|
||||
if (idColumn != null && oldId == null) {
|
||||
oldId = crs.getObject(idColumn);
|
||||
}
|
||||
|
||||
valueProvider.readValues();
|
||||
if (idColumn == null) {
|
||||
result = true;
|
||||
} else {
|
||||
Object peekedId = crs.peek(idColumn);
|
||||
if (peekedId == null || !peekedId.equals(oldId)) {
|
||||
|
||||
result = true;
|
||||
oldId = peekedId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Object getResultAndReset() {
|
||||
|
||||
try {
|
||||
return hydrateInstance(instantiator, valueProvider, valueProvider.baseEntity);
|
||||
} finally {
|
||||
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
|
||||
valueProvider = new ResultSetParameterValueProvider(crs, basePath);
|
||||
result = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Reader} for reading collections of entities.
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class CollectionReader implements Reader {
|
||||
|
||||
// debugging only
|
||||
private final String name;
|
||||
|
||||
private final Supplier<Collection> collectionInitializer;
|
||||
private final Reader entityReader;
|
||||
|
||||
private Collection result;
|
||||
|
||||
private static Supplier<Collection> collectionInitializerFor(AggregatePath path) {
|
||||
|
||||
RelationalPersistentProperty property = path.getRequiredLeafProperty();
|
||||
if (List.class.isAssignableFrom(property.getType())) {
|
||||
return ListAdapter::new;
|
||||
} else if (property.isMap()) {
|
||||
return MapAdapter::new;
|
||||
} else {
|
||||
return HashSet::new;
|
||||
}
|
||||
}
|
||||
|
||||
private CollectionReader(AggregatePath basePath, CachingResultSet crs) {
|
||||
|
||||
this.collectionInitializer = collectionInitializerFor(basePath);
|
||||
|
||||
String keyColumn = null;
|
||||
final RelationalPersistentProperty property = basePath.getRequiredLeafProperty();
|
||||
if (property.isMap() || List.class.isAssignableFrom(basePath.getRequiredLeafProperty().getType())) {
|
||||
keyColumn = propertyToColumn.keyColumn(basePath);
|
||||
}
|
||||
|
||||
if (property.isQualified()) {
|
||||
this.entityReader = new EntryReader(basePath, crs, keyColumn, property.getQualifierColumnType());
|
||||
} else {
|
||||
this.entityReader = new EntityReader(basePath, crs, keyColumn);
|
||||
}
|
||||
reset();
|
||||
name = "Reader for " + basePath.toDotPath();
|
||||
}
|
||||
|
||||
private CollectionReader(CachingResultSet crs) {
|
||||
|
||||
this.collectionInitializer = ArrayList::new;
|
||||
this.entityReader = new EntityReader(context.getAggregatePath(rootEntity), crs);
|
||||
reset();
|
||||
|
||||
name = "Collectionreader for <root>";
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read() {
|
||||
|
||||
entityReader.read();
|
||||
if (entityReader.hasResult()) {
|
||||
result.add(entityReader.getResultAndReset());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasResult() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getResultAndReset() {
|
||||
|
||||
try {
|
||||
if (result instanceof MapAdapter) {
|
||||
return ((MapAdapter) result).map;
|
||||
}
|
||||
if (result instanceof ListAdapter) {
|
||||
return ((ListAdapter) result).list;
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
result = collectionInitializer.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Reader} for reading collection entries. Most of the work is done by an {@link EntityReader}, but a
|
||||
* additional key column might get read. The result is
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
private class EntryReader implements Reader {
|
||||
|
||||
final EntityReader delegate;
|
||||
final String keyColumn;
|
||||
private final TypeInformation<?> keyColumnType;
|
||||
|
||||
Object key;
|
||||
|
||||
EntryReader(AggregatePath basePath, CachingResultSet crs, String keyColumn, Class<?> keyColumnType) {
|
||||
|
||||
this.keyColumnType = TypeInformation.of(keyColumnType);
|
||||
this.delegate = new EntityReader(basePath, crs, keyColumn);
|
||||
this.keyColumn = keyColumn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read() {
|
||||
|
||||
if (key == null) {
|
||||
Object unconvertedKeyObject = delegate.crs.getObject(keyColumn);
|
||||
key = converter.readValue(unconvertedKeyObject, keyColumnType);
|
||||
}
|
||||
delegate.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasResult() {
|
||||
return delegate.hasResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getResultAndReset() {
|
||||
|
||||
try {
|
||||
return new AbstractMap.SimpleEntry<>(key, delegate.getResultAndReset());
|
||||
} finally {
|
||||
key = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link ParameterValueProvider} that provided the values for an entity from a continues set of rows in a {@link ResultSet}. These might be referenced entities or collections of such entities. {@link ResultSet}.
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
private class ResultSetParameterValueProvider implements ParameterValueProvider<RelationalPersistentProperty> {
|
||||
|
||||
private final CachingResultSet rs;
|
||||
/**
|
||||
* The path which is used to determine columnNames
|
||||
*/
|
||||
private final AggregatePath basePath;
|
||||
private final RelationalPersistentEntity<?> baseEntity;
|
||||
|
||||
/**
|
||||
* Holds all the values for the entity, either directly or in the form of an appropriate {@link Reader}.
|
||||
*/
|
||||
private final Map<RelationalPersistentProperty, Object> aggregatedValues = new HashMap<>();
|
||||
|
||||
ResultSetParameterValueProvider(CachingResultSet rs, AggregatePath basePath) {
|
||||
|
||||
this.rs = rs;
|
||||
this.basePath = basePath;
|
||||
this.baseEntity = basePath.isRoot() ? rootEntity
|
||||
: context.getRequiredPersistentEntity(basePath.getRequiredLeafProperty().getActualType());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
@Nullable
|
||||
public <S> S getParameterValue(Parameter<S, RelationalPersistentProperty> parameter) {
|
||||
|
||||
return (S) getValue(baseEntity.getRequiredPersistentProperty(parameter.getName()));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Object getValue(RelationalPersistentProperty property) {
|
||||
|
||||
Object value = aggregatedValues.get(property);
|
||||
|
||||
if (value instanceof Reader) {
|
||||
return ((Reader) value).getResultAndReset();
|
||||
}
|
||||
|
||||
value = converter.readValue(value, property.getTypeInformation());
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* read values for all collection like properties and aggregate them in a collection.
|
||||
*/
|
||||
void readValues() {
|
||||
baseEntity.forEach(this::readValue);
|
||||
}
|
||||
|
||||
private void readValue(RelationalPersistentProperty p) {
|
||||
|
||||
if (p.isEntity()) {
|
||||
|
||||
Reader reader = null;
|
||||
|
||||
if (p.isCollectionLike() || p.isMap()) { // even when there are no values we still want a (empty) collection.
|
||||
|
||||
reader = (Reader) aggregatedValues.computeIfAbsent(p, pp -> new CollectionReader(basePath.append(pp), rs));
|
||||
}
|
||||
if (getIndicatorOf(p) != null) {
|
||||
|
||||
if (!(p.isCollectionLike() || p.isMap())) { // for single entities we want a null entity instead of on filled
|
||||
// with null values.
|
||||
|
||||
reader = (Reader) aggregatedValues.computeIfAbsent(p, pp -> new EntityReader(basePath.append(pp), rs));
|
||||
}
|
||||
|
||||
Assert.state(reader != null, "reader must not be null");
|
||||
|
||||
reader.read();
|
||||
}
|
||||
} else {
|
||||
aggregatedValues.computeIfAbsent(p, this::getObject);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Object getIndicatorOf(RelationalPersistentProperty p) {
|
||||
if (p.isMap() || List.class.isAssignableFrom(p.getType())) {
|
||||
return rs.getObject(getKeyName(p));
|
||||
}
|
||||
|
||||
if (p.isEmbedded()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return rs.getObject(getColumnName(p));
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a single columnValue from the resultset without throwing an exception. If the column does not exist a null
|
||||
* value is returned. Does not instantiate complex objects.
|
||||
*
|
||||
* @param property
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
private Object getObject(RelationalPersistentProperty property) {
|
||||
return rs.getObject(getColumnName(property));
|
||||
}
|
||||
|
||||
/**
|
||||
* converts a property into a column name representing that property.
|
||||
*
|
||||
* @param property
|
||||
* @return
|
||||
*/
|
||||
private String getColumnName(RelationalPersistentProperty property) {
|
||||
|
||||
return propertyToColumn.column(basePath.append(property));
|
||||
}
|
||||
|
||||
private String getKeyName(RelationalPersistentProperty property) {
|
||||
|
||||
return propertyToColumn.keyColumn(basePath.append(property));
|
||||
}
|
||||
|
||||
private boolean hasValue() {
|
||||
|
||||
for (Object value : aggregatedValues.values()) {
|
||||
if (value != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -394,6 +394,7 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc
|
||||
}
|
||||
|
||||
private <S> ReadingContext<S> extendBy(RelationalPersistentProperty property) {
|
||||
|
||||
return new ReadingContext<>(
|
||||
(RelationalPersistentEntity<S>) getMappingContext().getRequiredPersistentEntity(property.getActualType()),
|
||||
rootPath.append(property), path.append(property), identifier, key,
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2023 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.core.convert;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Despite its name not really a {@link ResultSet}, but it offers the part of the {@literal ResultSet} API that is used
|
||||
* by {@link AggregateReader}. It allows peeking in the next row of a ResultSet by caching one row of the ResultSet.
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class CachingResultSet {
|
||||
|
||||
private final ResultSetAccessor accessor;
|
||||
private final ResultSet resultSet;
|
||||
private Cache cache;
|
||||
|
||||
CachingResultSet(ResultSet resultSet) {
|
||||
|
||||
this.accessor = new ResultSetAccessor(resultSet);
|
||||
this.resultSet = resultSet;
|
||||
}
|
||||
|
||||
public boolean next() {
|
||||
|
||||
if (isPeeking()) {
|
||||
|
||||
final boolean next = cache.next;
|
||||
cache = null;
|
||||
return next;
|
||||
}
|
||||
|
||||
try {
|
||||
return resultSet.next();
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed to advance CachingResultSet", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Object getObject(String columnLabel) {
|
||||
|
||||
Object returnValue;
|
||||
if (isPeeking()) {
|
||||
returnValue = cache.values.get(columnLabel);
|
||||
} else {
|
||||
returnValue = safeGetFromDelegate(columnLabel);
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Object peek(String columnLabel) {
|
||||
|
||||
if (!isPeeking()) {
|
||||
createCache();
|
||||
}
|
||||
|
||||
if (!cache.next) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return safeGetFromDelegate(columnLabel);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Object safeGetFromDelegate(String columnLabel) {
|
||||
return accessor.getObject(columnLabel);
|
||||
}
|
||||
|
||||
private void createCache() {
|
||||
cache = new Cache();
|
||||
|
||||
try {
|
||||
int columnCount = resultSet.getMetaData().getColumnCount();
|
||||
for (int i = 1; i <= columnCount; i++) {
|
||||
// at least some databases return lower case labels although rs.getObject(UPPERCASE_LABEL) returns the expected
|
||||
// value. The aliases we use happen to be uppercase. So we transform everything to upper case.
|
||||
cache.add(resultSet.getMetaData().getColumnLabel(i).toLowerCase(),
|
||||
accessor.getObject(resultSet.getMetaData().getColumnLabel(i)));
|
||||
}
|
||||
|
||||
cache.next = resultSet.next();
|
||||
} catch (SQLException se) {
|
||||
throw new RuntimeException("Can't cache result set data", se);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean isPeeking() {
|
||||
return cache != null;
|
||||
}
|
||||
|
||||
private static class Cache {
|
||||
|
||||
boolean next;
|
||||
Map<String, Object> values = new HashMap<>();
|
||||
|
||||
void add(String columnName, Object value) {
|
||||
values.put(columnName, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,6 @@ import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.mapping.PersistentPropertyPath;
|
||||
import org.springframework.data.relational.core.conversion.IdValueSource;
|
||||
import org.springframework.data.relational.core.mapping.AggregatePath;
|
||||
import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
@@ -69,6 +68,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
private final NamedParameterJdbcOperations operations;
|
||||
private final SqlParametersFactory sqlParametersFactory;
|
||||
private final InsertStrategyFactory insertStrategyFactory;
|
||||
private final ReadingDataAccessStrategy singleSelectDelegate;
|
||||
|
||||
/**
|
||||
* Creates a {@link DefaultDataAccessStrategy}
|
||||
@@ -96,6 +96,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
this.operations = operations;
|
||||
this.sqlParametersFactory = sqlParametersFactory;
|
||||
this.insertStrategyFactory = insertStrategyFactory;
|
||||
this.singleSelectDelegate = new SingleQueryDataAccessStrategy(context, sqlGeneratorSource.getDialect(), converter, operations);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -260,6 +261,10 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
@Override
|
||||
public <T> T findById(Object id, Class<T> domainType) {
|
||||
|
||||
if (isSingleSelectQuerySupported(domainType)) {
|
||||
return singleSelectDelegate.findById(id, domainType);
|
||||
}
|
||||
|
||||
String findOneSql = sql(domainType).getFindOne();
|
||||
SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER);
|
||||
|
||||
@@ -272,6 +277,11 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
|
||||
@Override
|
||||
public <T> Iterable<T> findAll(Class<T> domainType) {
|
||||
|
||||
if (isSingleSelectQuerySupported(domainType)){
|
||||
return singleSelectDelegate.findAll(domainType);
|
||||
}
|
||||
|
||||
return operations.query(sql(domainType).getFindAll(), getEntityRowMapper(domainType));
|
||||
}
|
||||
|
||||
@@ -282,10 +292,12 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if (isSingleSelectQuerySupported(domainType)){
|
||||
return singleSelectDelegate.findAllById(ids, domainType);
|
||||
}
|
||||
|
||||
SqlParameterSource parameterSource = sqlParametersFactory.forQueryByIds(ids, domainType);
|
||||
|
||||
String findAllInListSql = sql(domainType).getFindAllInList();
|
||||
|
||||
return operations.query(findAllInListSql, parameterSource, getEntityRowMapper(domainType));
|
||||
}
|
||||
|
||||
@@ -430,4 +442,40 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
|
||||
return baseProperty.getOwner().getType();
|
||||
}
|
||||
|
||||
private boolean isSingleSelectQuerySupported(Class<?> entityType) {
|
||||
|
||||
return context.isSingleQueryLoadingEnabled() && sqlGeneratorSource.getDialect().supportsSingleQueryLoading()//
|
||||
&& entityQualifiesForSingleSelectQuery(entityType);
|
||||
}
|
||||
|
||||
private boolean entityQualifiesForSingleSelectQuery(Class<?> entityType) {
|
||||
|
||||
boolean referenceFound = false;
|
||||
for (PersistentPropertyPath<RelationalPersistentProperty> path : context.findPersistentPropertyPaths(entityType, __ -> true)) {
|
||||
RelationalPersistentProperty property = path.getLeafProperty();
|
||||
if (property.isEntity()) {
|
||||
|
||||
// embedded entities are currently not supported
|
||||
if (property.isEmbedded()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// only a single reference is currently supported
|
||||
if (referenceFound) {
|
||||
return false;
|
||||
}
|
||||
|
||||
referenceFound = true;
|
||||
}
|
||||
|
||||
// AggregateReferences aren't supported yet
|
||||
if (property.isAssociation()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2023 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.core.convert;
|
||||
|
||||
import org.springframework.data.mapping.PersistentPropertyPath;
|
||||
import org.springframework.data.relational.core.mapping.AggregatePath;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
|
||||
/**
|
||||
* A mapping between {@link PersistentPropertyPath} and column names of a query. Column names are intentionally
|
||||
* represented by {@link String} values, since this is what a {@link java.sql.ResultSet} uses, and since all the query
|
||||
* columns should be aliases there is no need for quoting or similar as provided by
|
||||
* {@link org.springframework.data.relational.core.sql.SqlIdentifier}.
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
public interface PathToColumnMapping {
|
||||
|
||||
String column(AggregatePath path);
|
||||
|
||||
String keyColumn(AggregatePath path);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2023 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.core.convert;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* The finding methods of a {@link DataAccessStrategy}.
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
interface ReadingDataAccessStrategy {
|
||||
/**
|
||||
* Loads a single entity identified by type and id.
|
||||
*
|
||||
* @param id the id of the entity to load. Must not be {@code null}.
|
||||
* @param domainType the domain type of the entity. Must not be {@code null}.
|
||||
* @param <T> the type of the entity.
|
||||
* @return Might return {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
<T> T findById(Object id, Class<T> domainType);
|
||||
|
||||
/**
|
||||
* Loads all entities of the given type.
|
||||
*
|
||||
* @param domainType the type of entities to load. Must not be {@code null}.
|
||||
* @param <T> the type of entities to load.
|
||||
* @return Guaranteed to be not {@code null}.
|
||||
*/
|
||||
<T> Iterable<T> findAll(Class<T> domainType);
|
||||
|
||||
/**
|
||||
* Loads all entities that match one of the ids passed as an argument. It is not guaranteed that the number of ids
|
||||
* passed in matches the number of entities returned.
|
||||
*
|
||||
* @param ids the Ids of the entities to load. Must not be {@code null}.
|
||||
* @param domainType the type of entities to load. Must not be {@code null}.
|
||||
* @param <T> type of entities to load.
|
||||
* @return the loaded entities. Guaranteed to be not {@code null}.
|
||||
*/
|
||||
<T> Iterable<T> findAllById(Iterable<?> ids, Class<T> domainType);
|
||||
|
||||
/**
|
||||
* Loads all entities of the given type, sorted.
|
||||
*
|
||||
* @param domainType the type of entities to load. Must not be {@code null}.
|
||||
* @param <T> the type of entities to load.
|
||||
* @param sort the sorting information. Must not be {@code null}.
|
||||
* @return Guaranteed to be not {@code null}.
|
||||
* @since 2.0
|
||||
*/
|
||||
<T> Iterable<T> findAll(Class<T> domainType, Sort sort);
|
||||
|
||||
/**
|
||||
* Loads all entities of the given type, paged and sorted.
|
||||
*
|
||||
* @param domainType the type of entities to load. Must not be {@code null}.
|
||||
* @param <T> the type of entities to load.
|
||||
* @param pageable the pagination information. Must not be {@code null}.
|
||||
* @return Guaranteed to be not {@code null}.
|
||||
* @since 2.0
|
||||
*/
|
||||
<T> Iterable<T> findAll(Class<T> 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 domainType 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
|
||||
*/
|
||||
<T> Optional<T> findOne(Query query, Class<T> domainType);
|
||||
|
||||
/**
|
||||
* Execute a {@code SELECT} query and convert the resulting items to a {@link Iterable}.
|
||||
*
|
||||
* @param query must not be {@literal null}.
|
||||
* @param domainType 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
|
||||
*/
|
||||
<T> Iterable<T> findAll(Query query, Class<T> domainType);
|
||||
|
||||
/**
|
||||
* 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 domainType 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.
|
||||
* @since 3.0
|
||||
*/
|
||||
<T> Iterable<T> findAll(Query query, Class<T> domainType, Pageable pageable);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2023 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.core.convert;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.relational.core.dialect.Dialect;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.data.relational.core.query.Query;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
|
||||
|
||||
/**
|
||||
* A {@link ReadingDataAccessStrategy} that uses an {@link AggregateReader} to load entities with a single query.
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
public class SingleQueryDataAccessStrategy implements ReadingDataAccessStrategy {
|
||||
private final AggregateReaderFactory readerFactory;
|
||||
private final RelationalMappingContext mappingContext;
|
||||
|
||||
public SingleQueryDataAccessStrategy(RelationalMappingContext mappingContext, Dialect dialect,
|
||||
JdbcConverter converter, NamedParameterJdbcOperations jdbcTemplate) {
|
||||
|
||||
this.mappingContext = mappingContext;
|
||||
this.readerFactory = new AggregateReaderFactory(mappingContext, dialect, converter, jdbcTemplate);
|
||||
;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T findById(Object id, Class<T> domainType) {
|
||||
return getReader(domainType).findById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Iterable<T> findAll(Class<T> domainType) {
|
||||
return getReader(domainType).findAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Iterable<T> findAllById(Iterable<?> ids, Class<T> domainType) {
|
||||
return getReader(domainType).findAllById(ids);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Iterable<T> findAll(Class<T> domainType, Sort sort) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Iterable<T> findAll(Class<T> domainType, Pageable pageable) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Optional<T> findOne(Query query, Class<T> domainType) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Iterable<T> findAll(Query query, Class<T> domainType) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Iterable<T> findAll(Query query, Class<T> domainType, Pageable pageable) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
private <T> AggregateReader<T> getReader(Class<T> domainType) {
|
||||
|
||||
RelationalPersistentEntity<T> persistentEntity = (RelationalPersistentEntity<T>) mappingContext
|
||||
.getRequiredPersistentEntity(domainType);
|
||||
return readerFactory.createAggregateReaderFor(persistentEntity);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import java.util.function.Function;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.assertj.core.api.SoftAssertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -90,13 +91,21 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
@Transactional
|
||||
@TestExecutionListeners(value = AssumeFeatureTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS)
|
||||
@ExtendWith(SpringExtension.class)
|
||||
class JdbcAggregateTemplateIntegrationTests {
|
||||
abstract class AbstractJdbcAggregateTemplateIntegrationTests {
|
||||
|
||||
@Autowired JdbcAggregateOperations template;
|
||||
@Autowired NamedParameterJdbcOperations jdbcTemplate;
|
||||
@Autowired RelationalMappingContext mappingContext;
|
||||
|
||||
LegoSet legoSet = createLegoSet("Star Destroyer");
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach(){
|
||||
mappingContext.setSingleQueryLoadingEnabled(useSingleQuery());
|
||||
}
|
||||
|
||||
abstract boolean useSingleQuery();
|
||||
|
||||
/**
|
||||
* creates an instance of {@link NoIdListChain4} with the following properties:
|
||||
* <ul>
|
||||
@@ -193,6 +202,42 @@ class JdbcAggregateTemplateIntegrationTests {
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void findById() {
|
||||
|
||||
WithInsertOnly entity = new WithInsertOnly();
|
||||
entity.insertOnly = "entity";
|
||||
entity = template.save(entity);
|
||||
|
||||
WithInsertOnly other = new WithInsertOnly();
|
||||
other.insertOnly = "other";
|
||||
other = template.save(other);
|
||||
|
||||
assertThat(template.findById(entity.id, WithInsertOnly.class).insertOnly).isEqualTo("entity");
|
||||
assertThat(template.findById(other.id, WithInsertOnly.class).insertOnly).isEqualTo("other");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void findAllById() {
|
||||
|
||||
WithInsertOnly entity = new WithInsertOnly();
|
||||
entity.insertOnly = "entity";
|
||||
entity = template.save(entity);
|
||||
|
||||
WithInsertOnly other = new WithInsertOnly();
|
||||
other.insertOnly = "other";
|
||||
other = template.save(other);
|
||||
|
||||
WithInsertOnly yetAnother = new WithInsertOnly();
|
||||
yetAnother.insertOnly = "yetAnother";
|
||||
yetAnother = template.save(yetAnother);
|
||||
|
||||
Iterable<WithInsertOnly> reloadedById = template.findAllById(asList(entity.id, yetAnother.id),
|
||||
WithInsertOnly.class);
|
||||
assertThat(reloadedById).extracting(e -> e.id, e -> e.insertOnly)
|
||||
.containsExactlyInAnyOrder(tuple(entity.id, "entity"), tuple(yetAnother.id, "yetAnother"));
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-112
|
||||
@EnabledOnFeature(SUPPORTS_QUOTED_IDS)
|
||||
void saveAndLoadAnEntityWithReferencedEntityById() {
|
||||
@@ -1833,4 +1878,17 @@ class JdbcAggregateTemplateIntegrationTests {
|
||||
return new JdbcAggregateTemplate(publisher, context, converter, dataAccessStrategy);
|
||||
}
|
||||
}
|
||||
|
||||
static class JdbcAggregateTemplateIntegrationTests extends AbstractJdbcAggregateTemplateIntegrationTests {
|
||||
@Override
|
||||
boolean useSingleQuery() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
static class JdbcAggregateTemplateSqlIntegrationTests extends AbstractJdbcAggregateTemplateIntegrationTests {
|
||||
@Override
|
||||
boolean useSingleQuery() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,718 @@
|
||||
/*
|
||||
* Copyright 2023 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.core.convert;
|
||||
|
||||
import static java.util.Arrays.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.data.jdbc.core.convert.AggregateResultSetExtractorUnitTests.ColumnType.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
|
||||
import org.springframework.data.mapping.PersistentPropertyPath;
|
||||
import org.springframework.data.relational.core.mapping.AggregatePath;
|
||||
import org.springframework.data.relational.core.mapping.DefaultNamingStrategy;
|
||||
import org.springframework.data.relational.core.mapping.Embedded;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
|
||||
/**
|
||||
* Unit tests for the {@link AggregateResultSetExtractor}.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
public class AggregateResultSetExtractorUnitTests {
|
||||
|
||||
RelationalMappingContext context = new JdbcMappingContext(new DefaultNamingStrategy());
|
||||
private final JdbcConverter converter = new BasicJdbcConverter(context, mock(RelationResolver.class));
|
||||
|
||||
private final PathToColumnMapping column = new PathToColumnMapping() {
|
||||
@Override
|
||||
public String column(AggregatePath path) {
|
||||
return AggregateResultSetExtractorUnitTests.this.column(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String keyColumn(AggregatePath path) {
|
||||
return column(path) + "_key";
|
||||
}
|
||||
};
|
||||
|
||||
AggregateResultSetExtractor<SimpleEntity> extractor = getExtractor(SimpleEntity.class);
|
||||
|
||||
@Test // GH-1446
|
||||
void emptyResultSetYieldsEmptyResult() throws SQLException {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList("T0_C0_ID1", "T0_C1_NAME"));
|
||||
assertThat(extractor.extractData(resultSet)).isEmpty();
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void singleSimpleEntityGetsExtractedFromSingleRow() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name")), //
|
||||
1, "Alfred");
|
||||
assertThat(extractor.extractData(resultSet)).extracting(e -> e.id1, e -> e.name)
|
||||
.containsExactly(tuple(1L, "Alfred"));
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void multipleSimpleEntitiesGetExtractedFromMultipleRows() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name")), //
|
||||
1, "Alfred", //
|
||||
2, "Bertram" //
|
||||
);
|
||||
assertThat(extractor.extractData(resultSet)).extracting(e -> e.id1, e -> e.name).containsExactly( //
|
||||
tuple(1L, "Alfred"), //
|
||||
tuple(2L, "Bertram") //
|
||||
);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Conversions {
|
||||
|
||||
@Test // GH-1446
|
||||
void appliesConversionToProperty() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name")), //
|
||||
new BigDecimal(1), "Alfred");
|
||||
assertThat(extractor.extractData(resultSet)).extracting(e -> e.id1, e -> e.name)
|
||||
.containsExactly(tuple(1L, "Alfred"));
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void appliesConversionToConstructorValue() {
|
||||
|
||||
AggregateResultSetExtractor<DummyRecord> extractor = getExtractor(DummyRecord.class);
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name")), //
|
||||
new BigDecimal(1), "Alfred");
|
||||
assertThat(extractor.extractData(resultSet)).extracting(e -> e.id1, e -> e.name)
|
||||
.containsExactly(tuple(1L, "Alfred"));
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void appliesConversionToKeyValue() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
|
||||
asList(column("id1"), column("dummyList", KEY), column("dummyList.dummyName")), //
|
||||
1, new BigDecimal(0), "Dummy Alfred", //
|
||||
1, new BigDecimal(1), "Dummy Berta", //
|
||||
1, new BigDecimal(2), "Dummy Carl");
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
|
||||
assertThat(result.iterator().next().dummyList).extracting(d -> d.dummyName) //
|
||||
.containsExactly("Dummy Alfred", "Dummy Berta", "Dummy Carl");
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private <T> AggregateResultSetExtractor<T> getExtractor(Class<T> type) {
|
||||
return (AggregateResultSetExtractor<T>) new AggregateResultSetExtractor<>(context,
|
||||
(RelationalPersistentEntity<DummyRecord>) context.getPersistentEntity(type), converter, column);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class EmbeddedReference {
|
||||
@Test // GH-1446
|
||||
void embeddedGetsExtractedFromSingleRow() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("embeddedNullable.dummyName")), //
|
||||
1, "Imani");
|
||||
|
||||
assertThat(extractor.extractData(resultSet)).extracting(e -> e.id1, e -> e.embeddedNullable.dummyName)
|
||||
.containsExactly(tuple(1L, "Imani"));
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void nullEmbeddedGetsExtractedFromSingleRow() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("embeddedNullable.dummyName")), //
|
||||
1, null);
|
||||
|
||||
assertThat(extractor.extractData(resultSet)).extracting(e -> e.id1, e -> e.embeddedNullable)
|
||||
.containsExactly(tuple(1L, null));
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void emptyEmbeddedGetsExtractedFromSingleRow() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("embeddedNonNull.dummyName")), //
|
||||
1, null);
|
||||
|
||||
assertThat(extractor.extractData(resultSet)) //
|
||||
.extracting(e -> e.id1, e -> e.embeddedNonNull.dummyName) //
|
||||
.containsExactly(tuple(1L, null));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ToOneRelationships {
|
||||
@Test // GH-1446
|
||||
void entityReferenceGetsExtractedFromSingleRow() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
|
||||
asList(column("id1"), column("dummy"), column("dummy.dummyName")), //
|
||||
1, 1, "Dummy Alfred");
|
||||
|
||||
assertThat(extractor.extractData(resultSet)) //
|
||||
.extracting(e -> e.id1, e -> e.dummy.dummyName) //
|
||||
.containsExactly(tuple(1L, "Dummy Alfred"));
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void nullEntityReferenceGetsExtractedFromSingleRow() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
|
||||
asList(column("id1"), column("dummy"), column("dummy.dummyName")), //
|
||||
1, null, "Dummy Alfred");
|
||||
|
||||
assertThat(extractor.extractData(resultSet)).extracting(e -> e.id1, e -> e.dummy)
|
||||
.containsExactly(tuple(1L, null));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Sets {
|
||||
|
||||
@Test // GH-1446
|
||||
void extractEmptySetReference() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
|
||||
asList(column("id1"), column("dummies"), column("dummies.dummyName")), //
|
||||
1, null, null, //
|
||||
1, null, null, //
|
||||
1, null, null);
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
|
||||
assertThat(result.iterator().next().dummies).isEmpty();
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractSingleSetReference() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
|
||||
asList(column("id1"), column("dummies"), column("dummies.dummyName")), //
|
||||
1, 1, "Dummy Alfred", //
|
||||
1, 1, "Dummy Berta", //
|
||||
1, 1, "Dummy Carl");
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
|
||||
assertThat(result.iterator().next().dummies).extracting(d -> d.dummyName) //
|
||||
.containsExactlyInAnyOrder("Dummy Alfred", "Dummy Berta", "Dummy Carl");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractSetReferenceAndSimpleProperty() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
|
||||
asList(column("id1"), column("name"), column("dummies"), column("dummies.dummyName")), //
|
||||
1, "Simplicissimus", 1, "Dummy Alfred", //
|
||||
1, null, 1, "Dummy Berta", //
|
||||
1, null, 1, "Dummy Carl");
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1, e -> e.name).containsExactly(tuple(1L, "Simplicissimus"));
|
||||
assertThat(result.iterator().next().dummies).extracting(d -> d.dummyName) //
|
||||
.containsExactlyInAnyOrder("Dummy Alfred", "Dummy Berta", "Dummy Carl");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractMultipleSetReference() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), //
|
||||
column("dummies"), column("dummies.dummyName"), //
|
||||
column("otherDummies"), column("otherDummies.dummyName")), //
|
||||
1, 1, "Dummy Alfred", 1, "Other Ephraim", //
|
||||
1, 1, "Dummy Berta", 1, "Other Zeno", //
|
||||
1, 1, "Dummy Carl", null, null);
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
|
||||
assertThat(result.iterator().next().dummies).extracting(d -> d.dummyName) //
|
||||
.containsExactlyInAnyOrder("Dummy Alfred", "Dummy Berta", "Dummy Carl");
|
||||
assertThat(result.iterator().next().otherDummies).extracting(d -> d.dummyName) //
|
||||
.containsExactlyInAnyOrder("Other Ephraim", "Other Zeno");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractNestedSetsWithId() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name"), //
|
||||
column("intermediates"), column("intermediates.iId"), column("intermediates.intermediateName"), //
|
||||
column("intermediates.dummies"), column("intermediates.dummies.dummyName")), //
|
||||
1, "Alfred", 1, 23, "Inami", 23, "Dustin", //
|
||||
1, null, 1, 23, null, 23, "Dora", //
|
||||
1, null, 1, 24, "Ina", 24, "Dotty", //
|
||||
1, null, 1, 25, "Ion", null, null, //
|
||||
2, "Bon Jovi", 2, 26, "Judith", 26, "Ephraim", //
|
||||
2, null, 2, 26, null, 26, "Erin", //
|
||||
2, null, 2, 27, "Joel", 27, "Erika", //
|
||||
2, null, 2, 28, "Justin", null, null //
|
||||
);
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1, e -> e.name, e -> e.intermediates.size())
|
||||
.containsExactlyInAnyOrder(tuple(1L, "Alfred", 3), tuple(2L, "Bon Jovi", 3));
|
||||
|
||||
assertThat(result).extracting(e -> e.id1, e -> e.name, e -> e.intermediates.size())
|
||||
.containsExactlyInAnyOrder(tuple(1L, "Alfred", 3), tuple(2L, "Bon Jovi", 3));
|
||||
|
||||
final Iterator<SimpleEntity> iter = result.iterator();
|
||||
SimpleEntity alfred = iter.next();
|
||||
assertThat(alfred).extracting("id1", "name").containsExactly(1L, "Alfred");
|
||||
assertThat(alfred.intermediates).extracting(d -> d.intermediateName).containsExactlyInAnyOrder("Inami", "Ina",
|
||||
"Ion");
|
||||
|
||||
assertThat(alfred.findInIntermediates("Inami").dummies).extracting(d -> d.dummyName)
|
||||
.containsExactlyInAnyOrder("Dustin", "Dora");
|
||||
assertThat(alfred.findInIntermediates("Ina").dummies).extracting(d -> d.dummyName)
|
||||
.containsExactlyInAnyOrder("Dotty");
|
||||
assertThat(alfred.findInIntermediates("Ion").dummies).isEmpty();
|
||||
|
||||
SimpleEntity bonJovy = iter.next();
|
||||
assertThat(bonJovy).extracting("id1", "name").containsExactly(2L, "Bon Jovi");
|
||||
assertThat(bonJovy.intermediates).extracting(d -> d.intermediateName).containsExactlyInAnyOrder("Judith", "Joel",
|
||||
"Justin");
|
||||
assertThat(bonJovy.findInIntermediates("Judith").dummies).extracting(d -> d.dummyName)
|
||||
.containsExactlyInAnyOrder("Ephraim", "Erin");
|
||||
assertThat(bonJovy.findInIntermediates("Joel").dummies).extracting(d -> d.dummyName).containsExactly("Erika");
|
||||
assertThat(bonJovy.findInIntermediates("Justin").dummyList).isEmpty();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Lists {
|
||||
|
||||
@Test // GH-1446
|
||||
void extractSingleListReference() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
|
||||
asList(column("id1"), column("dummyList", KEY), column("dummyList.dummyName")), //
|
||||
1, 0, "Dummy Alfred", //
|
||||
1, 1, "Dummy Berta", //
|
||||
1, 2, "Dummy Carl");
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
|
||||
assertThat(result.iterator().next().dummyList).extracting(d -> d.dummyName) //
|
||||
.containsExactly("Dummy Alfred", "Dummy Berta", "Dummy Carl");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractSingleUnorderedListReference() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
|
||||
asList(column("id1"), column("dummyList", KEY), column("dummyList.dummyName")), //
|
||||
1, 0, "Dummy Alfred", //
|
||||
1, 2, "Dummy Carl", 1, 1, "Dummy Berta" //
|
||||
);
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
|
||||
assertThat(result.iterator().next().dummyList).extracting(d -> d.dummyName) //
|
||||
.containsExactly("Dummy Alfred", "Dummy Berta", "Dummy Carl");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractListReferenceAndSimpleProperty() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
|
||||
asList(column("id1"), column("name"), column("dummyList", KEY), column("dummyList.dummyName")), //
|
||||
1, "Simplicissimus", 0, "Dummy Alfred", //
|
||||
1, null, 1, "Dummy Berta", //
|
||||
1, null, 2, "Dummy Carl");
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1, e -> e.name).containsExactly(tuple(1L, "Simplicissimus"));
|
||||
assertThat(result.iterator().next().dummyList).extracting(d -> d.dummyName) //
|
||||
.containsExactly("Dummy Alfred", "Dummy Berta", "Dummy Carl");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractMultipleCollectionReference() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), //
|
||||
column("dummyList", KEY), column("dummyList.dummyName"), //
|
||||
column("otherDummies"), column("otherDummies.dummyName")), //
|
||||
1, 0, "Dummy Alfred", 1, "Other Ephraim", //
|
||||
1, 1, "Dummy Berta", 1, "Other Zeno", //
|
||||
1, 2, "Dummy Carl", null, null);
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
|
||||
assertThat(result.iterator().next().dummyList).extracting(d -> d.dummyName) //
|
||||
.containsExactly("Dummy Alfred", "Dummy Berta", "Dummy Carl");
|
||||
assertThat(result.iterator().next().otherDummies).extracting(d -> d.dummyName) //
|
||||
.containsExactlyInAnyOrder("Other Ephraim", "Other Zeno");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractNestedListsWithId() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name"), //
|
||||
column("intermediateList", KEY), column("intermediateList.iId"), column("intermediateList.intermediateName"), //
|
||||
column("intermediateList.dummyList", KEY), column("intermediateList.dummyList.dummyName")), //
|
||||
1, "Alfred", 0, 23, "Inami", 0, "Dustin", //
|
||||
1, null, 0, 23, null, 1, "Dora", //
|
||||
1, null, 1, 24, "Ina", 0, "Dotty", //
|
||||
1, null, 2, 25, "Ion", null, null, //
|
||||
2, "Bon Jovi", 0, 26, "Judith", 0, "Ephraim", //
|
||||
2, null, 0, 26, null, 1, "Erin", //
|
||||
2, null, 1, 27, "Joel", 0, "Erika", //
|
||||
2, null, 2, 28, "Justin", null, null //
|
||||
);
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1, e -> e.name, e -> e.intermediateList.size())
|
||||
.containsExactlyInAnyOrder(tuple(1L, "Alfred", 3), tuple(2L, "Bon Jovi", 3));
|
||||
|
||||
final Iterator<SimpleEntity> iter = result.iterator();
|
||||
SimpleEntity alfred = iter.next();
|
||||
assertThat(alfred).extracting("id1", "name").containsExactly(1L, "Alfred");
|
||||
assertThat(alfred.intermediateList).extracting(d -> d.intermediateName).containsExactly("Inami", "Ina", "Ion");
|
||||
|
||||
assertThat(alfred.findInIntermediateList("Inami").dummyList).extracting(d -> d.dummyName)
|
||||
.containsExactly("Dustin", "Dora");
|
||||
assertThat(alfred.findInIntermediateList("Ina").dummyList).extracting(d -> d.dummyName).containsExactly("Dotty");
|
||||
assertThat(alfred.findInIntermediateList("Ion").dummyList).isEmpty();
|
||||
|
||||
SimpleEntity bonJovy = iter.next();
|
||||
assertThat(bonJovy).extracting("id1", "name").containsExactly(2L, "Bon Jovi");
|
||||
assertThat(bonJovy.intermediateList).extracting(d -> d.intermediateName).containsExactly("Judith", "Joel",
|
||||
"Justin");
|
||||
assertThat(bonJovy.findInIntermediateList("Judith").dummyList).extracting(d -> d.dummyName)
|
||||
.containsExactly("Ephraim", "Erin");
|
||||
assertThat(bonJovy.findInIntermediateList("Joel").dummyList).extracting(d -> d.dummyName)
|
||||
.containsExactly("Erika");
|
||||
assertThat(bonJovy.findInIntermediateList("Justin").dummyList).isEmpty();
|
||||
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractNestedListsWithOutId() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name"), //
|
||||
column("intermediateListNoId", KEY), column("intermediateListNoId.intermediateName"), //
|
||||
column("intermediateListNoId.dummyList", KEY), column("intermediateListNoId.dummyList.dummyName")), //
|
||||
1, "Alfred", 0, "Inami", 0, "Dustin", //
|
||||
1, null, 0, null, 1, "Dora", //
|
||||
1, null, 1, "Ina", 0, "Dotty", //
|
||||
1, null, 2, "Ion", null, null, //
|
||||
2, "Bon Jovi", 0, "Judith", 0, "Ephraim", //
|
||||
2, null, 0, null, 1, "Erin", //
|
||||
2, null, 1, "Joel", 0, "Erika", //
|
||||
2, null, 2, "Justin", null, null //
|
||||
);
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1, e -> e.name, e -> e.intermediateListNoId.size())
|
||||
.containsExactlyInAnyOrder(tuple(1L, "Alfred", 3), tuple(2L, "Bon Jovi", 3));
|
||||
|
||||
final Iterator<SimpleEntity> iter = result.iterator();
|
||||
SimpleEntity alfred = iter.next();
|
||||
assertThat(alfred).extracting("id1", "name").containsExactly(1L, "Alfred");
|
||||
assertThat(alfred.intermediateListNoId).extracting(d -> d.intermediateName).containsExactly("Inami", "Ina",
|
||||
"Ion");
|
||||
|
||||
assertThat(alfred.findInIntermediateListNoId("Inami").dummyList).extracting(d -> d.dummyName)
|
||||
.containsExactly("Dustin", "Dora");
|
||||
assertThat(alfred.findInIntermediateListNoId("Ina").dummyList).extracting(d -> d.dummyName)
|
||||
.containsExactly("Dotty");
|
||||
assertThat(alfred.findInIntermediateListNoId("Ion").dummyList).isEmpty();
|
||||
|
||||
SimpleEntity bonJovy = iter.next();
|
||||
assertThat(bonJovy).extracting("id1", "name").containsExactly(2L, "Bon Jovi");
|
||||
assertThat(bonJovy.intermediateListNoId).extracting(d -> d.intermediateName).containsExactly("Judith", "Joel",
|
||||
"Justin");
|
||||
|
||||
assertThat(bonJovy.findInIntermediateListNoId("Judith").dummyList).extracting(d -> d.dummyName)
|
||||
.containsExactly("Ephraim", "Erin");
|
||||
assertThat(bonJovy.findInIntermediateListNoId("Joel").dummyList).extracting(d -> d.dummyName)
|
||||
.containsExactly("Erika");
|
||||
assertThat(bonJovy.findInIntermediateListNoId("Justin").dummyList).isEmpty();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Maps {
|
||||
|
||||
@Test // GH-1446
|
||||
void extractSingleMapReference() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
|
||||
asList(column("id1"), column("dummyMap", KEY), column("dummyMap.dummyName")), //
|
||||
1, "alpha", "Dummy Alfred", //
|
||||
1, "beta", "Dummy Berta", //
|
||||
1, "gamma", "Dummy Carl");
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
|
||||
Map<String, DummyEntity> dummyMap = result.iterator().next().dummyMap;
|
||||
assertThat(dummyMap).extracting("alpha").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Alfred");
|
||||
assertThat(dummyMap).extracting("beta").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Berta");
|
||||
assertThat(dummyMap).extracting("gamma").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Carl");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractMapReferenceAndSimpleProperty() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
|
||||
asList(column("id1"), column("name"), column("dummyMap", KEY), column("dummyMap.dummyName")), //
|
||||
1, "Simplicissimus", "alpha", "Dummy Alfred", //
|
||||
1, null, "beta", "Dummy Berta", //
|
||||
1, null, "gamma", "Dummy Carl");
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1, e -> e.name).containsExactly(tuple(1L, "Simplicissimus"));
|
||||
Map<String, DummyEntity> dummyMap = result.iterator().next().dummyMap;
|
||||
assertThat(dummyMap).extracting("alpha").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Alfred");
|
||||
assertThat(dummyMap).extracting("beta").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Berta");
|
||||
assertThat(dummyMap).extracting("gamma").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Carl");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractMultipleCollectionReference() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), //
|
||||
column("dummyMap", KEY), column("dummyMap.dummyName"), //
|
||||
column("otherDummies"), column("otherDummies.dummyName")), //
|
||||
1, "alpha", "Dummy Alfred", 1, "Other Ephraim", //
|
||||
1, "beta", "Dummy Berta", 1, "Other Zeno", //
|
||||
1, "gamma", "Dummy Carl", null, null);
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
|
||||
Map<String, DummyEntity> dummyMap = result.iterator().next().dummyMap;
|
||||
assertThat(dummyMap).extracting("alpha").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Alfred");
|
||||
assertThat(dummyMap).extracting("beta").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Berta");
|
||||
assertThat(dummyMap).extracting("gamma").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Carl");
|
||||
|
||||
assertThat(result.iterator().next().otherDummies).extracting(d -> d.dummyName) //
|
||||
.containsExactlyInAnyOrder("Other Ephraim", "Other Zeno");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractNestedMapsWithId() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name"), //
|
||||
column("intermediateMap", KEY), column("intermediateMap.iId"), column("intermediateMap.intermediateName"), //
|
||||
column("intermediateMap.dummyMap", KEY), column("intermediateMap.dummyMap.dummyName")), //
|
||||
1, "Alfred", "alpha", 23, "Inami", "omega", "Dustin", //
|
||||
1, null, "alpha", 23, null, "zeta", "Dora", //
|
||||
1, null, "beta", 24, "Ina", "eta", "Dotty", //
|
||||
1, null, "gamma", 25, "Ion", null, null, //
|
||||
2, "Bon Jovi", "phi", 26, "Judith", "theta", "Ephraim", //
|
||||
2, null, "phi", 26, null, "jota", "Erin", //
|
||||
2, null, "chi", 27, "Joel", "sigma", "Erika", //
|
||||
2, null, "psi", 28, "Justin", null, null //
|
||||
);
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1, e -> e.name, e -> e.intermediateMap.size())
|
||||
.containsExactlyInAnyOrder(tuple(1L, "Alfred", 3), tuple(2L, "Bon Jovi", 3));
|
||||
|
||||
final Iterator<SimpleEntity> iter = result.iterator();
|
||||
SimpleEntity alfred = iter.next();
|
||||
assertThat(alfred).extracting("id1", "name").containsExactly(1L, "Alfred");
|
||||
|
||||
assertThat(alfred.intermediateMap.get("alpha").dummyMap.get("omega").dummyName).isEqualTo("Dustin");
|
||||
assertThat(alfred.intermediateMap.get("alpha").dummyMap.get("zeta").dummyName).isEqualTo("Dora");
|
||||
assertThat(alfred.intermediateMap.get("beta").dummyMap.get("eta").dummyName).isEqualTo("Dotty");
|
||||
assertThat(alfred.intermediateMap.get("gamma").dummyMap).isEmpty();
|
||||
|
||||
SimpleEntity bonJovy = iter.next();
|
||||
|
||||
assertThat(bonJovy.intermediateMap.get("phi").dummyMap.get("theta").dummyName).isEqualTo("Ephraim");
|
||||
assertThat(bonJovy.intermediateMap.get("phi").dummyMap.get("jota").dummyName).isEqualTo("Erin");
|
||||
assertThat(bonJovy.intermediateMap.get("chi").dummyMap.get("sigma").dummyName).isEqualTo("Erika");
|
||||
assertThat(bonJovy.intermediateMap.get("psi").dummyMap).isEmpty();
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractNestedMapsWithOutId() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name"), //
|
||||
column("intermediateMapNoId", KEY), column("intermediateMapNoId.intermediateName"), //
|
||||
column("intermediateMapNoId.dummyMap", KEY), column("intermediateMapNoId.dummyMap.dummyName")), //
|
||||
1, "Alfred", "alpha", "Inami", "omega", "Dustin", //
|
||||
1, null, "alpha", null, "zeta", "Dora", //
|
||||
1, null, "beta", "Ina", "eta", "Dotty", //
|
||||
1, null, "gamma", "Ion", null, null, //
|
||||
2, "Bon Jovi", "phi", "Judith", "theta", "Ephraim", //
|
||||
2, null, "phi", null, "jota", "Erin", //
|
||||
2, null, "chi", "Joel", "sigma", "Erika", //
|
||||
2, null, "psi", "Justin", null, null //
|
||||
);
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1, e -> e.name, e -> e.intermediateMapNoId.size())
|
||||
.containsExactlyInAnyOrder(tuple(1L, "Alfred", 3), tuple(2L, "Bon Jovi", 3));
|
||||
|
||||
final Iterator<SimpleEntity> iter = result.iterator();
|
||||
SimpleEntity alfred = iter.next();
|
||||
assertThat(alfred).extracting("id1", "name").containsExactly(1L, "Alfred");
|
||||
|
||||
assertThat(alfred.intermediateMapNoId.get("alpha").dummyMap.get("omega").dummyName).isEqualTo("Dustin");
|
||||
assertThat(alfred.intermediateMapNoId.get("alpha").dummyMap.get("zeta").dummyName).isEqualTo("Dora");
|
||||
assertThat(alfred.intermediateMapNoId.get("beta").dummyMap.get("eta").dummyName).isEqualTo("Dotty");
|
||||
assertThat(alfred.intermediateMapNoId.get("gamma").dummyMap).isEmpty();
|
||||
|
||||
SimpleEntity bonJovy = iter.next();
|
||||
|
||||
assertThat(bonJovy.intermediateMapNoId.get("phi").dummyMap.get("theta").dummyName).isEqualTo("Ephraim");
|
||||
assertThat(bonJovy.intermediateMapNoId.get("phi").dummyMap.get("jota").dummyName).isEqualTo("Erin");
|
||||
assertThat(bonJovy.intermediateMapNoId.get("chi").dummyMap.get("sigma").dummyName).isEqualTo("Erika");
|
||||
assertThat(bonJovy.intermediateMapNoId.get("psi").dummyMap).isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private String column(String path) {
|
||||
return column(path, NORMAL);
|
||||
}
|
||||
|
||||
private String column(String path, ColumnType columnType) {
|
||||
|
||||
PersistentPropertyPath<RelationalPersistentProperty> propertyPath = context.getPersistentPropertyPath(path,
|
||||
SimpleEntity.class);
|
||||
|
||||
return column(context.getAggregatePath(propertyPath)) + (columnType == KEY ? "_key" : "");
|
||||
}
|
||||
|
||||
private String column(AggregatePath path) {
|
||||
return path.toDotPath();
|
||||
}
|
||||
|
||||
enum ColumnType {
|
||||
NORMAL, KEY
|
||||
}
|
||||
|
||||
private static class SimpleEntity {
|
||||
|
||||
@Id long id1;
|
||||
String name;
|
||||
DummyEntity dummy;
|
||||
@Embedded.Nullable DummyEntity embeddedNullable;
|
||||
@Embedded.Empty DummyEntity embeddedNonNull;
|
||||
|
||||
Set<Intermediate> intermediates;
|
||||
|
||||
Set<DummyEntity> dummies;
|
||||
Set<DummyEntity> otherDummies;
|
||||
|
||||
List<DummyEntity> dummyList;
|
||||
List<Intermediate> intermediateList;
|
||||
List<IntermediateNoId> intermediateListNoId;
|
||||
|
||||
Map<String, DummyEntity> dummyMap;
|
||||
Map<String, Intermediate> intermediateMap;
|
||||
Map<String, IntermediateNoId> intermediateMapNoId;
|
||||
|
||||
Intermediate findInIntermediates(String name) {
|
||||
for (Intermediate intermediate : intermediates) {
|
||||
if (intermediate.intermediateName.equals(name)) {
|
||||
return intermediate;
|
||||
}
|
||||
}
|
||||
fail("No intermediate with name " + name + " found in intermediates.");
|
||||
return null;
|
||||
}
|
||||
|
||||
Intermediate findInIntermediateList(String name) {
|
||||
for (Intermediate intermediate : intermediateList) {
|
||||
if (intermediate.intermediateName.equals(name)) {
|
||||
return intermediate;
|
||||
}
|
||||
}
|
||||
fail("No intermediate with name " + name + " found in intermediateList.");
|
||||
return null;
|
||||
}
|
||||
|
||||
IntermediateNoId findInIntermediateListNoId(String name) {
|
||||
for (IntermediateNoId intermediate : intermediateListNoId) {
|
||||
if (intermediate.intermediateName.equals(name)) {
|
||||
return intermediate;
|
||||
}
|
||||
}
|
||||
fail("No intermediates with name " + name + " found in intermediateListNoId.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Intermediate {
|
||||
|
||||
@Id long iId;
|
||||
String intermediateName;
|
||||
|
||||
Set<DummyEntity> dummies;
|
||||
List<DummyEntity> dummyList;
|
||||
Map<String, DummyEntity> dummyMap;
|
||||
}
|
||||
|
||||
private static class IntermediateNoId {
|
||||
|
||||
String intermediateName;
|
||||
|
||||
Set<DummyEntity> dummies;
|
||||
List<DummyEntity> dummyList;
|
||||
Map<String, DummyEntity> dummyMap;
|
||||
}
|
||||
|
||||
private static class DummyEntity {
|
||||
String dummyName;
|
||||
Long longValue;
|
||||
}
|
||||
|
||||
private record DummyRecord(Long id1, String name) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
* Copyright 2023 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.core.convert;
|
||||
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedCaseInsensitiveMap;
|
||||
|
||||
import javax.naming.OperationNotSupportedException;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.ResultSetMetaData;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Utility for mocking ResultSets for tests.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class ResultSetTestUtil {
|
||||
|
||||
static ResultSet mockResultSet(List<String> columns, Object... values) {
|
||||
|
||||
Assert.isTrue( //
|
||||
values.length % columns.size() == 0, //
|
||||
String //
|
||||
.format( //
|
||||
"Number of values [%d] must be a multiple of the number of columns [%d]", //
|
||||
values.length, //
|
||||
columns.size() //
|
||||
) //
|
||||
);
|
||||
|
||||
List<Map<String, Object>> result = convertValues(columns, values);
|
||||
|
||||
return mock(ResultSet.class, new ResultSetAnswer(columns, result));
|
||||
}
|
||||
|
||||
|
||||
private static List<Map<String, Object>> convertValues(List<String> columns, Object[] values) {
|
||||
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
|
||||
int index = 0;
|
||||
while (index < values.length) {
|
||||
|
||||
Map<String, Object> row = new LinkedCaseInsensitiveMap<>();
|
||||
result.add(row);
|
||||
for (String column : columns) {
|
||||
|
||||
row.put(column, values[index]);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static class ResultSetAnswer implements Answer<Object> {
|
||||
|
||||
private final List<String> names;
|
||||
private final List<Map<String, Object>> values;
|
||||
private int index = -1;
|
||||
|
||||
ResultSetAnswer(List<String> names, List<Map<String, Object>> values) {
|
||||
|
||||
this.names = names;
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
|
||||
switch (invocation.getMethod().getName()) {
|
||||
case "next" -> {
|
||||
return next();
|
||||
}
|
||||
case "getObject" -> {
|
||||
Object argument = invocation.getArgument(0);
|
||||
String name = argument instanceof Integer ? names.get(((Integer) argument) - 1) : (String) argument;
|
||||
return getObject(name);
|
||||
}
|
||||
case "isAfterLast" -> {
|
||||
return isAfterLast();
|
||||
}
|
||||
case "isBeforeFirst" -> {
|
||||
return isBeforeFirst();
|
||||
}
|
||||
case "getRow" -> {
|
||||
return isAfterLast() || isBeforeFirst() ? 0 : index + 1;
|
||||
}
|
||||
case "toString" -> {
|
||||
return this.toString();
|
||||
}
|
||||
case "findColumn" -> {
|
||||
return isThereAColumnNamed(invocation.getArgument(0));
|
||||
}
|
||||
case "getMetaData" -> {
|
||||
return new MockedMetaData();
|
||||
}
|
||||
default -> throw new OperationNotSupportedException(invocation.getMethod().getName());
|
||||
}
|
||||
}
|
||||
|
||||
private int isThereAColumnNamed(String name) {
|
||||
throw new UnsupportedOperationException("duh");
|
||||
// Optional<Map<String, Object>> first = values.stream().filter(s -> s.equals(name)).findFirst();
|
||||
// return (first.isPresent()) ? 1 : 0;
|
||||
}
|
||||
|
||||
private boolean isAfterLast() {
|
||||
return index >= values.size() && !values.isEmpty();
|
||||
}
|
||||
|
||||
private boolean isBeforeFirst() {
|
||||
return index < 0 && !values.isEmpty();
|
||||
}
|
||||
|
||||
private Object getObject(String column) throws SQLException {
|
||||
|
||||
Map<String, Object> rowMap = values.get(index);
|
||||
|
||||
if (!rowMap.containsKey(column)) {
|
||||
throw new SQLException(String.format("Trying to access a column (%s) that does not exist", column));
|
||||
}
|
||||
|
||||
return rowMap.get(column);
|
||||
}
|
||||
|
||||
private boolean next() {
|
||||
|
||||
index++;
|
||||
return index < values.size();
|
||||
}
|
||||
|
||||
private class MockedMetaData implements ResultSetMetaData {
|
||||
@Override
|
||||
public int getColumnCount() {
|
||||
return names.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAutoIncrement(int i) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCaseSensitive(int i) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSearchable(int i) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCurrency(int i) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int isNullable(int i) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSigned(int i) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnDisplaySize(int i) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnLabel(int i) {
|
||||
return names.get(i - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnName(int i) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSchemaName(int i) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPrecision(int i) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScale(int i) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTableName(int i) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCatalogName(int i) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnType(int i) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnTypeName(int i) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadOnly(int i) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWritable(int i) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDefinitelyWritable(int i) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnClassName(int i) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T unwrap(Class<T> aClass) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWrapperFor(Class<?> aClass) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -160,8 +160,7 @@ public class JdbcRepositoryCustomConversionIntegrationTests {
|
||||
repository.saveAll(asList(entityA, entityB, entityC));
|
||||
|
||||
assertThat(repository.findByEnumTypeIn(Set.of(Direction.LEFT, Direction.RIGHT)))
|
||||
.extracting(entity -> entity.direction)
|
||||
.containsExactlyInAnyOrder(Direction.LEFT, Direction.RIGHT);
|
||||
.extracting(entity -> entity.direction).containsExactlyInAnyOrder(Direction.LEFT, Direction.RIGHT);
|
||||
}
|
||||
|
||||
@Test // GH-1212
|
||||
@@ -175,8 +174,7 @@ public class JdbcRepositoryCustomConversionIntegrationTests {
|
||||
entityC.direction = Direction.RIGHT;
|
||||
repository.saveAll(asList(entityA, entityB, entityC));
|
||||
|
||||
assertThat(repository.findByEnumTypeIn(Set.of(Direction.CENTER)))
|
||||
.extracting(entity -> entity.direction)
|
||||
assertThat(repository.findByEnumTypeIn(Set.of(Direction.CENTER))).extracting(entity -> entity.direction)
|
||||
.containsExactly(Direction.CENTER);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,13 +19,11 @@ import javax.sql.DataSource;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
|
||||
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
|
||||
import org.testcontainers.containers.MSSQLServerContainer;
|
||||
|
||||
import com.microsoft.sqlserver.jdbc.SQLServerDataSource;
|
||||
|
||||
|
||||
/**
|
||||
* {@link DataSource} setup for PostgreSQL.
|
||||
* <p>
|
||||
@@ -36,14 +34,14 @@ import com.microsoft.sqlserver.jdbc.SQLServerDataSource;
|
||||
* @see <a href="https://github.com/testcontainers/testcontainers-java/tree/master/modules/mssqlserver"></a>
|
||||
*/
|
||||
@Configuration
|
||||
@Profile({"mssql"})
|
||||
@Profile({ "mssql" })
|
||||
public class MsSqlDataSourceConfiguration extends DataSourceConfiguration {
|
||||
|
||||
public static final String MS_SQL_SERVER_VERSION = "mcr.microsoft.com/mssql/server:2019-CU16-ubuntu-20.04";
|
||||
public static final String MS_SQL_SERVER_VERSION = "mcr.microsoft.com/mssql/server:2022-CU5-ubuntu-20.04";
|
||||
private static MSSQLServerContainer<?> MSSQL_CONTAINER;
|
||||
|
||||
@Override
|
||||
protected DataSource createDataSource() {
|
||||
@Override
|
||||
protected DataSource createDataSource() {
|
||||
|
||||
if (MSSQL_CONTAINER == null) {
|
||||
|
||||
@@ -54,14 +52,13 @@ public class MsSqlDataSourceConfiguration extends DataSourceConfiguration {
|
||||
MSSQL_CONTAINER = container;
|
||||
}
|
||||
|
||||
SQLServerDataSource sqlServerDataSource = new SQLServerDataSource();
|
||||
SQLServerDataSource sqlServerDataSource = new SQLServerDataSource();
|
||||
sqlServerDataSource.setURL(MSSQL_CONTAINER.getJdbcUrl());
|
||||
sqlServerDataSource.setUser(MSSQL_CONTAINER.getUsername());
|
||||
sqlServerDataSource.setPassword(MSSQL_CONTAINER.getPassword());
|
||||
|
||||
return sqlServerDataSource;
|
||||
}
|
||||
|
||||
return sqlServerDataSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void customizePopulator(ResourceDatabasePopulator populator) {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
DROP TABLE DUMMY_ENTITY;
|
||||
DROP TABLE DUMMY_ENTITY CASCADE CONSTRAINTS;
|
||||
|
||||
CREATE TABLE DUMMY_ENTITY ( id NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY);
|
||||
@@ -1,3 +1,3 @@
|
||||
DROP TABLE DUMMY_ENTITY;
|
||||
DROP TABLE DUMMY_ENTITY CASCADE CONSTRAINTS;
|
||||
|
||||
CREATE TABLE DUMMY_ENTITY ( id NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY);
|
||||
@@ -97,6 +97,12 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.jsqlparser</groupId>
|
||||
<artifactId>jsqlparser</artifactId>
|
||||
<version>4.6</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -116,9 +116,8 @@ public interface Dialect {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return an appropriate {@link InsertRenderContext} for that specific dialect.
|
||||
* for most of the Dialects the default implementation will be valid, but, for
|
||||
* example, in case of {@link SqlServerDialect} it is not.
|
||||
* @return an appropriate {@link InsertRenderContext} for that specific dialect. for most of the Dialects the default
|
||||
* implementation will be valid, but, for example, in case of {@link SqlServerDialect} it is not.
|
||||
* @since 2.4
|
||||
*/
|
||||
default InsertRenderContext getInsertRenderContext() {
|
||||
@@ -136,12 +135,16 @@ public interface Dialect {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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(){
|
||||
default SimpleFunction getExistsFunction() {
|
||||
return Functions.count(SQL.literalOf(1));
|
||||
}
|
||||
|
||||
default boolean supportsSingleQueryLoading() {
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -108,4 +108,9 @@ public class H2Dialect extends AbstractDialect {
|
||||
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSingleQueryLoading() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,11 @@ public class HsqlDbDialect extends AbstractDialect {
|
||||
return AnsiDialect.LOCK_CLAUSE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSingleQueryLoading() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static final LimitClause LIMIT_CLAUSE = new LimitClause() {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.relational.core.dialect;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
@@ -34,6 +35,8 @@ public class MariaDbDialect extends MySqlDialect {
|
||||
|
||||
@Override
|
||||
public Collection<Object> getConverters() {
|
||||
return Collections.singletonList(TimestampAtUtcToOffsetDateTimeConverter.INSTANCE);
|
||||
return Arrays.asList(
|
||||
TimestampAtUtcToOffsetDateTimeConverter.INSTANCE,
|
||||
NumberToBooleanConverter.INSTANCE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.data.relational.core.dialect;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
@@ -131,7 +133,10 @@ public class MySqlDialect extends AbstractDialect {
|
||||
|
||||
@Override
|
||||
public Collection<Object> getConverters() {
|
||||
return Collections.singletonList(TimestampAtUtcToOffsetDateTimeConverter.INSTANCE);
|
||||
return Arrays.asList(
|
||||
TimestampAtUtcToOffsetDateTimeConverter.INSTANCE,
|
||||
NumberToBooleanConverter.INSTANCE
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2023 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.relational.core.dialect;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.convert.ReadingConverter;
|
||||
|
||||
/**
|
||||
* A {@link ReadingConverter} to convert from {@link Number} to {@link Boolean}. 0 is considered {@literal false}
|
||||
* everything else is considered {@literal true}.
|
||||
*/
|
||||
@ReadingConverter
|
||||
enum NumberToBooleanConverter implements Converter<Number, Boolean> {
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public Boolean convert(Number number) {
|
||||
return number.intValue() != 0;
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,9 @@
|
||||
package org.springframework.data.relational.core.dialect;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.convert.ReadingConverter;
|
||||
import org.springframework.data.convert.WritingConverter;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import static java.util.Arrays.*;
|
||||
|
||||
@@ -56,15 +54,6 @@ public class OracleDialect extends AnsiDialect {
|
||||
return asList(TimestampAtUtcToOffsetDateTimeConverter.INSTANCE, NumberToBooleanConverter.INSTANCE, BooleanToIntegerConverter.INSTANCE);
|
||||
}
|
||||
|
||||
@ReadingConverter
|
||||
enum NumberToBooleanConverter implements Converter<Number, Boolean> {
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public Boolean convert(Number number) {
|
||||
return number.intValue() != 0;
|
||||
}
|
||||
}
|
||||
@WritingConverter
|
||||
enum BooleanToIntegerConverter implements Converter<Boolean, Integer> {
|
||||
INSTANCE;
|
||||
|
||||
@@ -21,7 +21,7 @@ import java.util.function.Predicate;
|
||||
/**
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
class AggregatePathTraversal {
|
||||
public class AggregatePathTraversal {
|
||||
|
||||
public static AggregatePath getIdDefiningPath(AggregatePath aggregatePath) {
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ public class RelationalMappingContext
|
||||
private boolean forceQuote = true;
|
||||
|
||||
private final ExpressionEvaluator expressionEvaluator = new ExpressionEvaluator(EvaluationContextProvider.DEFAULT);
|
||||
private boolean singleQueryLoadingEnabled = false;
|
||||
|
||||
/**
|
||||
* Creates a new {@link RelationalMappingContext}.
|
||||
@@ -130,6 +131,27 @@ public class RelationalMappingContext
|
||||
return persistentProperty;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.2
|
||||
* @return iff single query loading is enabled.
|
||||
* @see #setSingleQueryLoadingEnabled(boolean)
|
||||
*/
|
||||
public boolean isSingleQueryLoadingEnabled() {
|
||||
return singleQueryLoadingEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@literal singleQueryLoadingEnabled} flag. If it is set to true and the
|
||||
* {@link org.springframework.data.relational.core.dialect.Dialect} supports it, Spring Data JDBC will try to use
|
||||
* Single Query Loading if possible.
|
||||
*
|
||||
* @since 3.2
|
||||
* @param singleQueryLoadingEnabled
|
||||
*/
|
||||
public void setSingleQueryLoadingEnabled(boolean singleQueryLoadingEnabled) {
|
||||
this.singleQueryLoadingEnabled = singleQueryLoadingEnabled;
|
||||
}
|
||||
|
||||
protected void applyDefaults(BasicRelationalPersistentProperty persistentProperty) {
|
||||
|
||||
persistentProperty.setForceQuote(isForceQuote());
|
||||
|
||||
@@ -21,7 +21,7 @@ package org.springframework.data.relational.core.sql;
|
||||
* @author Jens Schauder
|
||||
* @since 1.1
|
||||
*/
|
||||
class AliasedExpression extends AbstractSegment implements Aliased, Expression {
|
||||
public class AliasedExpression extends AbstractSegment implements Aliased, Expression {
|
||||
|
||||
private final Expression expression;
|
||||
private final SqlIdentifier alias;
|
||||
@@ -49,6 +49,6 @@ class AliasedExpression extends AbstractSegment implements Aliased, Expression {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return expression + " AS " + alias;
|
||||
return expression.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,9 +136,9 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultSelectBuilder orderBy(Column... columns) {
|
||||
public DefaultSelectBuilder orderBy(Expression... columns) {
|
||||
|
||||
for (Column column : columns) {
|
||||
for (Expression column : columns) {
|
||||
this.orderBy.add(OrderByField.from(column));
|
||||
}
|
||||
|
||||
@@ -299,7 +299,7 @@ class DefaultSelectBuilder implements SelectBuilder, SelectAndFrom, SelectFromAn
|
||||
}
|
||||
|
||||
@Override
|
||||
public SelectOrdered orderBy(Column... columns) {
|
||||
public SelectOrdered orderBy(Expression... columns) {
|
||||
selectBuilder.join(finishJoin());
|
||||
return selectBuilder.orderBy(columns);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -49,10 +50,26 @@ public class Functions {
|
||||
}
|
||||
|
||||
public static SimpleFunction least(Expression... expressions) {
|
||||
|
||||
return SimpleFunction.create("LEAST", Arrays.asList(expressions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@literal GREATEST} function with the given arguments.
|
||||
* @since 3.2
|
||||
*/
|
||||
public static SimpleFunction greatest(Expression... expressions) {
|
||||
return greatest(Arrays.asList(expressions));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a {@literal GREATEST} function with the given arguments.
|
||||
* @since 3.2
|
||||
*/
|
||||
public static SimpleFunction greatest(List<Expression> list) {
|
||||
return SimpleFunction.create("GREATEST", list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@code COUNT} function.
|
||||
*
|
||||
@@ -96,4 +113,8 @@ public class Functions {
|
||||
|
||||
// Utility constructor.
|
||||
private Functions() {}
|
||||
|
||||
public static SimpleFunction coalesce(Expression... expressions) {
|
||||
return SimpleFunction.create("COALESCE", Arrays.asList(expressions));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ public interface SelectBuilder {
|
||||
SelectFromAndOrderBy from(Collection<? extends TableLike> tables);
|
||||
|
||||
@Override
|
||||
SelectFromAndOrderBy orderBy(Column... columns);
|
||||
SelectFromAndOrderBy orderBy(Expression... columns);
|
||||
|
||||
@Override
|
||||
SelectFromAndOrderBy orderBy(OrderByField... orderByFields);
|
||||
@@ -393,7 +393,7 @@ public interface SelectBuilder {
|
||||
* @param columns the columns to order by.
|
||||
* @return {@code this} builder.
|
||||
*/
|
||||
SelectOrdered orderBy(Column... columns);
|
||||
SelectOrdered orderBy(Expression... columns);
|
||||
|
||||
/**
|
||||
* Add one or more {@link OrderByField order by fields}.
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2023 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.relational.core.sqlgeneration;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.data.relational.core.mapping.AggregatePath;
|
||||
import org.springframework.data.relational.core.mapping.AggregatePathTraversal;
|
||||
|
||||
/**
|
||||
* Creates aliases to be used in SQL generation
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
public class AliasFactory {
|
||||
private final SingleAliasFactory columnAliases = new SingleAliasFactory("c");
|
||||
private final SingleAliasFactory tableAliases = new SingleAliasFactory("t");
|
||||
private final SingleAliasFactory rowNumberAliases = new SingleAliasFactory("rn");
|
||||
private final SingleAliasFactory rowCountAliases = new SingleAliasFactory("rc");
|
||||
private final SingleAliasFactory backReferenceAliases = new SingleAliasFactory("br");
|
||||
private final SingleAliasFactory keyAliases = new SingleAliasFactory("key");
|
||||
private int counter = 0;
|
||||
|
||||
private static String sanitize(String name) {
|
||||
return name.replaceAll("\\W", "");
|
||||
}
|
||||
|
||||
public String getColumnAlias(AggregatePath path) {
|
||||
return columnAliases.getOrCreateFor(path);
|
||||
}
|
||||
|
||||
public String getTableAlias(AggregatePath path) {
|
||||
return tableAliases.getOrCreateFor(path);
|
||||
}
|
||||
|
||||
public String getRowNumberAlias(AggregatePath path) {
|
||||
return rowNumberAliases.getOrCreateFor(AggregatePathTraversal.getTableOwningPath(path));
|
||||
}
|
||||
|
||||
public String getRowCountAlias(AggregatePath path) {
|
||||
return rowCountAliases.getOrCreateFor(path);
|
||||
}
|
||||
|
||||
public String getBackReferenceAlias(AggregatePath path) {
|
||||
return backReferenceAliases.getOrCreateFor(path);
|
||||
}
|
||||
|
||||
public String getKeyAlias(AggregatePath path) {
|
||||
return keyAliases.getOrCreateFor(path);
|
||||
}
|
||||
|
||||
private class SingleAliasFactory {
|
||||
private final String prefix;
|
||||
private final Map<AggregatePath, String> cache = new ConcurrentHashMap<>();
|
||||
|
||||
SingleAliasFactory(String prefix) {
|
||||
this.prefix = prefix + "_";
|
||||
}
|
||||
|
||||
String getOrCreateFor(AggregatePath path) {
|
||||
return cache.computeIfAbsent(path, this::createName);
|
||||
}
|
||||
|
||||
private String createName(AggregatePath path) {
|
||||
return prefix + getName(path) + "_" + ++counter;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getName(AggregatePath path) {
|
||||
return sanitize( //
|
||||
path.isEntity() //
|
||||
? path.getTableInfo().qualifiedTableName().getReference() //
|
||||
: path.getColumnInfo().name().getReference()) //
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2023 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.relational.core.sqlgeneration;
|
||||
|
||||
import org.springframework.data.util.Lazy;
|
||||
|
||||
/**
|
||||
* A wrapper for the {@link SqlGenerator} that caches the generated statements.
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
public class CachingSqlGenerator implements SqlGenerator{
|
||||
|
||||
private final SqlGenerator delegate;
|
||||
|
||||
private final Lazy<String> findAll;
|
||||
private final Lazy<String> findById;
|
||||
private final Lazy<String> findAllById;
|
||||
|
||||
public CachingSqlGenerator(SqlGenerator delegate) {
|
||||
|
||||
this.delegate = delegate;
|
||||
|
||||
findAll = Lazy.of(delegate.findAll());
|
||||
findById = Lazy.of(delegate.findById());
|
||||
findAllById = Lazy.of(delegate.findAllById());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String findAll() {
|
||||
return findAll.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String findById() {
|
||||
return findById.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String findAllById() {
|
||||
return findAllById.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AliasFactory getAliasFactory() {
|
||||
return delegate.getAliasFactory();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
/*
|
||||
* Copyright 2023 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.relational.core.sqlgeneration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mapping.PersistentPropertyPath;
|
||||
import org.springframework.data.mapping.PersistentPropertyPaths;
|
||||
import org.springframework.data.relational.core.dialect.Dialect;
|
||||
import org.springframework.data.relational.core.dialect.RenderContextFactory;
|
||||
import org.springframework.data.relational.core.mapping.AggregatePath;
|
||||
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.sql.*;
|
||||
import org.springframework.data.relational.core.sql.render.SqlRenderer;
|
||||
|
||||
/**
|
||||
* A {@link SqlGenerator} that creates SQL statements for loading complete aggregates with a single statement.
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
public class SingleQuerySqlGenerator implements SqlGenerator {
|
||||
|
||||
private final RelationalMappingContext context;
|
||||
private final Dialect dialect;
|
||||
private final AliasFactory aliases = new AliasFactory();
|
||||
|
||||
private final RelationalPersistentEntity<?> aggregate;
|
||||
private final Table table;
|
||||
|
||||
public SingleQuerySqlGenerator(RelationalMappingContext context, Dialect dialect,
|
||||
RelationalPersistentEntity<?> aggregate) {
|
||||
|
||||
this.context = context;
|
||||
this.dialect = dialect;
|
||||
this.aggregate = aggregate;
|
||||
|
||||
this.table = Table.create(aggregate.getQualifiedTableName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String findAll() {
|
||||
return createSelect(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String findById() {
|
||||
|
||||
AggregatePath path = getRootIdPath();
|
||||
Condition condition = Conditions.isEqual(table.column(path.getColumnInfo().name()), Expressions.just(":id"));
|
||||
|
||||
return createSelect(condition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String findAllById() {
|
||||
|
||||
AggregatePath path = getRootIdPath();
|
||||
Condition condition = Conditions.in(table.column(path.getColumnInfo().name()), Expressions.just(":ids"));
|
||||
|
||||
return createSelect(condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The {@link AggregatePath} to the id property of the aggregate root.
|
||||
*/
|
||||
private AggregatePath getRootIdPath() {
|
||||
return context.getAggregatePath(aggregate).append(aggregate.getRequiredIdProperty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a SQL suitable of loading all the data required for constructing complete aggregates.
|
||||
*
|
||||
* @param condition a constraint for limiting the aggregates to be loaded.
|
||||
* @return a {@literal String} containing the generated SQL statement
|
||||
*/
|
||||
private String createSelect(Condition condition) {
|
||||
|
||||
AggregatePath rootPath = context.getAggregatePath(aggregate);
|
||||
QueryMeta queryMeta = createInlineQuery(rootPath, condition);
|
||||
InlineQuery rootQuery = queryMeta.inlineQuery;
|
||||
List<Expression> columns = new ArrayList<>(queryMeta.selectableExpressions);
|
||||
|
||||
List<Expression> rownumbers = new ArrayList<>();
|
||||
rownumbers.add(queryMeta.rowNumber);
|
||||
PersistentPropertyPaths<?, RelationalPersistentProperty> entityPaths = context
|
||||
.findPersistentPropertyPaths(aggregate.getType(), PersistentProperty::isEntity);
|
||||
List<QueryMeta> inlineQueries = createInlineQueries(entityPaths);
|
||||
inlineQueries.forEach(qm -> {
|
||||
columns.addAll(qm.selectableExpressions);
|
||||
rownumbers.add(qm.rowNumber);
|
||||
});
|
||||
|
||||
Expression totalRownumber = rownumbers.size() > 1 ? greatest(rownumbers).as("rn")
|
||||
: new AliasedExpression(rownumbers.get(0), "rn");
|
||||
columns.add(totalRownumber);
|
||||
|
||||
InlineQuery inlineQuery = createMainSelect(columns, rootPath, rootQuery, inlineQueries);
|
||||
|
||||
Expression rootIdExpression = just(aliases.getColumnAlias(rootPath.append(aggregate.getRequiredIdProperty())));
|
||||
|
||||
List<Expression> finalColumns = new ArrayList<>();
|
||||
queryMeta.simpleColumns
|
||||
.forEach(e -> finalColumns.add(filteredColumnExpression(queryMeta.rowNumber.toString(), e.toString())));
|
||||
|
||||
for (QueryMeta meta : inlineQueries) {
|
||||
meta.simpleColumns
|
||||
.forEach(e -> finalColumns.add(filteredColumnExpression(meta.rowNumber.toString(), e.toString())));
|
||||
if (meta.id != null) {
|
||||
finalColumns.add(meta.id);
|
||||
}
|
||||
if (meta.key != null) {
|
||||
finalColumns.add(meta.key);
|
||||
}
|
||||
}
|
||||
|
||||
finalColumns.add(rootIdExpression);
|
||||
|
||||
Select fullQuery = StatementBuilder.select(finalColumns).from(inlineQuery).orderBy(rootIdExpression, just("rn"))
|
||||
.build();
|
||||
|
||||
return SqlRenderer.create(new RenderContextFactory(dialect).createRenderContext()).render(fullQuery);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private InlineQuery createMainSelect(List<Expression> columns, AggregatePath rootPath, InlineQuery rootQuery,
|
||||
List<QueryMeta> inlineQueries) {
|
||||
|
||||
SelectBuilder.SelectJoin select = StatementBuilder.select(columns).from(rootQuery);
|
||||
|
||||
select = applyJoins(rootPath, inlineQueries, select);
|
||||
|
||||
SelectBuilder.BuildSelect buildSelect = applyWhereCondition(rootPath, inlineQueries, select);
|
||||
Select mainSelect = buildSelect.build();
|
||||
|
||||
return InlineQuery.create(mainSelect, "main");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates inline queries for all entities referenced by the paths passed as an argument.
|
||||
*
|
||||
* @param paths the paths to consider.
|
||||
* @return a {@link Map} that contains all the inline queries indexed by the path to the entity that gets loaded by
|
||||
* the subquery.
|
||||
*/
|
||||
private List<QueryMeta> createInlineQueries(PersistentPropertyPaths<?, RelationalPersistentProperty> paths) {
|
||||
|
||||
List<QueryMeta> inlineQueries = new ArrayList<>();
|
||||
|
||||
for (PersistentPropertyPath ppp : paths) {
|
||||
|
||||
QueryMeta queryMeta = createInlineQuery(context.getAggregatePath(ppp), null);
|
||||
inlineQueries.add(queryMeta);
|
||||
}
|
||||
return inlineQueries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a single inline query for the given basePath. The query selects all the columns for the entity plus a
|
||||
* rownumber and a rowcount expression. The first numbers all rows of the subselect sequentially starting from 1. The
|
||||
* rowcount contains the total number of child rows. All selected expressions are globally uniquely aliased and are
|
||||
* referenced by that alias in the rest of the query. This ensures that we don't run into problems with column names
|
||||
* that are not unique across tables and also the generated SQL doesn't contain quotes and funny column names, making
|
||||
* them easier to understand and also potentially shorter.
|
||||
*
|
||||
* @param basePath the path for which to create the inline query.
|
||||
* @param condition a condition that is to be applied to the query. May be {@literal null}.
|
||||
* @return an inline query for the given path.
|
||||
*/
|
||||
private QueryMeta createInlineQuery(AggregatePath basePath, Condition condition) {
|
||||
|
||||
RelationalPersistentEntity<?> entity = basePath.getRequiredLeafEntity();
|
||||
Table table = Table.create(entity.getQualifiedTableName());
|
||||
|
||||
List<AggregatePath> paths = new ArrayList<>();
|
||||
|
||||
entity.doWithProperties((RelationalPersistentProperty p) -> {
|
||||
if (!p.isEntity()) {
|
||||
paths.add(basePath.append(p));
|
||||
}
|
||||
});
|
||||
|
||||
List<Expression> columns = new ArrayList<>();
|
||||
List<Expression> columnAliases = new ArrayList<>();
|
||||
|
||||
String rowNumberAlias = aliases.getRowNumberAlias(basePath);
|
||||
Expression rownumber = basePath.isRoot() ? new AliasedExpression(SQL.literalOf(1), rowNumberAlias)
|
||||
: createRowNumberExpression(basePath, table, rowNumberAlias);
|
||||
columns.add(rownumber);
|
||||
|
||||
String rowCountAlias = aliases.getRowCountAlias(basePath);
|
||||
Expression count = basePath.isRoot() ? new AliasedExpression(SQL.literalOf(1), rowCountAlias)
|
||||
: AnalyticFunction.create("count", Expressions.just("*"))
|
||||
.partitionBy(table.column(basePath.getTableInfo().reverseColumnInfo().name())).as(rowCountAlias);
|
||||
columns.add(count);
|
||||
String backReferenceAlias = null;
|
||||
String keyAlias = null;
|
||||
if (!basePath.isRoot()) {
|
||||
|
||||
backReferenceAlias = aliases.getBackReferenceAlias(basePath);
|
||||
columns.add(table.column(basePath.getTableInfo().reverseColumnInfo().name()).as(backReferenceAlias));
|
||||
|
||||
if (basePath.isQualified()) {
|
||||
|
||||
keyAlias = aliases.getKeyAlias(basePath);
|
||||
columns.add(table.column(basePath.getTableInfo().qualifierColumnInfo().name()).as(keyAlias));
|
||||
} else {
|
||||
|
||||
String alias = aliases.getColumnAlias(basePath);
|
||||
columns.add(new AliasedExpression(just("1"), alias));
|
||||
columnAliases.add(just(alias));
|
||||
}
|
||||
}
|
||||
String id = null;
|
||||
|
||||
for (AggregatePath path : paths) {
|
||||
|
||||
String alias = aliases.getColumnAlias(path);
|
||||
if (path.getRequiredLeafProperty().isIdProperty()) {
|
||||
id = alias;
|
||||
} else {
|
||||
columnAliases.add(just(alias));
|
||||
}
|
||||
columns.add(table.column(path.getColumnInfo().name()).as(alias));
|
||||
}
|
||||
|
||||
SelectBuilder.SelectWhere select = StatementBuilder.select(columns).from(table);
|
||||
|
||||
SelectBuilder.BuildSelect buildSelect = condition != null ? select.where(condition) : select;
|
||||
|
||||
InlineQuery inlineQuery = InlineQuery.create(buildSelect.build(),
|
||||
aliases.getTableAlias(context.getAggregatePath(entity)));
|
||||
return QueryMeta.of(basePath, inlineQuery, columnAliases, just(id), just(backReferenceAlias), just(keyAlias),
|
||||
just(rowNumberAlias), just(rowCountAlias));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static AnalyticFunction createRowNumberExpression(AggregatePath basePath, Table table,
|
||||
String rowNumberAlias) {
|
||||
return AnalyticFunction.create("row_number") //
|
||||
.partitionBy(table.column(basePath.getTableInfo().reverseColumnInfo().name())) //
|
||||
.orderBy(table.column(basePath.getTableInfo().reverseColumnInfo().name())) //
|
||||
.as(rowNumberAlias);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds joins to a select.
|
||||
*
|
||||
* @param rootPath the AggregatePath that gets selected by the select in question.
|
||||
* @param inlineQueries all the inline queries to added as joins as returned by
|
||||
* {@link #createInlineQueries(PersistentPropertyPaths)}
|
||||
* @param select the select to modify.
|
||||
* @return the original select but with added joins
|
||||
*/
|
||||
private SelectBuilder.SelectJoin applyJoins(AggregatePath rootPath, List<QueryMeta> inlineQueries,
|
||||
SelectBuilder.SelectJoin select) {
|
||||
|
||||
RelationalPersistentProperty rootIdProperty = rootPath.getRequiredIdProperty();
|
||||
AggregatePath rootIdPath = rootPath.append(rootIdProperty);
|
||||
for (QueryMeta queryMeta : inlineQueries) {
|
||||
|
||||
AggregatePath path = queryMeta.basePath();
|
||||
String backReferenceAlias = aliases.getBackReferenceAlias(path);
|
||||
Comparison joinCondition = Conditions.isEqual(Expressions.just(aliases.getColumnAlias(rootIdPath)),
|
||||
Expressions.just(backReferenceAlias));
|
||||
select = select.leftOuterJoin(queryMeta.inlineQuery).on(joinCondition);
|
||||
}
|
||||
return select;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a where condition to the select. The Where condition is constructed such that one root and multiple child
|
||||
* selects are combined such that.
|
||||
* <ol>
|
||||
* <li>all child elements with a given rn become part of a single row. I.e. all child rows with for example rownumber
|
||||
* 3 are contained in a single row</li>
|
||||
* <li>if for a given rownumber no matching element is present for a given child the columns for that child are either
|
||||
* null (when there is no child elements at all) or the values for rownumber 1 are used for that child</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param rootPath path to the root entity that gets selected.
|
||||
* @param inlineQueries all in the inline queries for all the children, as returned by
|
||||
* {@link #createInlineQueries(PersistentPropertyPaths)}
|
||||
* @param select the select to which the where clause gets added.
|
||||
* @return the modified select.
|
||||
*/
|
||||
private SelectBuilder.SelectOrdered applyWhereCondition(AggregatePath rootPath, List<QueryMeta> inlineQueries,
|
||||
SelectBuilder.SelectJoin select) {
|
||||
|
||||
SelectBuilder.SelectWhereAndOr selectWhere = null;
|
||||
for (QueryMeta queryMeta : inlineQueries) {
|
||||
|
||||
AggregatePath path = queryMeta.basePath;
|
||||
Expression childRowNumber = just(aliases.getRowNumberAlias(path));
|
||||
Condition pseudoJoinCondition = Conditions.isNull(childRowNumber)
|
||||
.or(Conditions.isEqual(childRowNumber, Expressions.just(aliases.getRowNumberAlias(rootPath))))
|
||||
.or(Conditions.isGreater(childRowNumber, Expressions.just(aliases.getRowCountAlias(rootPath))));
|
||||
|
||||
selectWhere = ((SelectBuilder.SelectWhere) select).where(pseudoJoinCondition);
|
||||
}
|
||||
|
||||
return selectWhere == null ? (SelectBuilder.SelectOrdered) select : selectWhere;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AliasFactory getAliasFactory() {
|
||||
return aliases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a SQL function of the following form
|
||||
* {@code GREATEST(Coalesce(x1, 1), Coalesce(x2, 1), ..., Coalesce(xN, 1)}. this is used for cobining rownumbers from
|
||||
* different child tables. The {@code coalesce} is used because the values {@code x1 ... xN} might be {@code null} and
|
||||
* we want {@code null} to be equivalent with the first entry.
|
||||
*
|
||||
* @param expressions the different values to combined.
|
||||
*/
|
||||
private static SimpleFunction greatest(List<Expression> expressions) {
|
||||
|
||||
List<Expression> guarded = new ArrayList<>();
|
||||
for (Expression expression : expressions) {
|
||||
guarded.add(Functions.coalesce(expression, SQL.literalOf(1)));
|
||||
}
|
||||
return Functions.greatest(guarded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs SQL of the form {@code CASE WHEN x = rn THEN alias ELSE NULL END AS ALIAS}. This expression is used to
|
||||
* replace values that would appear multiple times in the result with {@code null} values in all but the first
|
||||
* occurrence. With out this the result for an aggregate root with a single collection item would look like this:
|
||||
* <table>
|
||||
* <th>
|
||||
* <td>root value</td>
|
||||
* <td>child value</td></th>
|
||||
* <tr>
|
||||
* <td>root1</td>
|
||||
* <td>child1</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>root1</td>
|
||||
* <td>child2</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>root1</td>
|
||||
* <td>child3</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>root1</td>
|
||||
* <td>child4</td>
|
||||
* </tr>
|
||||
* </table>
|
||||
* This expression transforms this into
|
||||
* <table>
|
||||
* <th>
|
||||
* <td>root value</td>
|
||||
* <td>child value</td></th>
|
||||
* <tr>
|
||||
* <td>root1</td>
|
||||
* <td>child1</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>null</td>
|
||||
* <td>child2</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>null</td>
|
||||
* <td>child3</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>null</td>
|
||||
* <td>child4</td>
|
||||
* </tr>
|
||||
* </table>
|
||||
*
|
||||
* @param rowNumberAlias the alias of the rownumber column of the subselect under consideration. This determines if
|
||||
* the other value is replaced by null or not.
|
||||
* @param alias the column potentially to be replace by null
|
||||
* @return a SQL expression.
|
||||
*/
|
||||
private static Expression filteredColumnExpression(String rowNumberAlias, String alias) {
|
||||
return just("case when " + rowNumberAlias + " = rn THEN " + alias + " else null end as " + alias);
|
||||
}
|
||||
|
||||
private static Expression just(String alias) {
|
||||
if (alias == null) {
|
||||
return null;
|
||||
}
|
||||
return Expressions.just(alias);
|
||||
}
|
||||
|
||||
record QueryMeta(AggregatePath basePath, InlineQuery inlineQuery, Collection<Expression> simpleColumns,
|
||||
Collection<Expression> selectableExpressions, Expression id, Expression backReference, Expression key,
|
||||
Expression rowNumber, Expression rowCount) {
|
||||
|
||||
static QueryMeta of(AggregatePath basePath, InlineQuery inlineQuery, Collection<Expression> simpleColumns,
|
||||
Expression id, Expression backReference, Expression key, Expression rowNumber, Expression rowCount) {
|
||||
|
||||
List<Expression> selectableExpressions = new ArrayList<>(simpleColumns);
|
||||
selectableExpressions.add(rowNumber);
|
||||
|
||||
if (id != null) {
|
||||
selectableExpressions.add(id);
|
||||
}
|
||||
if (backReference != null) {
|
||||
selectableExpressions.add(backReference);
|
||||
}
|
||||
if (key != null) {
|
||||
selectableExpressions.add(key);
|
||||
}
|
||||
|
||||
return new QueryMeta(basePath, inlineQuery, simpleColumns, selectableExpressions, id, backReference, key,
|
||||
rowNumber, rowCount);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2023 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.relational.core.sqlgeneration;
|
||||
|
||||
/**
|
||||
* Generates SQL statements for loading aggregates.
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
public interface SqlGenerator {
|
||||
String findAll();
|
||||
|
||||
String findById();
|
||||
|
||||
String findAllById();
|
||||
|
||||
AliasFactory getAliasFactory();
|
||||
}
|
||||
@@ -620,6 +620,21 @@ class SelectRendererUnitTests {
|
||||
.isEqualTo("SELECT * FROM tableA JOIN tableB ON tableA.id = tableB.id ORDER BY tableA.name, tableB.name");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void rendersAliasedExpression() {
|
||||
|
||||
Table table = SQL.table("table");
|
||||
Column tableName = table.column("name");
|
||||
|
||||
Select select = StatementBuilder.select(new AliasedExpression(tableName, "alias")) //
|
||||
.from(table) //
|
||||
.build();
|
||||
|
||||
String rendered = SqlRenderer.toString(select);
|
||||
assertThat(rendered)
|
||||
.isEqualTo("SELECT table.name AS alias FROM table");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the rendering of analytic functions.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright 2023 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.relational.core.sqlgeneration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
|
||||
/**
|
||||
* Unit tests for the {@link AliasFactory}.
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class AliasFactoryUnitTests {
|
||||
|
||||
RelationalMappingContext context = new RelationalMappingContext();
|
||||
AliasFactory aliasFactory = new AliasFactory();
|
||||
|
||||
@Nested
|
||||
class SimpleAlias {
|
||||
@Test
|
||||
void aliasForRoot() {
|
||||
|
||||
String alias = aliasFactory
|
||||
.getColumnAlias(context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class)));
|
||||
|
||||
assertThat(alias).isEqualTo("c_dummy_entity_1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void aliasSimpleProperty() {
|
||||
|
||||
String alias = aliasFactory
|
||||
.getColumnAlias(context.getAggregatePath(context.getPersistentPropertyPath("name", DummyEntity.class)));
|
||||
|
||||
assertThat(alias).isEqualTo("c_name_1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nameGetsSanatized() {
|
||||
|
||||
String alias = aliasFactory.getColumnAlias(
|
||||
context.getAggregatePath( context.getPersistentPropertyPath("evil", DummyEntity.class)));
|
||||
|
||||
assertThat(alias).isEqualTo("c_ameannamecontains3illegal_characters_1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void aliasIsStable() {
|
||||
|
||||
String alias1 = aliasFactory.getColumnAlias(
|
||||
context.getAggregatePath( context.getRequiredPersistentEntity(DummyEntity.class)));
|
||||
String alias2 = aliasFactory.getColumnAlias(
|
||||
context.getAggregatePath( context.getRequiredPersistentEntity(DummyEntity.class)));
|
||||
|
||||
assertThat(alias1).isEqualTo(alias2);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class RnAlias {
|
||||
|
||||
@Test
|
||||
void aliasIsStable() {
|
||||
|
||||
String alias1 = aliasFactory.getRowNumberAlias(
|
||||
context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class)));
|
||||
String alias2 = aliasFactory.getRowNumberAlias(
|
||||
context.getAggregatePath( context.getRequiredPersistentEntity(DummyEntity.class)));
|
||||
|
||||
assertThat(alias1).isEqualTo(alias2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void aliasProjectsOnTableReferencingPath() {
|
||||
|
||||
String alias1 = aliasFactory.getRowNumberAlias(
|
||||
context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class)));
|
||||
|
||||
String alias2 = aliasFactory.getRowNumberAlias(
|
||||
context.getAggregatePath(context.getPersistentPropertyPath("evil", DummyEntity.class)));
|
||||
|
||||
assertThat(alias1).isEqualTo(alias2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rnAliasIsIndependentOfTableAlias() {
|
||||
|
||||
String alias1 = aliasFactory.getRowNumberAlias(
|
||||
context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class)));
|
||||
String alias2 = aliasFactory.getColumnAlias(
|
||||
context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class)));
|
||||
|
||||
assertThat(alias1).isNotEqualTo(alias2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class BackReferenceAlias {
|
||||
@Test
|
||||
void testBackReferenceAlias() {
|
||||
|
||||
String alias = aliasFactory.getBackReferenceAlias(
|
||||
context.getAggregatePath(context.getPersistentPropertyPath("dummy", Reference.class)));
|
||||
|
||||
assertThat(alias).isEqualTo("br_dummy_entity_1");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class KeyAlias {
|
||||
@Test
|
||||
void testKeyAlias() {
|
||||
|
||||
String alias = aliasFactory.getKeyAlias(
|
||||
context.getAggregatePath(context.getPersistentPropertyPath("dummy", Reference.class)));
|
||||
|
||||
assertThat(alias).isEqualTo("key_dummy_entity_1");
|
||||
}
|
||||
}
|
||||
|
||||
static class DummyEntity {
|
||||
String name;
|
||||
|
||||
@Column("a mean name <-- contains > 3 illegal_characters.") String evil;
|
||||
}
|
||||
|
||||
static class Reference {
|
||||
DummyEntity dummy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2023 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.relational.core.sqlgeneration;
|
||||
|
||||
import net.sf.jsqlparser.statement.select.SelectExpressionItem;
|
||||
import net.sf.jsqlparser.statement.select.SelectItem;
|
||||
|
||||
/**
|
||||
* Matches an expression with an alias.
|
||||
*
|
||||
* @param pattern for the expression to match
|
||||
* @param alias to match
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
record AliasedPattern (SelectItemPattern pattern, String alias) implements SelectItemPattern {
|
||||
|
||||
@Override
|
||||
public boolean matches(SelectItem selectItem) {
|
||||
|
||||
if (selectItem instanceof SelectExpressionItem sei) {
|
||||
return pattern.matches(sei) && sei.getAlias() != null && sei.getAlias().getName().equals(alias);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return pattern + " as " + alias;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2023 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.relational.core.sqlgeneration;
|
||||
|
||||
import net.sf.jsqlparser.expression.AnalyticExpression;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.statement.select.SelectExpressionItem;
|
||||
import net.sf.jsqlparser.statement.select.SelectItem;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Pattern matching analytic functions
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
public class AnalyticFunctionPattern extends TypedExpressionPattern<AnalyticExpression> {
|
||||
|
||||
private final ExpressionPattern partitionBy;
|
||||
private String functionName;
|
||||
|
||||
public AnalyticFunctionPattern(String rowNumber, ExpressionPattern partitionBy) {
|
||||
|
||||
super(AnalyticExpression.class);
|
||||
|
||||
this.functionName = rowNumber;
|
||||
this.partitionBy = partitionBy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(SelectItem selectItem) {
|
||||
|
||||
if (selectItem instanceof SelectExpressionItem sei) {
|
||||
Expression expression = sei.getExpression();
|
||||
if (expression instanceof AnalyticExpression analyticExpression) {
|
||||
return matches(analyticExpression);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean matches(AnalyticExpression analyticExpression) {
|
||||
return analyticExpression.getName().toLowerCase().equals(functionName)
|
||||
&& partitionByMatches(analyticExpression);
|
||||
}
|
||||
|
||||
private boolean partitionByMatches(AnalyticExpression analyticExpression) {
|
||||
|
||||
List<Expression> expressions = analyticExpression.getPartitionExpressionList().getExpressions();
|
||||
return expressions != null && expressions.size() == 1 && partitionBy.matches(expressions.get(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "row_number() OVER (PARTITION BY " + partitionBy + ')';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2023 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.relational.core.sqlgeneration;
|
||||
|
||||
import net.sf.jsqlparser.schema.Column;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Pattern matching just a simple column
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class ColumnPattern extends TypedExpressionPattern<Column> {
|
||||
private final String columnName;
|
||||
|
||||
/**
|
||||
* @param columnName name of the expected column.
|
||||
*/
|
||||
ColumnPattern(String columnName) {
|
||||
|
||||
super(Column.class);
|
||||
|
||||
this.columnName = columnName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(Column actualColumn) {
|
||||
return actualColumn.getColumnName().equals(columnName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return columnName;
|
||||
}
|
||||
|
||||
public String columnName() {
|
||||
return columnName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this)
|
||||
return true;
|
||||
if (obj == null || obj.getClass() != this.getClass())
|
||||
return false;
|
||||
var that = (ColumnPattern) obj;
|
||||
return Objects.equals(this.columnName, that.columnName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(columnName);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2023 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.relational.core.sqlgeneration;
|
||||
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
|
||||
/**
|
||||
* A pattern that matches various SQL expressions.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
public interface ExpressionPattern {
|
||||
boolean matches(Expression expression);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2023 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.relational.core.sqlgeneration;
|
||||
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.Function;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A pattern matching a function call.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
public final class FunctionPattern extends TypedExpressionPattern<Function> {
|
||||
private final String name;
|
||||
private final List<ExpressionPattern> params;
|
||||
|
||||
/**
|
||||
* @param name name of the function.
|
||||
* @param params patterns to match the function arguments.
|
||||
*/
|
||||
public FunctionPattern(String name, List<ExpressionPattern> params) {
|
||||
|
||||
super(Function.class);
|
||||
|
||||
this.name = name;
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
FunctionPattern(String name, ExpressionPattern... params) {
|
||||
this(name, Arrays.asList(params));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean matches(Function function) {
|
||||
|
||||
if (function.getName().equalsIgnoreCase(name)) {
|
||||
List<Expression> expressions = new ArrayList<>(function.getParameters().getExpressions());
|
||||
for (ExpressionPattern param : params) {
|
||||
boolean found = false;
|
||||
for (Expression exp : expressions) {
|
||||
if (param.matches(exp)) {
|
||||
expressions.remove(exp);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return expressions.isEmpty();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name + "(" + params.stream().map(Object::toString).collect(Collectors.joining(", ")) + ")";
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public List<ExpressionPattern> params() {
|
||||
return params;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this) return true;
|
||||
if (obj == null || obj.getClass() != this.getClass()) return false;
|
||||
var that = (FunctionPattern) obj;
|
||||
return Objects.equals(this.name, that.name) &&
|
||||
Objects.equals(this.params, that.params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, params);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2023 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.relational.core.sqlgeneration;
|
||||
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.statement.select.Join;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.assertj.core.api.AbstractAssert;
|
||||
|
||||
/**
|
||||
* AspectJ {@link org.assertj.core.api.Assert} for writing assertions about joins in SQL statements.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
public class JoinAssert extends AbstractAssert<JoinAssert, Join> {
|
||||
public JoinAssert(Join join) {
|
||||
super(join, JoinAssert.class);
|
||||
}
|
||||
|
||||
JoinAssert on(String left, String right) {
|
||||
|
||||
Collection<Expression> onExpressions = actual.getOnExpressions();
|
||||
|
||||
if (!(onExpressions.iterator().next().toString().equals(left + " = " + right))) {
|
||||
throw failureWithActualExpected(actual, left + " = " + right,
|
||||
"actual join condition %s does not match expected %s = %s", actual, left, right);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2023 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.relational.core.sqlgeneration;
|
||||
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.statement.select.SelectExpressionItem;
|
||||
import net.sf.jsqlparser.statement.select.SelectItem;
|
||||
|
||||
/**
|
||||
* Pattern matching a literal expression in a SQL statement.
|
||||
*
|
||||
* @param value the value of the expression
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
record LiteralPattern(Object value) implements SelectItemPattern, ExpressionPattern {
|
||||
|
||||
@Override
|
||||
public boolean matches(SelectItem selectItem) {
|
||||
return selectItem instanceof SelectExpressionItem sei && matches(sei.getExpression());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(Expression expression) {
|
||||
return expression.toString().equals(String.valueOf(value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2023 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.relational.core.sqlgeneration;
|
||||
|
||||
import net.sf.jsqlparser.statement.select.SelectItem;
|
||||
|
||||
/**
|
||||
* A pattern matching a simple column.
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
interface SelectItemPattern {
|
||||
default AliasedPattern as(String alias) {
|
||||
return new AliasedPattern(this, alias);
|
||||
}
|
||||
|
||||
boolean matches(SelectItem selectItem);
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
* Copyright 2023 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.relational.core.sqlgeneration;
|
||||
|
||||
import static org.springframework.data.relational.core.sqlgeneration.SqlAssert.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mapping.PersistentPropertyPath;
|
||||
import org.springframework.data.relational.core.dialect.Dialect;
|
||||
import org.springframework.data.relational.core.dialect.PostgresDialect;
|
||||
import org.springframework.data.relational.core.mapping.AggregatePath;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
|
||||
/**
|
||||
* Tests for {@link SingleQuerySqlGenerator}.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class SingleQuerySqlGeneratorUnitTests {
|
||||
|
||||
RelationalMappingContext context = new RelationalMappingContext();
|
||||
Dialect dialect = createDialect();
|
||||
|
||||
@Nested
|
||||
class TrivialAggregateWithoutReferences extends AbstractTestFixture {
|
||||
|
||||
TrivialAggregateWithoutReferences() {
|
||||
super(TrivialAggregate.class);
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void createSelectForFindAll() {
|
||||
|
||||
String sql = sqlGenerator.findAll();
|
||||
|
||||
SqlAssert fullSelect = assertThatParsed(sql);
|
||||
fullSelect.extractOrderBy().isEqualTo(alias("id") + ", rn");
|
||||
|
||||
SqlAssert baseSelect = fullSelect.hasInlineView();
|
||||
|
||||
baseSelect //
|
||||
.hasExactlyColumns( //
|
||||
col(rnAlias()).as("rn"), //
|
||||
col(rnAlias()), //
|
||||
col(alias("id")), //
|
||||
col(alias("name")) //
|
||||
) //
|
||||
.hasInlineViewSelectingFrom("\"trivial_aggregate\"") //
|
||||
.hasExactlyColumns( //
|
||||
lit(1).as(rnAlias()), //
|
||||
lit(1).as(rcAlias()), //
|
||||
col("\"id\"").as(alias("id")), //
|
||||
col("\"name\"").as(alias("name")) //
|
||||
);
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void createSelectForFindById() {
|
||||
|
||||
String sql = sqlGenerator.findById();
|
||||
|
||||
SqlAssert baseSelect = assertThatParsed(sql).hasInlineView();
|
||||
|
||||
baseSelect //
|
||||
.hasExactlyColumns( //
|
||||
col(rnAlias()).as("rn"), //
|
||||
col(rnAlias()), //
|
||||
col(alias("id")), //
|
||||
col(alias("name")) //
|
||||
) //
|
||||
.hasInlineViewSelectingFrom("\"trivial_aggregate\"") //
|
||||
.hasExactlyColumns( //
|
||||
lit(1).as(rnAlias()), //
|
||||
lit(1).as(rcAlias()), //
|
||||
col("\"id\"").as(alias("id")), //
|
||||
col("\"name\"").as(alias("name")) //
|
||||
) //
|
||||
.extractWhereClause().isEqualTo("\"trivial_aggregate\".\"id\" = :id");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void createSelectForFindAllById() {
|
||||
|
||||
String sql = sqlGenerator.findAllById();
|
||||
|
||||
SqlAssert baseSelect = assertThatParsed(sql).hasInlineView();
|
||||
|
||||
baseSelect //
|
||||
.hasExactlyColumns( //
|
||||
col(rnAlias()).as("rn"), //
|
||||
col(rnAlias()), //
|
||||
col(alias("id")), //
|
||||
col(alias("name")) //
|
||||
) //
|
||||
.hasInlineViewSelectingFrom("\"trivial_aggregate\"") //
|
||||
.hasExactlyColumns( //
|
||||
lit(1).as(rnAlias()), //
|
||||
lit(1).as(rcAlias()), //
|
||||
col("\"id\"").as(alias("id")), //
|
||||
col("\"name\"").as(alias("name")) //
|
||||
) //
|
||||
.extractWhereClause().isEqualTo("\"trivial_aggregate\".\"id\" IN (:ids)");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class AggregateWithSingleReference extends AbstractTestFixture {
|
||||
|
||||
private AggregateWithSingleReference() {
|
||||
super(SingleReferenceAggregate.class);
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void createSelectForFindById() {
|
||||
|
||||
String sql = sqlGenerator.findById();
|
||||
|
||||
String rootRowNumber = rnAlias();
|
||||
String rootCount = rcAlias();
|
||||
String trivialsRowNumber = rnAlias("trivials");
|
||||
String backref = backRefAlias("trivials");
|
||||
String keyAlias = keyAlias("trivials");
|
||||
|
||||
SqlAssert baseSelect = assertThatParsed(sql).hasInlineView();
|
||||
|
||||
baseSelect //
|
||||
.hasExactlyColumns( //
|
||||
|
||||
col(rootRowNumber), //
|
||||
col(alias("id")), //
|
||||
col(alias("name")), //
|
||||
col(trivialsRowNumber), //
|
||||
col(alias("trivials.id")), //
|
||||
col(alias("trivials.name")), //
|
||||
func("greatest", func("coalesce",col(rootRowNumber), lit(1)), func("coalesce",col(trivialsRowNumber), lit(1))), //
|
||||
col(backref), //
|
||||
col(keyAlias) //
|
||||
).extractWhereClause() //
|
||||
.doesNotContainIgnoringCase("and") //
|
||||
.containsIgnoringCase(trivialsRowNumber + " is null") //
|
||||
.containsIgnoringCase(trivialsRowNumber + " = " + rootRowNumber) //
|
||||
.containsIgnoringCase(trivialsRowNumber + " > " + rootCount);
|
||||
baseSelect.hasInlineViewSelectingFrom("\"single_reference_aggregate\"") //
|
||||
.hasExactlyColumns( //
|
||||
lit(1).as(rnAlias()), lit(1).as(rootCount), //
|
||||
col("\"id\"").as(alias("id")), //
|
||||
col("\"name\"").as(alias("name")) //
|
||||
) //
|
||||
.extractWhereClause().isEqualTo("\"single_reference_aggregate\".\"id\" = :id");
|
||||
baseSelect.hasInlineViewSelectingFrom("\"trivial_aggregate\"") //
|
||||
.hasExactlyColumns( //
|
||||
rn(col("\"single_reference_aggregate\"")).as(trivialsRowNumber), //
|
||||
count(col("\"single_reference_aggregate\"")).as(rcAlias("trivials")), //
|
||||
col("\"id\"").as(alias("trivials.id")), //
|
||||
col("\"name\"").as(alias("trivials.name")), //
|
||||
col("\"single_reference_aggregate\"").as(backref), //
|
||||
col("\"single_reference_aggregate_key\"").as(keyAlias) //
|
||||
).extractWhereClause().isEmpty();
|
||||
baseSelect.hasJoin().on(alias("id"), backref);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private AggregatePath path(Class<?> type) {
|
||||
return context.getAggregatePath(context.getRequiredPersistentEntity(type));
|
||||
}
|
||||
|
||||
private AggregatePath path(Class<?> type, String pathAsString) {
|
||||
PersistentPropertyPath<RelationalPersistentProperty> persistentPropertyPath = context
|
||||
.getPersistentPropertyPath(pathAsString, type);
|
||||
return context.getAggregatePath(persistentPropertyPath);
|
||||
}
|
||||
|
||||
private static Dialect createDialect() {
|
||||
|
||||
return PostgresDialect.INSTANCE;
|
||||
}
|
||||
|
||||
record TrivialAggregate(@Id Long id, String name) {
|
||||
}
|
||||
|
||||
record SingleReferenceAggregate(@Id Long id, String name, List<TrivialAggregate> trivials) {
|
||||
}
|
||||
|
||||
private class AbstractTestFixture {
|
||||
final Class<?> aggregateRootType;
|
||||
final SingleQuerySqlGenerator sqlGenerator;
|
||||
final AliasFactory aliases;
|
||||
|
||||
private AbstractTestFixture(Class<?> aggregateRootType) {
|
||||
|
||||
this.aggregateRootType = aggregateRootType;
|
||||
this.sqlGenerator = new SingleQuerySqlGenerator(context, dialect,
|
||||
context.getRequiredPersistentEntity(aggregateRootType));
|
||||
this.aliases = sqlGenerator.getAliasFactory();
|
||||
|
||||
}
|
||||
|
||||
AggregatePath path() {
|
||||
return SingleQuerySqlGeneratorUnitTests.this.path(aggregateRootType);
|
||||
}
|
||||
|
||||
AggregatePath path(String pathAsString) {
|
||||
return SingleQuerySqlGeneratorUnitTests.this.path(aggregateRootType, pathAsString);
|
||||
}
|
||||
|
||||
protected String rnAlias() {
|
||||
return aliases.getRowNumberAlias(path());
|
||||
}
|
||||
|
||||
protected String rnAlias(String path) {
|
||||
return aliases.getRowNumberAlias(path(path));
|
||||
}
|
||||
|
||||
protected String rcAlias() {
|
||||
return aliases.getRowCountAlias(path());
|
||||
}
|
||||
|
||||
protected String rcAlias(String path) {
|
||||
return aliases.getRowCountAlias(path(path));
|
||||
}
|
||||
|
||||
protected String alias(String path) {
|
||||
return aliases.getColumnAlias(path(path));
|
||||
}
|
||||
|
||||
protected String backRefAlias(String path) {
|
||||
return aliases.getBackReferenceAlias(path(path));
|
||||
}
|
||||
|
||||
protected String keyAlias(String path) {
|
||||
return aliases.getKeyAlias(path(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* Copyright 2023 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.relational.core.sqlgeneration;
|
||||
|
||||
import net.sf.jsqlparser.JSQLParserException;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
|
||||
import net.sf.jsqlparser.schema.Table;
|
||||
import net.sf.jsqlparser.statement.Statement;
|
||||
import net.sf.jsqlparser.statement.select.FromItem;
|
||||
import net.sf.jsqlparser.statement.select.Join;
|
||||
import net.sf.jsqlparser.statement.select.OrderByElement;
|
||||
import net.sf.jsqlparser.statement.select.PlainSelect;
|
||||
import net.sf.jsqlparser.statement.select.Select;
|
||||
import net.sf.jsqlparser.statement.select.SelectItem;
|
||||
import net.sf.jsqlparser.statement.select.SpecialSubSelect;
|
||||
import net.sf.jsqlparser.statement.select.SubSelect;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.assertj.core.api.AbstractAssert;
|
||||
import org.assertj.core.api.StringAssert;
|
||||
import org.assertj.core.util.Strings;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
|
||||
/**
|
||||
* AssertJ {@link org.assertj.core.api.Assert} for writing assertions about SQL statements.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class SqlAssert extends AbstractAssert<SqlAssert, PlainSelect> {
|
||||
private final PlainSelect actual;
|
||||
|
||||
public SqlAssert(PlainSelect actual) {
|
||||
super(actual, SqlAssert.class);
|
||||
|
||||
this.actual = actual;
|
||||
}
|
||||
|
||||
static SqlAssert assertThatParsed(String actualSql) {
|
||||
|
||||
try {
|
||||
Statement parsed = CCJSqlParserUtil.parse(actualSql);
|
||||
return new SqlAssert((PlainSelect) ((Select) parsed).getSelectBody());
|
||||
} catch (JSQLParserException e) {
|
||||
Assertions.fail("Couldn't parse '%s'".formatted(actualSql), e);
|
||||
}
|
||||
|
||||
throw new IllegalStateException("This should be unreachable");
|
||||
}
|
||||
|
||||
static LiteralPattern lit(Object value) {
|
||||
return new LiteralPattern(value);
|
||||
}
|
||||
|
||||
static ColumnPattern col(String columnName) {
|
||||
return new ColumnPattern(columnName);
|
||||
}
|
||||
|
||||
static AnalyticFunctionPattern rn(ExpressionPattern partitionBy) {
|
||||
return new AnalyticFunctionPattern("row_number", partitionBy);
|
||||
}
|
||||
|
||||
static AnalyticFunctionPattern count(ExpressionPattern partitionBy) {
|
||||
return new AnalyticFunctionPattern("count", partitionBy);
|
||||
}
|
||||
|
||||
static FunctionPattern func(String name, ExpressionPattern ... params) {
|
||||
return new FunctionPattern(name, params);
|
||||
}
|
||||
static FunctionPattern func(String name, String ... params) {
|
||||
return new FunctionPattern(name, Arrays.stream(params).map(p -> col(p)).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
SqlAssert hasExactlyColumns(String... columns) {
|
||||
|
||||
SelectItemPattern[] patterns = new SelectItemPattern[columns.length];
|
||||
|
||||
for (int i = 0; i < columns.length; i++) {
|
||||
patterns[i] = col(columns[i]);
|
||||
}
|
||||
|
||||
return hasExactlyColumns(patterns);
|
||||
}
|
||||
|
||||
SqlAssert hasExactlyColumns(SelectItemPattern... columns) {
|
||||
|
||||
List<SelectItem> actualSelectItems = actual.getSelectItems();
|
||||
List<SelectItemPattern> unmatchedPatterns = new ArrayList<>(Arrays.asList(columns));
|
||||
List<SelectItem> unmatchedSelectItems = new ArrayList<>();
|
||||
|
||||
for (SelectItem selectItem : actualSelectItems) {
|
||||
|
||||
SelectItemPattern matchedPattern = null;
|
||||
for (SelectItemPattern column : unmatchedPatterns) {
|
||||
if (column.matches(selectItem)) {
|
||||
matchedPattern = column;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedPattern != null) {
|
||||
unmatchedPatterns.remove(matchedPattern);
|
||||
} else {
|
||||
unmatchedSelectItems.add(selectItem);
|
||||
}
|
||||
}
|
||||
|
||||
if (unmatchedPatterns.isEmpty() && unmatchedSelectItems.isEmpty()) {
|
||||
return this;
|
||||
}
|
||||
|
||||
String preparedExpectedColumns = prepare(columns);
|
||||
|
||||
if (unmatchedPatterns.isEmpty()) {
|
||||
throw failureWithActualExpected(actual, preparedExpectedColumns, """
|
||||
Expected
|
||||
%s
|
||||
to select the columns
|
||||
%s
|
||||
but
|
||||
%s
|
||||
were not expected
|
||||
""", actual, preparedExpectedColumns, unmatchedSelectItems);
|
||||
}
|
||||
if (unmatchedSelectItems.isEmpty()) {
|
||||
throw failureWithActualExpected(actual, preparedExpectedColumns, """
|
||||
Expected
|
||||
%s
|
||||
to select the columns
|
||||
%s
|
||||
but
|
||||
%s
|
||||
were not present
|
||||
""", actual, preparedExpectedColumns, unmatchedPatterns);
|
||||
}
|
||||
throw failureWithActualExpected(actual, preparedExpectedColumns, """
|
||||
Expected
|
||||
%s
|
||||
to select the columns
|
||||
%s
|
||||
but
|
||||
%s
|
||||
were not present and
|
||||
%s
|
||||
were not expected""", actual, preparedExpectedColumns, unmatchedPatterns, unmatchedSelectItems);
|
||||
}
|
||||
|
||||
public StringAssert extractWhereClause() {
|
||||
Expression where = actual.getWhere();
|
||||
return new StringAssert(where == null ? "" : where.toString());
|
||||
}
|
||||
public JoinAssert hasJoin() {
|
||||
List<Join> joins = actual.getJoins();
|
||||
|
||||
if (joins == null || joins.size() < 1) {
|
||||
throw failureWithActualExpected(actual, "select with a join", "Expected %s to contain a join but it doesn't.", actual);
|
||||
}
|
||||
|
||||
return new JoinAssert(joins.get(0));
|
||||
}
|
||||
private String prepare(SelectItemPattern[] columns) {
|
||||
return Arrays.toString(columns);
|
||||
}
|
||||
|
||||
SqlAssert hasInlineViewSelectingFrom(String tableName) {
|
||||
|
||||
Optional<PlainSelect> matchingSelect = getSubSelects(actual)
|
||||
.filter(ps -> (ps.getFromItem()instanceof Table t) && t.getName().equals(tableName)).findFirst();
|
||||
|
||||
if (matchingSelect.isEmpty()) {
|
||||
throw failureWithActualExpected(actual, "Subselect from " + tableName,
|
||||
"%s is expected to contain a subselect selecting from %s but doesn't", actual, tableName);
|
||||
}
|
||||
|
||||
return new SqlAssert(matchingSelect.get());
|
||||
}
|
||||
|
||||
|
||||
public SqlAssert hasInlineView() {
|
||||
Optional<PlainSelect> matchingSelect = getSubSelects(actual).findFirst();
|
||||
|
||||
if (matchingSelect.isEmpty()) {
|
||||
throw failureWithActualExpected(actual, "Subselect",
|
||||
"%s is expected to contain a subselect", actual);
|
||||
}
|
||||
|
||||
return new SqlAssert(matchingSelect.get());
|
||||
}
|
||||
|
||||
private static Stream<PlainSelect> getSubSelects(PlainSelect select) {
|
||||
|
||||
FromItem fromItem = select.getFromItem();
|
||||
|
||||
Stream<PlainSelect> fromStream = subSelects(fromItem);
|
||||
|
||||
return Stream.of(select).flatMap(s -> {
|
||||
List<Join> joins = s.getJoins();
|
||||
if (joins == null) {
|
||||
return fromStream;
|
||||
}
|
||||
|
||||
Stream<PlainSelect> joinStream = joins.stream() //
|
||||
.map(j -> j.getRightItem()) //
|
||||
.flatMap(ss -> subSelects(ss));
|
||||
return Stream.concat(fromStream, joinStream);
|
||||
});
|
||||
}
|
||||
|
||||
private static Stream<PlainSelect> subSelects(FromItem fromItem) {
|
||||
Stream<PlainSelect> fromStream;
|
||||
if (fromItem instanceof SubSelect ss) {
|
||||
fromStream = Stream.of((PlainSelect) ss.getSelectBody());
|
||||
} else if (fromItem instanceof SpecialSubSelect ss) {
|
||||
fromStream = Stream.of((PlainSelect) ss.getSubSelect().getSelectBody());
|
||||
} else {
|
||||
fromStream = Stream.empty();
|
||||
}
|
||||
return fromStream;
|
||||
}
|
||||
|
||||
public StringAssert extractOrderBy() {
|
||||
|
||||
List<OrderByElement> orderByElements = actual.getOrderByElements();
|
||||
return new StringAssert(orderByElements == null ? "" : Strings.join(orderByElements).with(", "));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* Copyright 2023 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.relational.core.sqlgeneration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.springframework.data.relational.core.sqlgeneration.SqlAssert.*;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests for SqlAssert.
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class SqlAssertUnitTests {
|
||||
|
||||
@Test // GH-1446
|
||||
void givesProperNullPointerExceptionWhenSqlIsNull() {
|
||||
assertThatThrownBy(() -> SqlAssert.assertThatParsed(null)).isInstanceOf(NullPointerException.class);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class AssertWhereClause {
|
||||
@Test // GH-1446
|
||||
void assertWhereClause() {
|
||||
SqlAssert.assertThatParsed("select x from t where z > y").extractWhereClause().isEqualTo("z > y");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void assertNoWhereClause() {
|
||||
SqlAssert.assertThatParsed("select x from t").extractWhereClause().isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class AssertOrderByClause {
|
||||
@Test // GH-1446
|
||||
void assertOrderByClause() {
|
||||
SqlAssert.assertThatParsed("select x from t order by x, y").extractOrderBy().isEqualTo("x, y");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void assertNoOrderByClause() {
|
||||
SqlAssert.assertThatParsed("select x from t").extractOrderBy().isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class AssertColumns {
|
||||
@Test // GH-1446
|
||||
void matchingSimpleColumns() {
|
||||
SqlAssert.assertThatParsed("select x, y, z from t").hasExactlyColumns("x", "y", "z");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extraSimpleColumn() {
|
||||
|
||||
SqlAssert sqlAssert = SqlAssert.assertThatParsed("select x, y, z, a from t");
|
||||
|
||||
assertThatThrownBy(() -> sqlAssert.hasExactlyColumns("x", "y", "z")) //
|
||||
.hasMessageContaining("x, y, z") //
|
||||
.hasMessageContaining("x, y, z, a") //
|
||||
.hasMessageContaining("a");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void missingSimpleColumn() {
|
||||
|
||||
SqlAssert sqlAssert = SqlAssert.assertThatParsed("select x, y, z from t");
|
||||
|
||||
assertThatThrownBy(() -> sqlAssert.hasExactlyColumns("x", "y", "z", "a")) //
|
||||
.hasMessageContaining("x, y, z") //
|
||||
.hasMessageContaining("x, y, z, a") //
|
||||
.hasMessageContaining("a");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void wrongSimpleColumn() {
|
||||
|
||||
SqlAssert sqlAssert = SqlAssert.assertThatParsed("select x, y, z from t");
|
||||
|
||||
assertThatThrownBy(() -> sqlAssert.hasExactlyColumns("x", "a", "z")) //
|
||||
.hasMessageContaining("x, y, z") //
|
||||
.hasMessageContaining("x, a, z") //
|
||||
.hasMessageContaining("a") //
|
||||
.hasMessageContaining("y");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void matchesFullyQualifiedColumn() {
|
||||
|
||||
SqlAssert.assertThatParsed("select t.x from t") //
|
||||
.hasExactlyColumns("x");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void matchesFunction() { //
|
||||
|
||||
SqlAssert.assertThatParsed("select someFunc(x) from t")
|
||||
.hasExactlyColumns(func("someFunc", col("x")));
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void matchesFunctionCaseInsensitive() {
|
||||
|
||||
SqlAssert.assertThatParsed("select COUNT(x) from t") //
|
||||
.hasExactlyColumns(func("count", col("x")));
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void matchFunctionFailsOnDifferentName() {
|
||||
SqlAssert sqlAssert = assertThatParsed("select countx(x) from t");
|
||||
assertThatThrownBy(() -> sqlAssert.hasExactlyColumns(func("count", col("x")))) //
|
||||
.hasMessageContaining("countx(x)") //
|
||||
.hasMessageContaining("count(x)");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void matchFunctionFailsOnDifferentParameter() {
|
||||
|
||||
SqlAssert sqlAssert = assertThatParsed("select count(y) from t");
|
||||
assertThatThrownBy(() -> sqlAssert.hasExactlyColumns(func("count", col("x")))) //
|
||||
.hasMessageContaining("count(y)") //
|
||||
.hasMessageContaining("count(x)");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void matchFunctionFailsOnWrongParameterCount() {
|
||||
|
||||
SqlAssert sqlAssert = assertThatParsed("select count(x, y) from t");
|
||||
assertThatThrownBy(() -> sqlAssert.hasExactlyColumns(func("count", col("x")))) //
|
||||
.hasMessageContaining("count(x, y)") //
|
||||
.hasMessageContaining("count(x)");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class AssertRowNumber {
|
||||
@Test // GH-1446
|
||||
void testMatchingRowNumber() {
|
||||
|
||||
SqlAssert sqlAssert = assertThatParsed("select row_number() over (partition by x) from t");
|
||||
|
||||
sqlAssert.hasExactlyColumns(rn(col("x")));
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void testMatchingRowNumberUpperCase() {
|
||||
|
||||
SqlAssert sqlAssert = assertThatParsed("select ROW_NUMBER() over (partition by x) from t");
|
||||
|
||||
sqlAssert.hasExactlyColumns(rn(col("x")));
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void testFailureNoRowNumber() {
|
||||
|
||||
SqlAssert sqlAssert = assertThatParsed("select row_number as x from t");
|
||||
|
||||
assertThatThrownBy(() -> sqlAssert.hasExactlyColumns(rn(col("x")))) //
|
||||
.hasMessageContaining("row_number AS x") //
|
||||
.hasMessageContaining("row_number() OVER (PARTITION BY x)");
|
||||
;
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void testFailureWrongPartitionBy() {
|
||||
|
||||
SqlAssert sqlAssert = assertThatParsed("select row_number() over (partition by y) from t");
|
||||
|
||||
assertThatThrownBy(() -> sqlAssert.hasExactlyColumns(rn(col("x")))) //
|
||||
.hasMessageContaining("row_number() OVER (PARTITION BY y )") //
|
||||
.hasMessageContaining("row_number() OVER (PARTITION BY x)");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class AssertAliases {
|
||||
@Test // GH-1446
|
||||
void simpleColumnMatchesWithAlias() {
|
||||
|
||||
SqlAssert sqlAssert = SqlAssert.assertThatParsed("select x as a from t");
|
||||
|
||||
sqlAssert.hasExactlyColumns("x");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void matchWithAlias() {
|
||||
|
||||
SqlAssert sqlAssert = SqlAssert.assertThatParsed("select x as a from t");
|
||||
|
||||
sqlAssert.hasExactlyColumns(col("x").as("a"));
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void matchWithWrongAlias() {
|
||||
|
||||
SqlAssert sqlAssert = SqlAssert.assertThatParsed("select x as b from t");
|
||||
|
||||
assertThatThrownBy(() -> sqlAssert.hasExactlyColumns(col("x").as("a"))) //
|
||||
.hasMessageContaining("x as a") //
|
||||
.hasMessageContaining("x AS b");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void matchesIdenticalColumnsWithDifferentAliases() {
|
||||
|
||||
SqlAssert sqlAssert = SqlAssert.assertThatParsed("select 1 as x, 1 as y from t");
|
||||
|
||||
sqlAssert.hasExactlyColumns(lit(1).as("x"), lit(1).as("y"));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class AssertSubSelects {
|
||||
@Test // GH-1446
|
||||
void subselectGetsFound() {
|
||||
|
||||
SqlAssert sqlAssert = SqlAssert.assertThatParsed("select a from (select x as a from t) s");
|
||||
|
||||
sqlAssert //
|
||||
.hasInlineViewSelectingFrom("t") //
|
||||
.hasExactlyColumns(col("x").as("a"));
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void subselectWithWrongTableDoesNotGetFound() {
|
||||
|
||||
SqlAssert sqlAssert = SqlAssert.assertThatParsed("select a from (select x as a from u) s");
|
||||
|
||||
assertThatThrownBy(() -> sqlAssert //
|
||||
.hasInlineViewSelectingFrom("t"))
|
||||
.hasMessageContaining("is expected to contain a subselect selecting from t but doesn't");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class AssertJoins {
|
||||
@Test // GH-1446
|
||||
void hasJoin() {
|
||||
SqlAssert sqlAssert = SqlAssert.assertThatParsed("select c from t join s on x = y");
|
||||
|
||||
sqlAssert.hasJoin();
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void hasJoinFailure() {
|
||||
SqlAssert sqlAssert = SqlAssert.assertThatParsed("select c from t where x = y");
|
||||
|
||||
assertThatThrownBy(() -> sqlAssert //
|
||||
.hasJoin()).hasMessageContaining("to contain a join but it doesn't");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void on() {
|
||||
|
||||
SqlAssert sqlAssert = SqlAssert.assertThatParsed("select c from t join s on x = y");
|
||||
|
||||
sqlAssert.hasJoin().on("x", "y");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void onFailureFirst() {
|
||||
|
||||
SqlAssert sqlAssert = SqlAssert.assertThatParsed("select c from t join s on z = y");
|
||||
|
||||
assertThatThrownBy(() -> sqlAssert.hasJoin().on("x", "y"))
|
||||
.hasMessageContaining("z = y does not match expected x = y");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void onFailureSecond() {
|
||||
|
||||
SqlAssert sqlAssert = SqlAssert.assertThatParsed("select c from t join s on x = z");
|
||||
|
||||
assertThatThrownBy(() -> sqlAssert.hasJoin().on("x", "y"))
|
||||
.hasMessageContaining("x = z does not match expected x = y");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2023 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.relational.core.sqlgeneration;
|
||||
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.statement.select.SelectExpressionItem;
|
||||
import net.sf.jsqlparser.statement.select.SelectItem;
|
||||
|
||||
/**
|
||||
* A {@link SelectItemPattern} that matches a specific type of expression
|
||||
*
|
||||
* @param <T> the type of the expression that is matched by this pattern.
|
||||
*/
|
||||
abstract class TypedExpressionPattern<T> implements SelectItemPattern, ExpressionPattern {
|
||||
|
||||
private final Class<T> type;
|
||||
|
||||
TypedExpressionPattern(Class<T> type) {
|
||||
|
||||
this.type = type;
|
||||
}
|
||||
@Override
|
||||
public boolean matches(SelectItem selectItem) {
|
||||
|
||||
if (selectItem instanceof SelectExpressionItem sei) {
|
||||
|
||||
Expression expression = sei.getExpression();
|
||||
return matches(expression);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(Expression expression) {
|
||||
|
||||
if (type.isInstance(expression)) {
|
||||
return matches((T) expression);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
abstract boolean matches(T expression);
|
||||
}
|
||||
@@ -471,6 +471,33 @@ This process also applies to inserting new aggregates, where a `null` or `0` ver
|
||||
|
||||
During deletes the version check also applies but no version is increased.
|
||||
|
||||
[[jdbc.loading-aggregates]]
|
||||
== Loading Aggregates
|
||||
Spring Data JDBC offers two ways how it can load aggregates.
|
||||
The traditional and before version 3.2 the only way is really simple:
|
||||
Each query loads the aggregate roots, independently if the query is based on a `CrudRepository` method, a derived query or a annotated query.
|
||||
If the aggregate root references other entities those are loaded with separate statements.
|
||||
|
||||
Spring Data JDBC now allows the use of _Single Query Loading_.
|
||||
With this an arbitrary number of aggregates can be fully loaded with a single SQL query.
|
||||
This should be significant more efficient, especially for complex aggregates, consisting of many entities.
|
||||
|
||||
Currently this feature is very restricted.
|
||||
|
||||
1. It only works for aggregates that only reference one entity collection. The plan is to remove this constraint in the future.
|
||||
|
||||
2. The aggregate must also not use `AggregateReference` or embedded entities. The plan is to remove this constraint in the future.
|
||||
|
||||
3. The database dialect must support it. Of the dialects provided by Spring Data JDBC all but H2 and HSQL support this. H2 and HSQL don't support analytic functions (aka windowing functions).
|
||||
|
||||
4. It only works for the find methods in `CrudRepository`, not for derived queries and not for annotated queries. The plan is to remove this constraint in the future.
|
||||
|
||||
5. Single Query Loading needs to be enabled in the `JdbcMappingContext`, by calling `setSingleQueryLoadingEnabled(true)`
|
||||
|
||||
Note: Single Query Loading is to be considered experimental. We appreciate feedback on how it works for you.
|
||||
|
||||
Note:Single Query Loading can be abbreviated as SQL, but we highly discourage that since confusion with Structured Query Language is almost guaranteed.
|
||||
|
||||
[[jdbc.query-methods]]
|
||||
== Query Methods
|
||||
|
||||
|
||||
Reference in New Issue
Block a user