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 all 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
7 changes: 7 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 All @@ -1057,6 +1059,8 @@ List<Trade> findBySymbolContains(String symbolFragment);
----
The param `symbolFragment` is a https://cloud.google.com/spanner/docs/functions-and-operators#regexp_contains[regular expression] that is checked for occurrences.

The `In` and `NotIn` keywords must be used with `Iterable` corresponding parameters.

Delete queries are also supported.
For example, query methods such as `deleteByAction` or `removeByAction` delete entities found by `findByAction`.
The delete operation happens in a single transaction.
Expand Down Expand Up @@ -1177,6 +1181,9 @@ SpEL can also be used to provide SQL parameters:
List<Trade> fetchByActionNamedQuery(String action, Double priceRadius);
----

When using the `IN` SQL clause, remember to use `IN UNNEST(@iterableParam)` to specify a single `Iterable` parameter.
You can also use a fixed number of singular parameters such as `IN (@stringParam1, @stringParam2)`.

==== Projections
Spring Data Spanner supports {spring-data-commons-ref}/#projections[projections].
You can define projection interfaces based on domain types and add query methods that return them in your repository:
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 Expand Up @@ -545,7 +545,7 @@ private void resolveChildEntity(Object entity, Set<String> includeProperties) {
Supplier<List> getChildrenEntitiesFunc = () -> queryAndResolveChildren(childType,
SpannerStatementQueryExecutor.getChildrenRowsQuery(
this.spannerSchemaUtils.getKey(entity),
childPersistentEntity),
childPersistentEntity, this.spannerEntityProcessor.getWriteConverter()),
null);

