Skip to content

Commit

Permalink
Migrate JDBC to use RowDocument for reading aggregates.
Browse files Browse the repository at this point in the history
Original pull request #1618
See #1554
  • Loading branch information
mp911de authored and schauder committed Oct 13, 2023
1 parent 665ae6b commit 4e3120e
Show file tree
Hide file tree
Showing 11 changed files with 677 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLType;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
Expand All @@ -44,13 +46,17 @@
import org.springframework.data.mapping.model.SpELContext;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider;
import org.springframework.data.projection.EntityProjection;
import org.springframework.data.relational.core.conversion.MappingRelationalConverter;
import org.springframework.data.relational.core.conversion.ObjectPath;
import org.springframework.data.relational.core.conversion.RelationalConverter;
import org.springframework.data.relational.core.conversion.RowDocumentAccessor;
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.IdentifierProcessing;
import org.springframework.data.relational.domain.RowDocument;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -83,18 +89,15 @@ public class BasicJdbcConverter extends MappingRelationalConverter implements Jd
private SpELContext spELContext;

/**
* Creates a new {@link BasicJdbcConverter} given {@link MappingContext} and a
* {@link JdbcTypeFactory#unsupported() no-op type factory} throwing {@link UnsupportedOperationException} on type
* creation. Use
* Creates a new {@link BasicJdbcConverter} given {@link MappingContext} and a {@link JdbcTypeFactory#unsupported()
* no-op type factory} throwing {@link UnsupportedOperationException} on type creation. Use
* {@link #BasicJdbcConverter(RelationalMappingContext, RelationResolver, CustomConversions, JdbcTypeFactory, IdentifierProcessing)}
* (MappingContext, RelationResolver, JdbcTypeFactory)} to convert arrays and large objects into JDBC-specific types.
*
* @param context must not be {@literal null}.
* @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}.
*/
public BasicJdbcConverter(
RelationalMappingContext context,
RelationResolver relationResolver) {
public BasicJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver) {

super(context, new JdbcCustomConversions());

Expand All @@ -115,10 +118,8 @@ public BasicJdbcConverter(
* @param identifierProcessing must not be {@literal null}
* @since 2.0
*/
public BasicJdbcConverter(
RelationalMappingContext context,
RelationResolver relationResolver, CustomConversions conversions, JdbcTypeFactory typeFactory,
IdentifierProcessing identifierProcessing) {
public BasicJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver,
CustomConversions conversions, JdbcTypeFactory typeFactory, IdentifierProcessing identifierProcessing) {

super(context, conversions);

Expand Down Expand Up @@ -300,16 +301,241 @@ private JdbcValue tryToConvertToJdbcValue(@Nullable Object value) {

@Override
public <T> T mapRow(RelationalPersistentEntity<T> entity, ResultSet resultSet, Object key) {
return new ReadingContext<T>(getMappingContext().getAggregatePath( entity),
new ResultSetAccessor(resultSet), Identifier.empty(), key).mapRow();
return new ReadingContext<T>(getMappingContext().getAggregatePath(entity), new ResultSetAccessor(resultSet),
Identifier.empty(), key).mapRow();
}


@Override
public <T> T mapRow(AggregatePath path, ResultSet resultSet, Identifier identifier, Object key) {
return new ReadingContext<T>(path, new ResultSetAccessor(resultSet), identifier, key).mapRow();
}

@Override
public <R> R projectAndResolve(EntityProjection<R, ?> projection, RowDocument document) {

RelationalPersistentEntity<?> entity = getMappingContext()
.getRequiredPersistentEntity(projection.getActualDomainType());
ResolvingConversionContext context = new ResolvingConversionContext(newProjectingConversionContext(projection),
getMappingContext().getAggregatePath(entity), Identifier.empty());

return doReadProjection(context, document, projection);
}

@SuppressWarnings("unchecked")
@Override
public <R> R readAndResolve(Class<R> type, RowDocument source, Identifier identifier) {

RelationalPersistentEntity<R> entity = (RelationalPersistentEntity<R>) getMappingContext()
.getRequiredPersistentEntity(type);
AggregatePath path = getMappingContext().getAggregatePath(entity);
Identifier identifierToUse = ResolvingRelationalPropertyValueProvider.potentiallyAppendIdentifier(identifier,
entity, it -> source.get(it.getColumnName().getReference()));
ResolvingConversionContext context = new ResolvingConversionContext(getConversionContext(ObjectPath.ROOT), path,
identifierToUse);

return readAggregate(context, source, entity.getTypeInformation());
}

@Override
protected RelationalPropertyValueProvider newValueProvider(RowDocumentAccessor documentAccessor,
SpELExpressionEvaluator evaluator, ConversionContext context) {

if (context instanceof ResolvingConversionContext rcc) {

AggregatePathValueProvider delegate = (AggregatePathValueProvider) super.newValueProvider(documentAccessor,
evaluator, context);

return new ResolvingRelationalPropertyValueProvider(delegate, documentAccessor, rcc, rcc.identifier());
}

return super.newValueProvider(documentAccessor, evaluator, context);
}

/**
* {@link RelationalPropertyValueProvider} using a resolving context to lookup relations. This is highly
* context-sensitive. Note that the identifier is held here because of a chicken and egg problem, while
* {@link ResolvingConversionContext} hols the {@link AggregatePath}.
*/
class ResolvingRelationalPropertyValueProvider implements RelationalPropertyValueProvider {

private final AggregatePathValueProvider delegate;

private final RowDocumentAccessor accessor;

private final ResolvingConversionContext context;

private final Identifier identifier;

private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider delegate, RowDocumentAccessor accessor,
ResolvingConversionContext context, Identifier identifier) {

AggregatePath path = context.aggregatePath();

this.delegate = delegate;
this.accessor = accessor;
this.context = context;
this.identifier = path.isEntity()
? potentiallyAppendIdentifier(identifier, path.getRequiredLeafEntity(), delegate::getPropertyValue)
: identifier;
}

/**
* Conditionally append the identifier if the entity has an identifier property.
*/
static Identifier potentiallyAppendIdentifier(Identifier base, RelationalPersistentEntity<?> entity,
Function<RelationalPersistentProperty, Object> getter) {

if (entity.hasIdProperty()) {

RelationalPersistentProperty idProperty = entity.getRequiredIdProperty();
Object propertyValue = getter.apply(idProperty);

if (propertyValue != null) {
return base.withPart(idProperty.getColumnName(), propertyValue, idProperty.getType());
}
}

return base;
}

@SuppressWarnings("unchecked")
@Nullable
@Override
public <T> T getPropertyValue(RelationalPersistentProperty property) {

AggregatePath aggregatePath = this.context.aggregatePath();

if (getConversions().isSimpleType(property.getActualType())) {
return (T) delegate.getValue(aggregatePath);
}

if (property.isEntity()) {

if (property.isCollectionLike() || property.isMap()) {

Identifier identifier1 = this.identifier;
if (property.getOwner().hasIdProperty()) {
Object id = this.identifier.get(property.getOwner().getRequiredIdProperty().getColumnName());

if (id != null) {
identifier1 = Identifier.of(aggregatePath.getTableInfo().reverseColumnInfo().name(), id, Object.class);
}
}

Iterable<Object> allByPath = relationResolver.findAllByPath(identifier1,
aggregatePath.getRequiredPersistentPropertyPath());

if (property.isCollectionLike()) {
return (T) allByPath;
}

if (property.isMap()) {
return (T) ITERABLE_OF_ENTRY_TO_MAP_CONVERTER.convert(allByPath);
}

Iterator<Object> iterator = allByPath.iterator();
if (iterator.hasNext()) {
return (T) iterator.next();
}

return null;
}

return hasValue(property) ? (T) readAggregate(this.context, accessor, property.getTypeInformation()) : null;
}

return (T) delegate.getValue(aggregatePath);
}

@Override
public boolean hasValue(RelationalPersistentProperty property) {

if (property.isCollectionLike() || property.isMap()) {
// attempt relation fetch
return true;
}

AggregatePath aggregatePath = context.aggregatePath();

if (property.isEntity()) {

RelationalPersistentEntity<?> entity = getMappingContext().getRequiredPersistentEntity(property);
if (entity.hasIdProperty()) {

RelationalPersistentProperty referenceId = entity.getRequiredIdProperty();
AggregatePath toUse = aggregatePath.append(referenceId);
return delegate.hasValue(toUse);
}

return delegate.hasValue(aggregatePath.getTableInfo().reverseColumnInfo().alias());
}

return delegate.hasValue(aggregatePath);
}

@Override
public RelationalPropertyValueProvider withContext(ConversionContext context) {

return context == this.context ? this
: new ResolvingRelationalPropertyValueProvider(delegate.withContext(context), accessor,
(ResolvingConversionContext) context, identifier);
}

}

/**
* Marker object to indicate that the property value provider should resolve relations.
*
* @param delegate
* @param aggregatePath
* @param identifier
*/
private record ResolvingConversionContext(ConversionContext delegate, AggregatePath aggregatePath,
Identifier identifier) implements ConversionContext {

@Override
public <S> S convert(Object source, TypeInformation<? extends S> typeHint) {
return delegate.convert(source, typeHint);
}

@Override
public <S> S convert(Object source, TypeInformation<? extends S> typeHint, ConversionContext context) {
return delegate.convert(source, typeHint, context);
}

@Override
public ResolvingConversionContext forProperty(String name) {
RelationalPersistentProperty property = aggregatePath.getRequiredLeafEntity().getRequiredPersistentProperty(name);
return forProperty(property);
}

@Override
public ResolvingConversionContext forProperty(RelationalPersistentProperty property) {
ConversionContext nested = delegate.forProperty(property);
return new ResolvingConversionContext(nested, aggregatePath.append(property), identifier);
}

@Override
public ResolvingConversionContext withPath(ObjectPath currentPath) {
return new ResolvingConversionContext(delegate.withPath(currentPath), aggregatePath, identifier);
}

@Override
public ObjectPath getPath() {
return delegate.getPath();
}

@Override
public CustomConversions getCustomConversions() {
return delegate.getCustomConversions();
}

@Override
public RelationalConverter getSourceConverter() {
return delegate.getSourceConverter();
}
}

static Object[] requireObjectArray(Object source) {

Assert.isTrue(source.getClass().isArray(), "Source object is not an array");
Expand Down Expand Up @@ -361,25 +587,23 @@ private class ReadingContext<T> {
private final ResultSetAccessor accessor;

@SuppressWarnings("unchecked")
private ReadingContext(AggregatePath rootPath, ResultSetAccessor accessor, Identifier identifier,
Object key) {
private ReadingContext(AggregatePath rootPath, ResultSetAccessor accessor, Identifier identifier, Object key) {
RelationalPersistentEntity<T> entity = (RelationalPersistentEntity<T>) rootPath.getLeafEntity();

Assert.notNull(entity, "The rootPath must point to an entity");

this.entity = entity;
this.rootPath = rootPath;
this.path = getMappingContext().getAggregatePath( this.entity);
this.path = getMappingContext().getAggregatePath(this.entity);
this.identifier = identifier;
this.key = key;
this.propertyValueProvider = new JdbcPropertyValueProvider(path, accessor);
this.backReferencePropertyValueProvider = new JdbcBackReferencePropertyValueProvider(path, accessor);
this.accessor = accessor;
}

private ReadingContext(RelationalPersistentEntity<T> entity, AggregatePath rootPath,
AggregatePath path, Identifier identifier, Object key,
JdbcPropertyValueProvider propertyValueProvider,
private ReadingContext(RelationalPersistentEntity<T> entity, AggregatePath rootPath, AggregatePath path,
Identifier identifier, Object key, JdbcPropertyValueProvider propertyValueProvider,
JdbcBackReferencePropertyValueProvider backReferencePropertyValueProvider, ResultSetAccessor accessor) {

this.entity = entity;
Expand All @@ -396,8 +620,8 @@ private <S> ReadingContext<S> extendBy(RelationalPersistentProperty property) {

return new ReadingContext<>(
(RelationalPersistentEntity<S>) getMappingContext().getRequiredPersistentEntity(property.getActualType()),
rootPath.append(property), path.append(property), identifier, key,
propertyValueProvider.extendBy(property), backReferencePropertyValueProvider.extendBy(property), accessor);
rootPath.append(property), path.append(property), identifier, key, propertyValueProvider.extendBy(property),
backReferencePropertyValueProvider.extendBy(property), accessor);
}

T mapRow() {
Expand Down
Loading

0 comments on commit 4e3120e

Please sign in to comment.