Skip to content
This repository has been archived by the owner on Jan 19, 2022. It is now read-only.

Spanner array param binding #1703

Merged
merged 6 commits into from
Jun 20, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/src/main/asciidoc/spanner.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1038,6 +1038,8 @@ The following filter options are supported:
* Not like a string
* Contains a string
* Not contains a string
* In
* Not in
ChengyuanZhao marked this conversation as resolved.
Show resolved Hide resolved

Note that the phrase `SymbolIgnoreCase` is translated to `LOWER(SYMBOL) = LOWER(?)` indicating a non-case-sensitive matching.
The `IgnoreCase` phrase may only be appended to fields that correspond to columns of type STRING or BYTES.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ public <T> List<T> queryAll(Class<T> entityClass,
SpannerStatementQueryExecutor.buildStatementFromSqlWithArgs(
SpannerStatementQueryExecutor.applySortingPagingQueryOptions(
entityClass, options, sql, this.mappingContext),
null, null, null, null),
null, null, null, null, null),
options);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@ public <T> List<T> mapToList(ResultSet resultSet, Class<T> entityClass,
public Class<?> getCorrespondingSpannerJavaType(Class originalType, boolean isIterableInnerType) {
Class<?> compatible;
if (isIterableInnerType) {
if (ConverterAwareMappingSpannerEntityWriter.iterablePropertyType2ToMethodMap.keySet()
if (ConverterAwareMappingSpannerEntityWriter.iterablePropertyTypeToMethodMap.keySet()
.contains(originalType)) {
return originalType;
}
compatible = ConverterAwareMappingSpannerEntityWriter.findFirstCompatibleSpannerMultupleItemNativeType(
compatible = ConverterAwareMappingSpannerEntityWriter.findFirstCompatibleSpannerMultipleItemNativeType(
(spannerType) -> canHandlePropertyTypeForArrayRead(originalType, spannerType)
&& this.writeConverter.canConvert(originalType, spannerType));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,11 @@ public class ConverterAwareMappingSpannerEntityWriter implements SpannerEntityWr
*/
public static final Map<Class<?>, BiFunction<ValueBinder, ?, ?>> singleItemTypeValueBinderMethodMap;

static final Map<Class<?>, BiConsumer<ValueBinder<?>, Iterable>>
iterablePropertyType2ToMethodMap = createIterableTypeMapping();
/**
* A map of inner types to functions that bind the List parameter.
*/
public static final Map<Class<?>, BiConsumer<ValueBinder<?>, Iterable>>
iterablePropertyTypeToMethodMap = createIterableTypeMapping();

@SuppressWarnings("unchecked")
private static Map<Class<?>, BiConsumer<ValueBinder<?>, Iterable>> createIterableTypeMapping() {
Expand Down Expand Up @@ -123,8 +126,8 @@ public static Class<?> findFirstCompatibleSpannerSingleItemNativeType(Predicate<
return compatible.isPresent() ? compatible.get() : null;
}

public static Class<?> findFirstCompatibleSpannerMultupleItemNativeType(Predicate<Class> testFunc) {
Optional<Class<?>> compatible = iterablePropertyType2ToMethodMap.keySet().stream().filter(testFunc).findFirst();
public static Class<?> findFirstCompatibleSpannerMultipleItemNativeType(Predicate<Class> testFunc) {
Optional<Class<?>> compatible = iterablePropertyTypeToMethodMap.keySet().stream().filter(testFunc).findFirst();
return compatible.isPresent() ? compatible.get() : null;
}

Expand Down Expand Up @@ -328,15 +331,15 @@ private boolean attemptSetIterableValue(Iterable<Object> value,

// attempt check if there is directly a write method that can accept the
// property
if (!valueSet && iterablePropertyType2ToMethodMap.containsKey(innerType)) {
iterablePropertyType2ToMethodMap.get(innerType).accept(valueBinder,
if (!valueSet && iterablePropertyTypeToMethodMap.containsKey(innerType)) {
iterablePropertyTypeToMethodMap.get(innerType).accept(valueBinder,
value);
valueSet = true;
}

// Finally find any compatible conversion
if (!valueSet) {
for (Class<?> targetType : iterablePropertyType2ToMethodMap.keySet()) {
for (Class<?> targetType : iterablePropertyTypeToMethodMap.keySet()) {
valueSet = attemptSetIterablePropertyWithType(value, valueBinder,
innerType, targetType);
if (valueSet) {
Expand All @@ -352,7 +355,7 @@ private boolean attemptSetIterableValue(Iterable<Object> value,
private boolean attemptSetIterablePropertyWithType(Iterable<Object> value,
ValueBinder<WriteBuilder> valueBinder, Class innerType, Class<?> targetType) {
if (this.writeConverter.canConvert(innerType, targetType)) {
BiConsumer<ValueBinder<?>, Iterable> toMethod = iterablePropertyType2ToMethodMap
BiConsumer<ValueBinder<?>, Iterable> toMethod = iterablePropertyTypeToMethodMap
.get(targetType);
toMethod.accept(valueBinder,
(value != null) ? ConversionUtils.convertIterable(value, targetType, this.writeConverter) : null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,8 @@
import java.util.List;
import java.util.stream.Collectors;


import org.springframework.cloud.gcp.data.spanner.core.SpannerTemplate;
import org.springframework.cloud.gcp.data.spanner.core.mapping.SpannerMappingContext;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;

/**
Expand Down Expand Up @@ -104,7 +102,7 @@ Object processRawObjectForProjection(Object object) {
}

@Override
public QueryMethod getQueryMethod() {
public SpannerQueryMethod getQueryMethod() {
return this.queryMethod;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,23 @@ protected List executeRawResult(Object[] parameters) {
if (isCountOrExistsQuery()) {
return SpannerStatementQueryExecutor.executeQuery(
(struct) -> isCountQuery() ? struct.getLong(0) : struct.getBoolean(0),
this.entityType, this.tree, parameters, this.spannerTemplate,
this.entityType, this.tree, parameters, getQueryMethod().getMethod().getParameters(),
this.spannerTemplate,
this.spannerMappingContext);
}
if (this.tree.isDelete()) {
return this.spannerTemplate
.performReadWriteTransaction(getDeleteFunction(parameters));
}
return SpannerStatementQueryExecutor.executeQuery(this.entityType, this.tree,
parameters, this.spannerTemplate, this.spannerMappingContext);
parameters, getQueryMethod().getMethod().getParameters(), this.spannerTemplate,
this.spannerMappingContext);
}

private Function<SpannerTemplate, List> getDeleteFunction(Object[] parameters) {
return (transactionTemplate) -> {
List<T> entitiesToDelete = SpannerStatementQueryExecutor
.executeQuery(this.entityType, this.tree, parameters,
.executeQuery(this.entityType, this.tree, parameters, getQueryMethod().getMethod().getParameters(),
transactionTemplate, this.spannerMappingContext);
transactionTemplate.deleteAll(entitiesToDelete);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ private Optional<String> findAnnotatedQuery() {
.filter(StringUtils::hasText);
}

/**
* Get the method metadata.
* @return the method metadata.
*/
public Method getMethod() {
return this.method;
}

/**
* Returns the {@link Query} annotation that is applied to the method or {@code null}
* if none available.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@

package org.springframework.cloud.gcp.data.spanner.repository.query;

import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;

Expand All @@ -30,6 +35,7 @@

import org.springframework.cloud.gcp.data.spanner.core.SpannerPageableQueryOptions;
import org.springframework.cloud.gcp.data.spanner.core.SpannerTemplate;
import org.springframework.cloud.gcp.data.spanner.core.convert.ConversionUtils;
import org.springframework.cloud.gcp.data.spanner.core.convert.ConverterAwareMappingSpannerEntityWriter;
import org.springframework.cloud.gcp.data.spanner.core.convert.SpannerCustomConverter;
import org.springframework.cloud.gcp.data.spanner.core.mapping.SpannerDataException;
Expand Down Expand Up @@ -63,19 +69,32 @@ private SpannerStatementQueryExecutor() {
* @param type the type of the underlying entity
* @param tree the parsed metadata of the query
* @param params the parameters of this specific query
* @param queryMethodParamsMetadata parameter metadata from Query Method
* @param spannerTemplate used to execute the query
* @param spannerMappingContext used to get metadata about the entity type
* @param <T> the type of the underlying entity
* @return list of entities.
*/
public static <T> List<T> executeQuery(Class<T> type, PartTree tree, Object[] params,
Parameter[] queryMethodParamsMetadata,
SpannerTemplate spannerTemplate,
SpannerMappingContext spannerMappingContext) {
Pair<String, List<String>> sqlAndTags = buildPartTreeSqlString(tree,
spannerMappingContext, type);
Map<String, Parameter> paramMetadataMap = preparePartTreeSqlTagParameterMap(queryMethodParamsMetadata,
sqlAndTags);
return spannerTemplate.query(type, buildStatementFromSqlWithArgs(
sqlAndTags.getFirst(), sqlAndTags.getSecond(), null,
spannerTemplate.getSpannerEntityProcessor().getWriteConverter(), params), null);
spannerTemplate.getSpannerEntityProcessor().getWriteConverter(), params, paramMetadataMap), null);
}

private static Map<String, Parameter> preparePartTreeSqlTagParameterMap(Parameter[] queryMethodParamsMetadata,
Pair<String, List<String>> sqlAndTags) {
Map<String, Parameter> paramMetadataMap = new HashMap<>();
for (int i = 0; i < queryMethodParamsMetadata.length; i++) {
paramMetadataMap.put(sqlAndTags.getSecond().get(i), queryMethodParamsMetadata[i]);
}
return paramMetadataMap;
}

/**
Expand All @@ -85,20 +104,23 @@ public static <T> List<T> executeQuery(Class<T> type, PartTree tree, Object[] pa
* @param type the type of the underlying entity
* @param tree the parsed metadata of the query
* @param params the parameters of this specific query
* @param queryMethodParamsMetadata parameter metadata from Query Method
* @param spannerTemplate used to execute the query
* @param spannerMappingContext used to get metadata about the entity type
* @param <A> the type to which to convert Struct params
* @param <T> the type of the underlying entity on which to query
* @return list of objects mapped using the given function.
*/
public static <A, T> List<A> executeQuery(Function<Struct, A> rowFunc, Class<T> type,
PartTree tree, Object[] params, SpannerTemplate spannerTemplate,
PartTree tree, Object[] params, Parameter[] queryMethodParamsMetadata, SpannerTemplate spannerTemplate,
SpannerMappingContext spannerMappingContext) {
Pair<String, List<String>> sqlAndTags = buildPartTreeSqlString(tree,
spannerMappingContext, type);
Map<String, Parameter> paramMetadataMap = preparePartTreeSqlTagParameterMap(queryMethodParamsMetadata,
sqlAndTags);
return spannerTemplate.query(rowFunc, buildStatementFromSqlWithArgs(
sqlAndTags.getFirst(), sqlAndTags.getSecond(), null,
spannerTemplate.getSpannerEntityProcessor().getWriteConverter(), params), null);
spannerTemplate.getSpannerEntityProcessor().getWriteConverter(), params, paramMetadataMap), null);
}

/**
Expand Down Expand Up @@ -161,7 +183,7 @@ public static <T> Statement getChildrenRowsQuery(Key parentKey,
tagNum++;
}
return buildStatementFromSqlWithArgs(sb.toString() + sj.toString(), tags, null, null,
keyParts.toArray());
keyParts.toArray(), null);
}

/**
Expand All @@ -175,14 +197,15 @@ public static <T> Statement getChildrenRowsQuery(Key parentKey,
* Spanner native types. if {@code null} then this conversion is not attempted.
* @param params the parameters to substitute the tags. The ordering must be the same as
* the tags.
* @param queryMethodParams the parameter metadata from Query Method if available.
* @return an SQL statement ready to use with Spanner.
* @throws IllegalArgumentException if the number of tags does not match the number of
* params, or if a param of an unsupported type is given.
*/
@SuppressWarnings("unchecked")
public static Statement buildStatementFromSqlWithArgs(String sql, List<String> tags,
Function<Object, Struct> paramStructConvertFunc, SpannerCustomConverter spannerCustomConverter,
Object[] params) {
Object[] params, Map<String, Parameter> queryMethodParams) {
if (tags == null && params == null) {
return Statement.of(sql);
}
Expand All @@ -193,16 +216,45 @@ public static Statement buildStatementFromSqlWithArgs(String sql, List<String> t
Statement.Builder builder = Statement.newBuilder(sql);
for (int i = 0; i < tags.size(); i++) {
bindParameter(builder.bind(tags.get(i)), paramStructConvertFunc, spannerCustomConverter,
params[i]);
params[i], queryMethodParams == null ? null : queryMethodParams.get(tags.get(i)));
}
return builder.build();
}

private static void bindArrayParameter(ValueBinder<Statement.Builder> bind,
SpannerCustomConverter spannerCustomConverter,
Iterable originalParam, Parameter paramMetadata) {
Iterable param = originalParam;
Class innerType = (Class) ((ParameterizedType) paramMetadata.getParameterizedType())
.getActualTypeArguments()[0];
BiConsumer<ValueBinder<?>, Iterable> toMethod = getIterableValueBinderBiConsumer(innerType);

// attempt conversion of elements inside
if (toMethod == null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this logic be in ConverterAwareMappingSpannerEntityWriter?

Copy link
Contributor Author

@ChengyuanZhao ChengyuanZhao Jun 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EDIT:

actually yea I think most of this already is in ConverterAwareMappingSpannerEntityWriter. let me rework it a bit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dmitry-s actually great idea. I refactored . PTAL!

Class<?> compatible = ConverterAwareMappingSpannerEntityWriter
.findFirstCompatibleSpannerSingleItemNativeType(
(type) -> spannerCustomConverter.canConvert(innerType, type));
if (compatible == null) {
throw new IllegalArgumentException("Could not convert to an ARRAY of compatible type: " + innerType);
}
param = new ArrayList();
for (Object item : originalParam) {
((ArrayList) param).add(spannerCustomConverter.convert(item, compatible));
}
toMethod = getIterableValueBinderBiConsumer(compatible);
}
toMethod.accept(bind, param);
}

private static void bindParameter(ValueBinder<Statement.Builder> bind,
Function<Object, Struct> paramStructConvertFunc, SpannerCustomConverter spannerCustomConverter,
Object originalParam) {
Object originalParam, Parameter paramMetadata) {
if (ConversionUtils.isIterableNonByteArrayType(originalParam.getClass())) {
bindArrayParameter(bind, spannerCustomConverter, (Iterable) originalParam, paramMetadata);
return;
}
Object param = originalParam;
BiFunction<ValueBinder, Object, ?> toMethod = (BiFunction<ValueBinder, Object, ?>) getValueBinderBiFunction(
BiFunction<ValueBinder, Object, ?> toMethod = (BiFunction<ValueBinder, Object, ?>) getSingleValueBinderBiFunction(
param);
// the param is not a native Cloud Spanner type
if (toMethod == null && spannerCustomConverter != null) {
Expand All @@ -211,7 +263,7 @@ private static void bindParameter(ValueBinder<Statement.Builder> bind,
(type) -> spannerCustomConverter.canConvert(originalParam.getClass(), type));
if (compatible != null) {
param = spannerCustomConverter.convert(originalParam, compatible);
toMethod = (BiFunction<ValueBinder, Object, ?>) getValueBinderBiFunction(param);
toMethod = (BiFunction<ValueBinder, Object, ?>) getSingleValueBinderBiFunction(param);
}
}
// could not be converted, attempting to use it as a struct
Expand Down Expand Up @@ -239,7 +291,7 @@ public static String getColumnsStringForSelect(
return String.join(" , ", spannerPersistentEntity.columns());
}

private static BiFunction<ValueBinder, ?, ?> getValueBinderBiFunction(Object param) {
private static BiFunction<ValueBinder, ?, ?> getSingleValueBinderBiFunction(Object param) {
if (Struct.class.isAssignableFrom(param.getClass())) {
return ConverterAwareMappingSpannerEntityWriter.singleItemTypeValueBinderMethodMap.get(Struct.class);
}
Expand All @@ -254,6 +306,10 @@ else if (param.getClass().isEnum()) {
}
}

private static BiConsumer<ValueBinder<?>, Iterable> getIterableValueBinderBiConsumer(Class innerType) {
return ConverterAwareMappingSpannerEntityWriter.iterablePropertyTypeToMethodMap.get(innerType);
}

private static Pair<String, List<String>> buildPartTreeSqlString(PartTree tree,
SpannerMappingContext spannerMappingContext, Class type) {

Expand Down Expand Up @@ -394,6 +450,12 @@ else if (part.shouldIgnoreCase() != IgnoreCaseType.NEVER) {
case GREATER_THAN_EQUAL:
andString += ">=" + insertedTag;
break;
case IN:
andString += " IN UNNEST(" + insertedTag + ")";
break;
case NOT_IN:
andString += " NOT IN UNNEST(" + insertedTag + ")";
break;
default:
throw new UnsupportedOperationException("The statement type: "
+ part.getType() + " is not supported.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.springframework.cloud.gcp.data.spanner.core.mapping.SpannerPersistentEntity;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.query.Param;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
Expand Down Expand Up @@ -261,10 +262,15 @@ private List executeReadSql(Pageable pageable, Sort sort, QueryTagValue queryTag
}

private Statement buildStatementFromQueryAndTags(QueryTagValue queryTagValue) {
Map<String, java.lang.reflect.Parameter> paramMetadataMap = new HashMap<>();
for (java.lang.reflect.Parameter param : getQueryMethod().getMethod().getParameters()) {
Param annotation = param.getAnnotation(Param.class);
paramMetadataMap.put(annotation == null ? param.getName() : annotation.value(), param);
}
return SpannerStatementQueryExecutor.buildStatementFromSqlWithArgs(
queryTagValue.sql, queryTagValue.tags,
this.paramStructConvertFunc, null,
queryTagValue.params.toArray());
queryTagValue.params.toArray(), paramMetadataMap);
}

private Expression[] detectExpressions(String sql) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ else if (ConversionUtils.isIterableNonByteArrayType(paramType)) {
Class<?> innerParamType = (Class) ((ParameterizedType) method
.getGenericParameterTypes()[0]).getActualTypeArguments()[0];
assertThat(
ConverterAwareMappingSpannerEntityWriter.iterablePropertyType2ToMethodMap.keySet())
ConverterAwareMappingSpannerEntityWriter.iterablePropertyTypeToMethodMap.keySet())
.contains(innerParamType);
}
else {
Expand Down
Loading