accessor.setProperty(spannerPersistentProperty,
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 @@ -66,7 +66,7 @@ public class ConverterAwareMappingSpannerEntityWriter implements SpannerEntityWr
public static final Map<Class<?>, BiFunction<ValueBinder, ?, ?>> singleItemTypeValueBinderMethodMap;

static final Map<Class<?>, BiConsumer<ValueBinder<?>, Iterable>>
iterablePropertyType2ToMethodMap = createIterableTypeMapping();
iterablePropertyTypeToMethodMap = createIterableTypeMapping();

@SuppressWarnings("unchecked")
private static Map<Class<?>, BiConsumer<ValueBinder<?>, Iterable>> createIterableTypeMapping() {
Expand Down Expand Up @@ -123,8 +123,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 @@ -193,6 +193,81 @@ public SpannerWriteConverter getSpannerWriteConverter() {
return this.writeConverter;
}

/**
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 Don't be alarmed by these big new sections .They're just moved up because now they're public.

* Bind an iterable value to a ValueBinder.
*
* @param value the value to bind.
* @param valueBinder the binder that accepts the value.
* @param writeConverter the converter to use to convert the values.
* @param innerType the type of the items in the iterable.
* @return {@code true} if the binding was successful.
*/
public static boolean attemptSetIterableValueOnBinder(Iterable<Object> value, ValueBinder valueBinder,
SpannerCustomConverter writeConverter, Class innerType) {
boolean valueSet = false;
// attempt check if there is directly a write method that can accept the
// property
if (iterablePropertyTypeToMethodMap.containsKey(innerType)) {
iterablePropertyTypeToMethodMap.get(innerType).accept(valueBinder,
value);
valueSet = true;
}

// Finally find any compatible conversion
if (!valueSet) {
for (Class<?> targetType : iterablePropertyTypeToMethodMap.keySet()) {
valueSet = attemptSetIterablePropertyWithTypeConversion(value, valueBinder,
innerType, targetType, writeConverter);
if (valueSet) {
break;
}
}

}
return valueSet;
}

/**
* Bind a value to a ValueBinder.
*
* @param propertyValue the value to bind.
* @param propertyType the type of the value to bind.
* @param valueBinder the binder.
* @param spannerCustomConverter the converter used to convert if necessary.
* @return {@code true} if the value was bound successfully.
*/
public static boolean attemptBindSingleValue(Object propertyValue, Class<?> propertyType, ValueBinder valueBinder,
SpannerCustomConverter spannerCustomConverter) {
// directly try to set using the property's original Java type
boolean valueSet = attemptSetSingleItemValue(propertyValue, propertyType,
valueBinder, propertyType, spannerCustomConverter);

// Finally try and find any conversion that works
if (!valueSet) {
for (Class<?> targetType : singleItemTypeValueBinderMethodMap.keySet()) {
valueSet = attemptSetSingleItemValue(propertyValue, propertyType,
valueBinder, targetType, spannerCustomConverter);
if (valueSet) {
break;
}
}
}
return valueSet;
}

private static boolean attemptSetIterablePropertyWithTypeConversion(Iterable<Object> value,
ValueBinder<WriteBuilder> valueBinder, Class innerType, Class<?> targetType,
SpannerCustomConverter writeConverter) {
if (writeConverter.canConvert(innerType, targetType)) {
BiConsumer<ValueBinder<?>, Iterable> toMethod = iterablePropertyTypeToMethodMap
.get(targetType);
toMethod.accept(valueBinder,
(value != null) ? ConversionUtils.convertIterable(value, targetType, writeConverter) : null);
return true;
}
return false;
}

private Object convertKeyPart(Object object) {

if (object == null || isValidSpannerKeyType(ConversionUtils.boxIfNeeded(object.getClass()))) {
Expand Down Expand Up @@ -221,6 +296,51 @@ private boolean isValidSpannerKeyType(Class type) {

// @formatter:off

private static boolean attemptSetIterableValue(Iterable<Object> value,
ValueBinder<WriteBuilder> valueBinder,
SpannerPersistentProperty spannerPersistentProperty, SpannerCustomConverter writeConverter) {

Class innerType = ConversionUtils.boxIfNeeded(spannerPersistentProperty.getColumnInnerType());
if (innerType == null) {
return false;
}

boolean valueSet = false;

// use the annotated column type if possible.
if (spannerPersistentProperty.getAnnotatedColumnItemType() != null) {
valueSet = attemptSetIterablePropertyWithTypeConversion(value, valueBinder, innerType,
SpannerTypeMapper.getSimpleJavaClassFor(
spannerPersistentProperty.getAnnotatedColumnItemType()),
writeConverter);
}
else {
if (!valueSet) {
valueSet = attemptSetIterableValueOnBinder(value, valueBinder, writeConverter, innerType);
}
}
return valueSet;
}

@SuppressWarnings("unchecked")
private static <T> boolean attemptSetSingleItemValue(Object value, Class<?> sourceType,
ValueBinder<WriteBuilder> valueBinder, Class<T> targetType, SpannerCustomConverter writeConverter) {
if (!writeConverter.canConvert(sourceType, targetType)) {
return false;
}
Class innerType = ConversionUtils.boxIfNeeded(targetType);
BiFunction<ValueBinder, T, ?> toMethod = (BiFunction<ValueBinder, T, ?>) singleItemTypeValueBinderMethodMap
.get(innerType);
if (toMethod == null) {
return false;
}
// We're just checking for the bind to have succeeded, we don't need to chain the result.
// Spanner allows binding of null values.
Object ignored = toMethod.apply(valueBinder,
(value != null) ? writeConverter.convert(value, targetType) : null);
return true;
}

/**
* <p>
* For each property this method "set"s the column name and finds the corresponding "to"
Expand Down Expand Up @@ -264,7 +384,7 @@ private void writeProperty(MultipleValueBinder sink,
*/
if (ConversionUtils.isIterableNonByteArrayType(propertyType)) {
valueSet = attemptSetIterableValue((Iterable<Object>) propertyValue, valueBinder,
property);
property, this.writeConverter);
}
else {

Expand All @@ -273,30 +393,18 @@ private void writeProperty(MultipleValueBinder sink,
// time
if (property.isCommitTimestamp()) {
valueSet = attemptSetSingleItemValue(Value.COMMIT_TIMESTAMP, Timestamp.class, valueBinder,
Timestamp.class);
Timestamp.class, this.writeConverter);
}
// use the user's annotated column type if possible
else if (property.getAnnotatedColumnItemType() != null) {
valueSet = attemptSetSingleItemValue(propertyValue, propertyType,
valueBinder,
SpannerTypeMapper.getSimpleJavaClassFor(property.getAnnotatedColumnItemType()));
SpannerTypeMapper.getSimpleJavaClassFor(property.getAnnotatedColumnItemType()),
this.writeConverter);
}
else {
// directly try to set using the property's original Java type
if (!valueSet) {
valueSet = attemptSetSingleItemValue(propertyValue, propertyType,
valueBinder, propertyType);
}

// Finally try and find any conversion that works
if (!valueSet) {
for (Class<?> targetType : singleItemTypeValueBinderMethodMap.keySet()) {
valueSet = attemptSetSingleItemValue(propertyValue, propertyType,
valueBinder, targetType);
if (valueSet) {
break;
}
}
valueSet = attemptBindSingleValue(propertyValue, propertyType, valueBinder, this.writeConverter);
}
}
}
Expand All @@ -306,77 +414,4 @@ else if (property.getAnnotatedColumnItemType() != null) {
"Unsupported mapping for type: %s", propertyValue.getClass()));
}
}

private boolean attemptSetIterableValue(Iterable<Object> value,
ValueBinder<WriteBuilder> valueBinder,
SpannerPersistentProperty spannerPersistentProperty) {

Class innerType = ConversionUtils.boxIfNeeded(spannerPersistentProperty.getColumnInnerType());
if (innerType == null) {
return false;
}

boolean valueSet = false;

// use the annotated column type if possible.
if (spannerPersistentProperty.getAnnotatedColumnItemType() != null) {
valueSet = attemptSetIterablePropertyWithType(value, valueBinder, innerType,
SpannerTypeMapper.getSimpleJavaClassFor(
spannerPersistentProperty.getAnnotatedColumnItemType()));
}
else {

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

// Finally find any compatible conversion
if (!valueSet) {
for (Class<?> targetType : iterablePropertyType2ToMethodMap.keySet()) {
valueSet = attemptSetIterablePropertyWithType(value, valueBinder,
innerType, targetType);
if (valueSet) {
break;
}
}

}
}
return valueSet;
}

private boolean attemptSetIterablePropertyWithType(Iterable<Object> value,
ValueBinder<WriteBuilder> valueBinder, Class innerType, Class<?> targetType) {
if (this.writeConverter.canConvert(innerType, targetType)) {
BiConsumer<ValueBinder<?>, Iterable> toMethod = iterablePropertyType2ToMethodMap
.get(targetType);
toMethod.accept(valueBinder,
(value != null) ? ConversionUtils.convertIterable(value, targetType, this.writeConverter) : null);
return true;
}
return false;
}

@SuppressWarnings("unchecked")
private <T> boolean attemptSetSingleItemValue(Object value, Class<?> sourceType,
ValueBinder<WriteBuilder> valueBinder, Class<T> targetType) {
if (!this.writeConverter.canConvert(sourceType, targetType)) {
return false;
}
Class innerType = ConversionUtils.boxIfNeeded(targetType);
BiFunction<ValueBinder, T, ?> toMethod = (BiFunction<ValueBinder, T, ?>) singleItemTypeValueBinderMethodMap
.get(innerType);
if (toMethod == null) {
return false;
}
// We're just checking for the bind to have succeeded, we don't need to chain the result.
// Spanner allows binding of null values.
Object ignored = toMethod.apply(valueBinder,
(value != null) ? this.writeConverter.convert(value, targetType) : null);
return true;
}
}
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
Loading