From 1332253aa51d123c2b8c48cf5199f8479bce6fd1 Mon Sep 17 00:00:00 2001 From: Min Zhu Date: Wed, 18 May 2022 22:38:03 +0000 Subject: [PATCH 01/16] write ARRAY column. --- ...verterAwareMappingSpannerEntityWriter.java | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityWriter.java b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityWriter.java index 1b6aa3ef10..cf763c17ca 100644 --- a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityWriter.java +++ b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityWriter.java @@ -30,10 +30,12 @@ import com.google.cloud.spring.data.spanner.core.mapping.SpannerPersistentEntity; import com.google.cloud.spring.data.spanner.core.mapping.SpannerPersistentProperty; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.BiConsumer; @@ -308,7 +310,7 @@ private boolean isValidSpannerKeyType(Class type) { // @formatter:off - private static boolean attemptSetIterableValue( + private boolean attemptSetIterableValue( Iterable value, ValueBinder valueBinder, SpannerPersistentProperty spannerPersistentProperty, @@ -321,8 +323,13 @@ private static boolean attemptSetIterableValue( boolean valueSet = false; - // use the annotated column type if possible. - if (spannerPersistentProperty.getAnnotatedColumnItemType() != null) { + + if (spannerPersistentProperty.getAnnotatedColumnItemType() == Type.Code.JSON) { + // if column annotated with JSON, convert directly + valueBinder.toJsonArray(this.covertIterableJsonToValue(value)); + valueSet = true; + } else if (spannerPersistentProperty.getAnnotatedColumnItemType() != null) { + // use the annotated column type if possible. valueSet = attemptSetIterablePropertyWithTypeConversion( value, @@ -361,7 +368,7 @@ private static boolean attemptSetSingleItemValue( return true; } - private Value covertJsonToValue(Object value) { + private Value convertJsonToValue(Object value) { if (value == null) { return Value.json(null); } @@ -369,6 +376,12 @@ private Value covertJsonToValue(Object value) { return Value.json(jsonString); } + private Iterable covertIterableJsonToValue(Iterable value) { + List result = new ArrayList<>(); + value.forEach(item -> result.add(this.spannerMappingContext.getGson().toJson(item))); + return result; + } + /** * For each property this method "set"s the column name and finds the corresponding "to" method on * the {@link ValueBinder} interface. @@ -429,7 +442,7 @@ private void writeProperty( this.writeConverter); } else if (property.getAnnotatedColumnItemType() == Type.Code.JSON) { // annotated json column, bind directly - valueBinder.to(this.covertJsonToValue(propertyValue)); + valueBinder.to(this.convertJsonToValue(propertyValue)); valueSet = true; } else if (property.getAnnotatedColumnItemType() != null) { // use the user's annotated column type if possible From 0a5f93252e60737956b657b9951e62e7d3d1a970 Mon Sep 17 00:00:00 2001 From: Min Zhu Date: Wed, 18 May 2022 22:40:06 +0000 Subject: [PATCH 02/16] read ARRAY column. --- .../spanner/core/convert/StructAccessor.java | 25 +++++++++++++++++++ .../convert/StructPropertyValueProvider.java | 6 +++++ 2 files changed, 31 insertions(+) diff --git a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructAccessor.java b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructAccessor.java index dd0d063909..0c5447a95e 100644 --- a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructAccessor.java +++ b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructAccessor.java @@ -27,6 +27,7 @@ import com.google.cloud.spring.data.spanner.core.mapping.SpannerDataException; import com.google.gson.Gson; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -153,6 +154,30 @@ List getListValue(String colName) { return readMethod.apply(this.struct, colName); } + List getListJsonValue(String colName, Class colType) { + if (this.struct.getColumnType(colName).getCode() != Code.ARRAY) { + throw new SpannerDataException("Column is not an ARRAY type: " + colName); + } + List jsonStringList = this.struct.getJsonList(colName); + List result = new ArrayList<>(); + jsonStringList.forEach(item -> { + result.add(gson.fromJson(item, colType)); + }); + return result; + } + + public List getListJsonValue(int colIndex, Class colType) { + if (this.struct.getColumnType(colIndex).getCode() != Code.ARRAY) { + throw new SpannerDataException("Column is not an ARRAY type: " + colIndex); + } + List jsonStringList = this.struct.getJsonList(colIndex); + List result = new ArrayList<>(); + jsonStringList.forEach(item -> { + result.add(gson.fromJson(item, colType)); + }); + return result; + } + boolean hasColumn(String columnName) { return this.columnNamesIndex.contains(columnName); } diff --git a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructPropertyValueProvider.java b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructPropertyValueProvider.java index 364629a3ee..7fa0b68f53 100644 --- a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructPropertyValueProvider.java +++ b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructPropertyValueProvider.java @@ -125,6 +125,12 @@ private T readSingleWithConversion(SpannerPersistentProperty spannerPersiste private Iterable readIterableWithConversion( SpannerPersistentProperty spannerPersistentProperty) { String colName = spannerPersistentProperty.getColumnName(); + Type.Code spannerColumnType = spannerPersistentProperty.getAnnotatedColumnItemType(); + if (spannerColumnType == Type.Code.JSON) { + List value = (List) this.structAccessor.getListJsonValue(colName, + spannerPersistentProperty.getColumnInnerType()); + return value; + } List listValue = this.structAccessor.getListValue(colName); return listValue.stream() .map(item -> convertOrRead((Class) spannerPersistentProperty.getColumnInnerType(), item)) From fa4edc3ce29ceb0c5439c0227f1ba35011b8e55d Mon Sep 17 00:00:00 2001 From: Min Zhu Date: Wed, 18 May 2022 22:41:01 +0000 Subject: [PATCH 03/16] deal with gql querey when return is ARRAY column. --- .../mapping/SpannerPersistentEntityImpl.java | 15 ++++++++++++++- .../repository/query/SqlSpannerQuery.java | 18 +++++++++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/mapping/SpannerPersistentEntityImpl.java b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/mapping/SpannerPersistentEntityImpl.java index 67f3f05ebd..1758f0618a 100644 --- a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/mapping/SpannerPersistentEntityImpl.java +++ b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/mapping/SpannerPersistentEntityImpl.java @@ -91,6 +91,8 @@ public class SpannerPersistentEntityImpl private final Set> jsonProperties = new HashSet<>(); + private final Set> arrayJsonProperties = new HashSet<>(); + /** * Creates a {@link SpannerPersistentEntityImpl}. * @@ -188,7 +190,9 @@ public void addPersistentProperty(SpannerPersistentProperty property) { }); } - if (property.getAnnotatedColumnItemType() == Type.Code.JSON) { + if (property.getAnnotatedColumnItemType() == Type.Code.JSON && property.isCollectionLike()) { + this.arrayJsonProperties.add(property.getColumnInnerType()); + } else if (property.getAnnotatedColumnItemType() == Type.Code.JSON) { this.jsonProperties.add(property.getType()); } } @@ -423,6 +427,15 @@ public boolean isJsonProperty(Class type) { return this.jsonProperties.contains(type); } + /** + * Lookup whether a class is an innertype of a JSONARRAY entity property + * @param type + * @return + */ + public boolean isArrayJsonProperty(Class type) { + return this.arrayJsonProperties.contains(type); + } + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.context.addPropertyAccessor(new BeanFactoryAccessor()); this.context.setBeanResolver(new BeanFactoryResolver(applicationContext)); diff --git a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQuery.java b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQuery.java index be1c9ca5d5..bd5bdd77ef 100644 --- a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQuery.java +++ b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQuery.java @@ -47,6 +47,7 @@ import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.util.Pair; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.ParserContext; @@ -241,8 +242,14 @@ private List executeReadSql(Pageable pageable, Sort sort, QueryTagValue queryTag struct -> new StructAccessor(struct).getSingleValue(0), statement, spannerQueryOptions); } // check if returnedType is a field annotated as json - boolean isJsonField = isJsonFieldType(returnedType); - if (isJsonField) { + Pair isJsonField = isJsonFieldType(returnedType); + if (isJsonField.getSecond()) { + return this.spannerTemplate.query( + struct -> new StructAccessor(struct, + this.spannerMappingContext.getGson()).getListJsonValue(0, returnedType), + statement, + spannerQueryOptions); + } else if (isJsonField.getFirst()) { return this.spannerTemplate.query( struct -> new StructAccessor(struct, this.spannerMappingContext.getGson()).getSingleJsonValue(0, returnedType), statement, @@ -252,14 +259,15 @@ private List executeReadSql(Pageable pageable, Sort sort, QueryTagValue queryTag return this.spannerTemplate.query(this.entityType, statement, spannerQueryOptions); } - private boolean isJsonFieldType(Class returnedType) { + private Pair isJsonFieldType(Class returnedType) { SpannerPersistentEntityImpl persistentEntity = (SpannerPersistentEntityImpl) this.spannerMappingContext.getPersistentEntity(this.entityType); if (persistentEntity == null) { - return false; + return Pair.of(false, false); } - return persistentEntity.isJsonProperty(returnedType); + return Pair.of(persistentEntity.isJsonProperty(returnedType), + persistentEntity.isArrayJsonProperty(returnedType)); } private Statement buildStatementFromQueryAndTags(QueryTagValue queryTagValue) { From 77c2256eadb7c48690f508b4d2fc2e1432138937 Mon Sep 17 00:00:00 2001 From: Min Zhu Date: Thu, 2 Jun 2022 18:39:59 +0000 Subject: [PATCH 04/16] add null check for write and write tests. --- ...verterAwareMappingSpannerEntityWriter.java | 3 ++ ...rAwareMappingSpannerEntityWriterTests.java | 41 +++++++++++++++++++ .../spanner/core/convert/TestEntities.java | 16 +++++++- 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityWriter.java b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityWriter.java index cf763c17ca..9f995eb6bc 100644 --- a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityWriter.java +++ b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityWriter.java @@ -378,6 +378,9 @@ private Value convertJsonToValue(Object value) { private Iterable covertIterableJsonToValue(Iterable value) { List result = new ArrayList<>(); + if (value == null) { + return null; + } value.forEach(item -> result.add(this.spannerMappingContext.getGson().toJson(item))); return result; } diff --git a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityWriterTests.java b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityWriterTests.java index 7435024ca5..9ee849d8d6 100644 --- a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityWriterTests.java +++ b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityWriterTests.java @@ -363,6 +363,47 @@ void writeNullJsonTest() { verify(valueBinder).to(Value.json(null)); } + @Test + void writeJsonArrayTest() { + TestEntities.Params parameters = new TestEntities.Params("some value", "some other value"); + TestEntities.TestEntityJsonArray testEntity = new TestEntities.TestEntityJsonArray("id1", Arrays.asList(parameters, parameters)); + + WriteBuilder writeBuilder = mock(WriteBuilder.class); + ValueBinder valueBinder = mock(ValueBinder.class); + + when(writeBuilder.set("id")).thenReturn(valueBinder); + when(writeBuilder.set("paramsList")).thenReturn(valueBinder); + + this.spannerEntityWriter.write(testEntity, writeBuilder::set); + + List stringList = new ArrayList<>(); + stringList.add("{\"p1\":\"some value\",\"p2\":\"some other value\"}"); + stringList.add("{\"p1\":\"some value\",\"p2\":\"some other value\"}"); + + verify(valueBinder).to(testEntity.id); + verify(valueBinder).toJsonArray(stringList); + } + + @Test + void writeNullEmptyJsonArrayTest() { + TestEntities.TestEntityJsonArray testNull = new TestEntities.TestEntityJsonArray("id1", null); + TestEntities.TestEntityJsonArray testEmpty = new TestEntities.TestEntityJsonArray("id2", new ArrayList<>()); + + WriteBuilder writeBuilder = mock(WriteBuilder.class); + ValueBinder valueBinder = mock(ValueBinder.class); + + when(writeBuilder.set("id")).thenReturn(valueBinder); + when(writeBuilder.set("paramsList")).thenReturn(valueBinder); + + this.spannerEntityWriter.write(testNull, writeBuilder::set); + this.spannerEntityWriter.write(testEmpty, writeBuilder::set); + + verify(valueBinder).to(testNull.id); + verify(valueBinder).toJsonArray(isNull()); + verify(valueBinder).to(testEmpty.id); + verify(valueBinder).toJsonArray(new ArrayList<>()); + } + @Test void writeUnsupportedTypeIterableTest() { diff --git a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/TestEntities.java b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/TestEntities.java index 1289193888..9354479923 100644 --- a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/TestEntities.java +++ b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/TestEntities.java @@ -243,7 +243,7 @@ static class PartialConstructor { } /** A test class with Json field. */ - @Table(name = "custom_test_table") + @Table(name = "json_test_table") static class TestEntityJson { @PrimaryKey String id; @@ -256,6 +256,20 @@ static class TestEntityJson { } } + /** A test class with Json Array field. */ + @Table(name = "jsonarray_test_table2") + static class TestEntityJsonArray { + @PrimaryKey String id; + + @Column(spannerType = TypeCode.JSON) + List paramsList; + + TestEntityJsonArray(String id, List paramsList) { + this.id = id; + this.paramsList = paramsList; + } + } + static class Params { String p1; From 404e5e41b9c553d42e053f533918e097cb3a9f63 Mon Sep 17 00:00:00 2001 From: Min Zhu Date: Thu, 2 Jun 2022 19:34:13 +0000 Subject: [PATCH 05/16] add reader test. --- ...rAwareMappingSpannerEntityReaderTests.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityReaderTests.java b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityReaderTests.java index d571b07884..cd21907ad7 100644 --- a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityReaderTests.java +++ b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityReaderTests.java @@ -393,4 +393,35 @@ void readJsonFieldTest() { assertThat(result.params.p1).isEqualTo("address line"); assertThat(result.params.p2).isEqualTo("5"); } + + @Test + void readArrayJsonFieldTest() { + Struct row = mock(Struct.class); + when(row.getString("id")).thenReturn("1234"); + when(row.getType()) + .thenReturn( + Type.struct( + Arrays.asList( + Type.StructField.of("id", Type.string()), + Type.StructField.of("paramsList", Type.array(Type.json()))))); + when(row.getColumnType("id")).thenReturn(Type.string()); + + when(row.getColumnType("paramsList")).thenReturn(Type.array(Type.json())); + when(row.getJsonList("paramsList")).thenReturn( + Arrays.asList("{\"p1\":\"address line\",\"p2\":\"5\"}", + "{\"p1\":\"address line 2\",\"p2\":\"6\"}", null)); + + TestEntities.TestEntityJsonArray result = + this.spannerEntityReader.read(TestEntities.TestEntityJsonArray.class, row); + + assertThat(result.id).isEqualTo("1234"); + + assertThat(result.paramsList.get(0).p1).isEqualTo("address line"); + assertThat(result.paramsList.get(0).p2).isEqualTo("5"); + + assertThat(result.paramsList.get(1).p1).isEqualTo("address line 2"); + assertThat(result.paramsList.get(1).p2).isEqualTo("6"); + + assertThat(result.paramsList.get(2)).isNull(); + } } From ca708028a6459cc483f2a423f76bc337971bf4ac Mon Sep 17 00:00:00 2001 From: Min Zhu Date: Thu, 2 Jun 2022 20:44:52 +0000 Subject: [PATCH 06/16] for sqlSpannerQuery, re-consider and identify array or not in StructAccessor. This is to avoid conflict of class A and List annotated with Json at same time. --- .../spanner/core/convert/StructAccessor.java | 11 +++++++++-- .../mapping/SpannerPersistentEntityImpl.java | 14 ++------------ .../repository/query/SqlSpannerQuery.java | 19 ++++++------------- 3 files changed, 17 insertions(+), 27 deletions(-) diff --git a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructAccessor.java b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructAccessor.java index 0c5447a95e..95c9b50e0b 100644 --- a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructAccessor.java +++ b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructAccessor.java @@ -166,7 +166,14 @@ List getListJsonValue(String colName, Class colType) { return result; } - public List getListJsonValue(int colIndex, Class colType) { + public Object getJsonValue(int colIndex, Class colType) { + if (this.struct.getColumnType(colIndex).getCode() != Code.ARRAY) { + return getSingleJsonValue(colIndex, colType); + } + return getListJsonValue(colIndex, colType); + } + + private List getListJsonValue(int colIndex, Class colType) { if (this.struct.getColumnType(colIndex).getCode() != Code.ARRAY) { throw new SpannerDataException("Column is not an ARRAY type: " + colIndex); } @@ -209,7 +216,7 @@ T getSingleJsonValue(String colName, Class colType) { return gson.fromJson(jsonString, colType); } - public T getSingleJsonValue(int colIndex, Class colType) { + private T getSingleJsonValue(int colIndex, Class colType) { if (this.struct.getColumnType(colIndex).getCode() != Code.JSON) { throw new SpannerDataException("Column of index " + colIndex + " not an JSON type."); } diff --git a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/mapping/SpannerPersistentEntityImpl.java b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/mapping/SpannerPersistentEntityImpl.java index 1758f0618a..5e4293e555 100644 --- a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/mapping/SpannerPersistentEntityImpl.java +++ b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/mapping/SpannerPersistentEntityImpl.java @@ -91,8 +91,6 @@ public class SpannerPersistentEntityImpl private final Set> jsonProperties = new HashSet<>(); - private final Set> arrayJsonProperties = new HashSet<>(); - /** * Creates a {@link SpannerPersistentEntityImpl}. * @@ -191,7 +189,7 @@ public void addPersistentProperty(SpannerPersistentProperty property) { } if (property.getAnnotatedColumnItemType() == Type.Code.JSON && property.isCollectionLike()) { - this.arrayJsonProperties.add(property.getColumnInnerType()); + this.jsonProperties.add(property.getColumnInnerType()); } else if (property.getAnnotatedColumnItemType() == Type.Code.JSON) { this.jsonProperties.add(property.getType()); } @@ -423,19 +421,11 @@ public Set columns() { } // Lookup whether a particular class is a JSON entity property + // or is an inner type of a ARRAY property public boolean isJsonProperty(Class type) { return this.jsonProperties.contains(type); } - /** - * Lookup whether a class is an innertype of a JSONARRAY entity property - * @param type - * @return - */ - public boolean isArrayJsonProperty(Class type) { - return this.arrayJsonProperties.contains(type); - } - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.context.addPropertyAccessor(new BeanFactoryAccessor()); this.context.setBeanResolver(new BeanFactoryResolver(applicationContext)); diff --git a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQuery.java b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQuery.java index bd5bdd77ef..2616332aae 100644 --- a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQuery.java +++ b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQuery.java @@ -241,17 +241,11 @@ private List executeReadSql(Pageable pageable, Sort sort, QueryTagValue queryTag return this.spannerTemplate.query( struct -> new StructAccessor(struct).getSingleValue(0), statement, spannerQueryOptions); } - // check if returnedType is a field annotated as json - Pair isJsonField = isJsonFieldType(returnedType); - if (isJsonField.getSecond()) { + // check if returnedType is a field annotated as json or is inner-type of a field annotated as json + if (isJsonFieldType(returnedType)) { return this.spannerTemplate.query( struct -> new StructAccessor(struct, - this.spannerMappingContext.getGson()).getListJsonValue(0, returnedType), - statement, - spannerQueryOptions); - } else if (isJsonField.getFirst()) { - return this.spannerTemplate.query( - struct -> new StructAccessor(struct, this.spannerMappingContext.getGson()).getSingleJsonValue(0, returnedType), + this.spannerMappingContext.getGson()).getJsonValue(0, returnedType), statement, spannerQueryOptions); } @@ -259,15 +253,14 @@ private List executeReadSql(Pageable pageable, Sort sort, QueryTagValue queryTag return this.spannerTemplate.query(this.entityType, statement, spannerQueryOptions); } - private Pair isJsonFieldType(Class returnedType) { + private boolean isJsonFieldType(Class returnedType) { SpannerPersistentEntityImpl persistentEntity = (SpannerPersistentEntityImpl) this.spannerMappingContext.getPersistentEntity(this.entityType); if (persistentEntity == null) { - return Pair.of(false, false); + return false; } - return Pair.of(persistentEntity.isJsonProperty(returnedType), - persistentEntity.isArrayJsonProperty(returnedType)); + return persistentEntity.isJsonProperty(returnedType); } private Statement buildStatementFromQueryAndTags(QueryTagValue queryTagValue) { From 91c83275eab14b556370b91e5965509b20041b50 Mon Sep 17 00:00:00 2001 From: Min Zhu Date: Thu, 2 Jun 2022 21:15:15 +0000 Subject: [PATCH 07/16] added sqlSpannerQueryTest for ARRAY return type. --- .../query/SqlSpannerQueryTests.java | 90 ++++++++++++++++++- 1 file changed, 88 insertions(+), 2 deletions(-) diff --git a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQueryTests.java b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQueryTests.java index fafe4dcf2e..864741d928 100644 --- a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQueryTests.java +++ b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQueryTests.java @@ -48,6 +48,7 @@ import com.google.cloud.spring.data.spanner.core.mapping.SpannerMappingContext; import com.google.cloud.spring.data.spanner.core.mapping.Table; import com.google.cloud.spring.data.spanner.core.mapping.Where; +import com.google.common.base.Objects; import com.google.gson.Gson; import com.google.spanner.v1.TypeCode; import java.lang.reflect.Method; @@ -56,10 +57,8 @@ import java.util.Map; import java.util.function.Function; import org.assertj.core.data.Offset; -import org.junit.Rule; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.rules.ExpectedException; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.springframework.data.domain.PageRequest; @@ -654,6 +653,7 @@ void sqlReturnTypeIsJsonFieldTest() throws NoSuchMethodException { .thenReturn(Type.struct(Arrays.asList(Type.StructField.of("details", Type.json())))); when(row.getColumnType(0)).thenReturn(Type.json()); when(row.getJson(0)).thenReturn("{\"p1\":\"address line\",\"p2\":\"5\"}"); + when(row.getColumnType("detailsList")).thenReturn(Type.array(Type.json())); Object result = rowFunc.apply(row); @@ -662,6 +662,71 @@ void sqlReturnTypeIsJsonFieldTest() throws NoSuchMethodException { assertThat(((Detail) result).p2).isEqualTo("5"); } + @Test + void sqlReturnTypeIsArrayJsonFieldTest() throws NoSuchMethodException { + String sql = "SELECT detailsList from singer where stageName = @stageName"; + + Object[] params = new Object[]{"STAGENAME"}; + String[] paramNames = new String[]{"stageName"}; + + when(queryMethod.isCollectionQuery()).thenReturn(true); + ResultProcessor resultProcessor = mock(ResultProcessor.class); + ReturnedType returnedType = mock(ReturnedType.class); + when(this.queryMethod.getResultProcessor()).thenReturn(resultProcessor); + when(resultProcessor.getReturnedType()).thenReturn(returnedType); + when(returnedType.getReturnedType()).thenReturn((Class) Detail.class); + + EvaluationContext evaluationContext = new StandardEvaluationContext(); + + evaluationContext.setVariable(paramNames[0], params[0]); + when(this.evaluationContextProvider.getEvaluationContext(any(), any())) + .thenReturn(evaluationContext); + + SqlSpannerQuery sqlSpannerQuery = createQuery(sql, Singer.class, false); + + doAnswer( + invocation -> { + Statement statement = invocation.getArgument(1); + assertThat(statement.getSql()).isEqualTo(sql); + Map paramMap = statement.getParameters(); + assertThat(paramMap.get("stageName").getString()).isEqualTo(params[0]); + + return null; + }) + .when(this.spannerTemplate) + .query((Function) any(), any(), any()); + + // This dummy method was created so the metadata for the ARRAY param inner type is + // provided. + Method arrayParameterTriggeringMethod = + QueryHolder.class.getMethod("dummyMethod6", String.class); + when(this.queryMethod.getQueryMethod()).thenReturn(arrayParameterTriggeringMethod); + Mockito.when(this.queryMethod.getParameters()) + .thenReturn(new DefaultParameters(arrayParameterTriggeringMethod)); + + sqlSpannerQuery.execute(params); + // capturing the row function and verifying it's the correct one with mock data + ArgumentCaptor> argumentCaptor = + ArgumentCaptor.forClass(Function.class); + verify(this.spannerTemplate).query(argumentCaptor.capture(), any(), any()); + Function rowFunc = argumentCaptor.getValue(); + + Struct row = mock(Struct.class); + when(row.getType()) + .thenReturn(Type.struct( + Arrays.asList(Type.StructField.of("detailsList", Type.array(Type.json()))))); + when(row.getColumnType(0)).thenReturn(Type.array(Type.json())); + when(row.getJsonList(0)).thenReturn(Arrays.asList("{\"p1\":\"address line\",\"p2\":\"5\"}", + "{\"p1\":\"address line 2\",\"p2\":\"6\"}")); + when(row.getColumnType("detailsList")).thenReturn(Type.array(Type.json())); + + Object result = rowFunc.apply(row); + + assertThat(result).isInstanceOf(List.class); + assertThat((List) result).hasSize(2) + .containsExactly(new Detail("address line", "5"), new Detail("address line 2", "6")); + } + private static class Singer { @PrimaryKey String id; @@ -669,6 +734,9 @@ private static class Singer { @Column(spannerType = TypeCode.JSON) Detail details; + + @Column(spannerType = TypeCode.JSON) + List detailsList; } private class Detail { @@ -680,6 +748,24 @@ private class Detail { this.p1 = p1; this.p2 = p2; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Detail)) { + return false; + } + Detail detail = (Detail) o; + return Objects.equal(p1, detail.p1) + && Objects.equal(p2, detail.p2); + } + + @Override + public int hashCode() { + return Objects.hashCode(p1, p2); + } } private static class SymbolAction { From 6b5408fd9914510a06ea9f9bb146316fcaee4d84 Mon Sep 17 00:00:00 2001 From: Min Zhu Date: Fri, 3 Jun 2022 14:38:43 +0000 Subject: [PATCH 08/16] add test for create ddl string ARRAY type. --- .../data/spanner/core/admin/SpannerSchemaUtilsTests.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/admin/SpannerSchemaUtilsTests.java b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/admin/SpannerSchemaUtilsTests.java index 59a4e2a392..18cb75b291 100644 --- a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/admin/SpannerSchemaUtilsTests.java +++ b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/admin/SpannerSchemaUtilsTests.java @@ -161,6 +161,9 @@ void ddlForListOfListOfDoubles() { void createDdlForJson() { assertColumnDdl( JsonColumn.class, null, "jsonCol", Type.Code.JSON, OptionalLong.empty(), "jsonCol JSON"); + assertColumnDdl( + List.class, JsonColumn.class, "arrayJsonCol", Type.Code.JSON, OptionalLong.empty(), + "arrayJsonCol ARRAY"); } private void assertColumnDdl( From 3a18a5b881e451cae8428ae60f7a07633102c697 Mon Sep 17 00:00:00 2001 From: Min Zhu Date: Fri, 3 Jun 2022 14:45:30 +0000 Subject: [PATCH 09/16] add test for getting ARRAY innter type name. --- .../mapping/SpannerPersistentEntityImplTests.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/mapping/SpannerPersistentEntityImplTests.java b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/mapping/SpannerPersistentEntityImplTests.java index f39df8fe5e..f7278b309d 100644 --- a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/mapping/SpannerPersistentEntityImplTests.java +++ b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/mapping/SpannerPersistentEntityImplTests.java @@ -348,6 +348,12 @@ void testGetJsonPropertyName() { assertThat(entityWithNoJsonField.isJsonProperty(String.class)).isFalse(); assertThat(entityWithNoJsonField.isJsonProperty(long.class)).isFalse(); + + SpannerPersistentEntityImpl entityWithArrayJsonField = + (SpannerPersistentEntityImpl) + this.spannerMappingContext.getPersistentEntity(EntityWithArrayJsonField.class); + assertThat(entityWithArrayJsonField.isJsonProperty(JsonEntity.class)).isTrue(); + assertThat(entityWithArrayJsonField.isJsonProperty(String.class)).isFalse(); } private static class ParentInRelationship { @@ -512,5 +518,12 @@ private static class EntityWithJsonField { JsonEntity jsonField; } + private static class EntityWithArrayJsonField { + @PrimaryKey String id; + + @Column(spannerType = TypeCode.JSON) + List jsonListField; + } + private static class JsonEntity {} } From 8fd905c9f4b72f964c20a6fc516cdf6ac2da51bb Mon Sep 17 00:00:00 2001 From: Min Zhu Date: Fri, 3 Jun 2022 16:12:26 +0000 Subject: [PATCH 10/16] add to spanner template it test. --- .../it/SpannerTemplateIntegrationTests.java | 32 +++++++++++++++++++ .../data/spanner/test/domain/Trade.java | 12 +++++++ 2 files changed, 44 insertions(+) diff --git a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/it/SpannerTemplateIntegrationTests.java b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/it/SpannerTemplateIntegrationTests.java index 45f801fb26..18330cc429 100644 --- a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/it/SpannerTemplateIntegrationTests.java +++ b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/it/SpannerTemplateIntegrationTests.java @@ -133,6 +133,38 @@ void insertAndDeleteWithJsonField() { assertThat(this.spannerOperations.count(Trade.class)).isZero(); } + @Test + void insertAndDeleteWithArrayJsonField() { + + this.spannerOperations.delete(Trade.class, KeySet.all()); + assertThat(this.spannerOperations.count(Trade.class)).isZero(); + + Details details1 = new Details("abc", "def"); + Details details2 = new Details("123", "234"); + Trade trade1 = Trade.makeTrade(); + trade1.setAdditionalDetails(Arrays.asList(details1, details2)); + Trade trade2 = Trade.makeTrade(); + trade2.setAdditionalDetails(null); + + this.spannerOperations.insert(trade1); + this.spannerOperations.insert(trade2); + assertThat(this.spannerOperations.count(Trade.class)).isEqualTo(2L); + + List trades = + this.spannerOperations.queryAll(Trade.class, new SpannerPageableQueryOptions()); + + assertThat(trades).containsExactlyInAnyOrder(trade1, trade2); + + Trade retrievedTrade = + this.spannerOperations.read(Trade.class, Key.of(trade1.getId(), trade1.getTraderId())); + assertThat(retrievedTrade).isEqualTo(trade1); + assertThat(retrievedTrade.getAdditionalDetails()).isInstanceOf(List.class); + assertThat(retrievedTrade.getAdditionalDetails()).containsExactly(details1, details2); + + this.spannerOperations.deleteAll(Arrays.asList(trade1, trade2)); + assertThat(this.spannerOperations.count(Trade.class)).isZero(); + } + @Test void readWriteTransactionTest() { Trade trade = Trade.makeTrade(); diff --git a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/test/domain/Trade.java b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/test/domain/Trade.java index 8825d0d772..3e211cd337 100644 --- a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/test/domain/Trade.java +++ b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/test/domain/Trade.java @@ -78,6 +78,9 @@ public class Trade { @Column(spannerType = TypeCode.JSON) private Details backupDetails; + @Column(spannerType = TypeCode.JSON) + private List
additionalDetails; + /** * Partial constructor. Intentionally tests a field that is left null sometimes. * @@ -318,6 +321,15 @@ public void setOptionalDetails(Details optionalDetails) { this.optionalDetails = optionalDetails; } + public List
getAdditionalDetails() { + return additionalDetails; + } + + public void setAdditionalDetails( + List
additionalDetails) { + this.additionalDetails = additionalDetails; + } + public void setBackupDetails(Details backupDetails) { this.backupDetails = backupDetails; } From 5374320e91113a4035c4d79874cfc91a4befe58e Mon Sep 17 00:00:00 2001 From: Min Zhu Date: Fri, 3 Jun 2022 17:07:54 +0000 Subject: [PATCH 11/16] add repo it test. --- .../repository/query/SqlSpannerQuery.java | 1 - .../it/SpannerRepositoryIntegrationTests.java | 26 +++++++++++++++++++ .../spanner/test/domain/TradeRepository.java | 5 ++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQuery.java b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQuery.java index 2616332aae..b8d66cbaa7 100644 --- a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQuery.java +++ b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/repository/query/SqlSpannerQuery.java @@ -47,7 +47,6 @@ import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.data.util.Pair; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.ParserContext; diff --git a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/repository/it/SpannerRepositoryIntegrationTests.java b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/repository/it/SpannerRepositoryIntegrationTests.java index 5a8f548054..fcd333319d 100644 --- a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/repository/it/SpannerRepositoryIntegrationTests.java +++ b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/repository/it/SpannerRepositoryIntegrationTests.java @@ -570,6 +570,32 @@ void testWithJsonField() { assertThat(empty).isNotPresent(); } + @Test + void testWithArrayJsonField() { + Details details1 = new Details("abc", "def"); + Details details2 = new Details("123", "234"); + Trade trade1 = Trade.makeTrade(); + trade1.setAdditionalDetails(Arrays.asList(details1, details2)); + Trade trade2 = Trade.makeTrade(); + trade2.setAdditionalDetails(null); + Trade trade3 = Trade.makeTrade(); + trade3.setOptionalDetails(details1); + + this.tradeRepository.save(trade1); + this.tradeRepository.save(trade2); + this.tradeRepository.save(trade3); + + assertThat(this.tradeRepository.findAll()).contains(trade1, trade2, trade3); + + String traderId = trade1.getTraderId(); + List> detailsList = this.tradeRepository.getAdditionalDetailsById(traderId); + assertThat(detailsList.get(0)).containsExactly(details1, details2); + + String traderId3 = trade3.getTraderId(); + Optional
optionalDetails = this.tradeRepository.getOptionalDetailsById(traderId3); + assertThat(optionalDetails).isEqualTo(Optional.of(details1)); + } + @Test void testTransaction() { this.tradeRepositoryTransactionalService.testTransactionalAnnotation(2); diff --git a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/test/domain/TradeRepository.java b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/test/domain/TradeRepository.java index 810d7f79ab..357fe23fdc 100644 --- a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/test/domain/TradeRepository.java +++ b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/test/domain/TradeRepository.java @@ -142,6 +142,11 @@ public interface TradeRepository extends SpannerRepository { + " where trader_id = @trader_id") Optional
getOptionalDetailsById(@Param("trader_id") String traderId); + @Query( + "SELECT additionalDetails from :com.google.cloud.spring.data.spanner.test.domain.Trade:" + + " where trader_id = @trader_id") + List> getAdditionalDetailsById(@Param("trader_id") String traderId); + @NonNull Trade getByAction(String s); } From d23b58db4989253580627376be3aebfacfd10c45 Mon Sep 17 00:00:00 2001 From: Min Zhu Date: Fri, 3 Jun 2022 17:52:49 +0000 Subject: [PATCH 12/16] add ARRAY field to sample. --- .../com/example/SpannerRepositoryExample.java | 11 ++++++++++- .../src/main/java/com/example/Trader.java | 15 +++++++++++++++ .../SpannerRepositoryIntegrationTests.java | 15 ++++++++++----- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-repository-sample/src/main/java/com/example/SpannerRepositoryExample.java b/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-repository-sample/src/main/java/com/example/SpannerRepositoryExample.java index ccec5dd88f..963d0c6896 100644 --- a/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-repository-sample/src/main/java/com/example/SpannerRepositoryExample.java +++ b/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-repository-sample/src/main/java/com/example/SpannerRepositoryExample.java @@ -113,7 +113,7 @@ public void runExample() { LOGGER.info("Try http://localhost:8080/trades in the browser to see all trades."); LOGGER.info( - "JSON field should be annotated with \"@Column(spannerType = TypeCode.JSON)\" in data" + "JSON or ARRAY field should be annotated with \"@Column(spannerType = TypeCode.JSON)\" in data" + " class."); Trader trader1 = @@ -123,10 +123,15 @@ public void runExample() { Trader trader3 = new Trader("demo_trader_json3", "Scott", "Smith", new Address(8L, "fake address 3", false)); trader3.setHomeAddress(new Address(8L, "fake address 3 in unused detail", false)); + Trader trader4 = + new Trader("demo_trader_json4", "John", "Doe", + Arrays.asList(new Address(666L, "fake address 4", false), + new Address(777L, "fake address 5", false))); this.traderRepository.save(trader1); this.traderRepository.save(trader2); this.traderRepository.save(trader3); + this.traderRepository.save(trader4); LOGGER.info( "Find trader by Id and print out JSON field 'workAddress' as string: " @@ -136,6 +141,10 @@ public void runExample() { "Find trader by Id and print out JSON field 'unusedDetails' as string: " + this.traderRepository.findById("demo_trader_json3").get().getHomeAddress()); + LOGGER.info( + "Find trader by Id and print out ARRAY field 'addressList' as string: " + + this.traderRepository.findById("demo_trader_json4").get().getAddressList().toString()); + long count = this.traderRepository.getCountActive("true"); LOGGER.info("A query method can query on the properties of JSON values"); LOGGER.info("Count of records with workAddress.active = true is " + count + ". "); diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-repository-sample/src/main/java/com/example/Trader.java b/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-repository-sample/src/main/java/com/example/Trader.java index 860532b4e0..76218ca998 100644 --- a/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-repository-sample/src/main/java/com/example/Trader.java +++ b/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-repository-sample/src/main/java/com/example/Trader.java @@ -56,6 +56,9 @@ public class Trader { @Column(name = "home_address", spannerType = TypeCode.JSON) private Address homeAddress; + @Column(name = "address_list", spannerType = TypeCode.JSON) + private List
addressList; + public Trader() {} public Trader(String traderId, String firstName, String lastName) { @@ -71,6 +74,14 @@ public Trader(String traderId, String firstName, String lastName, Address workAd this.workAddress = workAddress; } + public Trader(String traderId, String firstName, String lastName, + List
addressList) { + this.traderId = traderId; + this.firstName = firstName; + this.lastName = lastName; + this.addressList = addressList; + } + public Trader( String traderId, String firstName, @@ -128,6 +139,10 @@ public Address getHomeAddress() { return homeAddress; } + public List
getAddressList() { + return addressList; + } + @Override public String toString() { return "Trader{" diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-repository-sample/src/test/java/com/example/SpannerRepositoryIntegrationTests.java b/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-repository-sample/src/test/java/com/example/SpannerRepositoryIntegrationTests.java index 7c2d3aee1a..e3c6cc91c9 100644 --- a/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-repository-sample/src/test/java/com/example/SpannerRepositoryIntegrationTests.java +++ b/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-repository-sample/src/test/java/com/example/SpannerRepositoryIntegrationTests.java @@ -24,6 +24,7 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -135,7 +136,8 @@ void testLoadsCorrectData() { "demo_trader3", "demo_trader_json1", "demo_trader_json2", - "demo_trader_json3"); + "demo_trader_json3", + "demo_trader_json4"); assertThat(this.tradeRepository.findAll()).hasSize(8); @@ -167,14 +169,17 @@ void testLoadsCorrectData() { } @Test - void testJsonFieldReadWrite() { + void testJsonAndArrayJsonFieldReadWrite() { - Address workAddress = new Address(5L, "address line", true); - Trader trader = new Trader("demo_trader1", "John", "Doe", workAddress); + Address address = new Address(5L, "address line", true); + Trader trader = new Trader("demo_trader1", "John", "Doe", + Arrays.asList(address, address, address)); + trader.setHomeAddress(address); this.traderRepository.save(trader); Trader traderFound = this.traderRepository.findById("demo_trader1").get(); assertThat(traderFound.getTraderId()).isEqualTo(trader.getTraderId()); - assertThat(traderFound.getWorkAddress()).isEqualTo(workAddress); + assertThat(traderFound.getHomeAddress()).isEqualTo(address); + assertThat(traderFound.getAddressList()).contains(address, address, address); } } From 377c948aa24986dce4a54352e5d11961258338c4 Mon Sep 17 00:00:00 2001 From: Min Zhu Date: Fri, 3 Jun 2022 20:15:26 +0000 Subject: [PATCH 13/16] mention support for ARRAY in doc. --- docs/src/main/asciidoc/spanner.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/spanner.adoc b/docs/src/main/asciidoc/spanner.adoc index 673938d31a..9e2069ccc4 100644 --- a/docs/src/main/asciidoc/spanner.adoc +++ b/docs/src/main/asciidoc/spanner.adoc @@ -483,7 +483,7 @@ Natively supported types: ==== JSON fields -Spanner supports `JSON` type for columns. `JSON` columns are mapped to custom POJOs annotated with `@Column(spannerType = TypeCode.JSON)`. Read, write and query with custom SQL query are supported for JSON annotated fields. +Spanner supports `JSON` and `ARRAY` type for columns. Such property needs to be annotated with `@Column(spannerType = TypeCode.JSON)`. `JSON` columns are mapped to custom POJOs and `ARRAY` columns are mapped to List of custom POJOs. Read, write and query with custom SQL query are supported for JSON annotated fields. NOTE: The default Gson instance used to convert to and from JSON representation can be customized by providing a bean of type `Gson`. From 926a89980c846bac5feb85fe5caf122cbecf7990 Mon Sep 17 00:00:00 2001 From: Min Zhu Date: Mon, 6 Jun 2022 14:55:41 +0000 Subject: [PATCH 14/16] revert breaking change and leave todo note. --- .../cloud/spring/data/spanner/core/convert/StructAccessor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructAccessor.java b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructAccessor.java index 95c9b50e0b..b1f9012fa5 100644 --- a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructAccessor.java +++ b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructAccessor.java @@ -216,7 +216,8 @@ T getSingleJsonValue(String colName, Class colType) { return gson.fromJson(jsonString, colType); } - private T getSingleJsonValue(int colIndex, Class colType) { + //TODO: change this to private in next major release + public T getSingleJsonValue(int colIndex, Class colType) { if (this.struct.getColumnType(colIndex).getCode() != Code.JSON) { throw new SpannerDataException("Column of index " + colIndex + " not an JSON type."); } From beb2d4b7bbc9fc03de5ecced2e11ddf1010a4d6d Mon Sep 17 00:00:00 2001 From: Min Zhu Date: Mon, 6 Jun 2022 15:54:18 +0000 Subject: [PATCH 15/16] minor: fixing some code smells. --- .../spanner/core/convert/StructAccessor.java | 17 ++++++++--------- .../convert/StructPropertyValueProvider.java | 3 +-- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructAccessor.java b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructAccessor.java index b1f9012fa5..5cc22052f7 100644 --- a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructAccessor.java +++ b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructAccessor.java @@ -100,6 +100,7 @@ public class StructAccessor { .build(); // @formatter:on + private static final String EXCEPTION_NARRATIVE_COL_NOT_ARRAY = "Column is not an ARRAY type: "; private Struct struct; @@ -146,7 +147,7 @@ public Object getSingleValue(int colIndex) { List getListValue(String colName) { if (this.struct.getColumnType(colName).getCode() != Code.ARRAY) { - throw new SpannerDataException("Column is not an ARRAY type: " + colName); + throw new SpannerDataException(EXCEPTION_NARRATIVE_COL_NOT_ARRAY + colName); } Type.Code innerTypeCode = this.struct.getColumnType(colName).getArrayElementType().getCode(); Class clazz = SpannerTypeMapper.getSimpleJavaClassFor(innerTypeCode); @@ -156,13 +157,12 @@ List getListValue(String colName) { List getListJsonValue(String colName, Class colType) { if (this.struct.getColumnType(colName).getCode() != Code.ARRAY) { - throw new SpannerDataException("Column is not an ARRAY type: " + colName); + throw new SpannerDataException(EXCEPTION_NARRATIVE_COL_NOT_ARRAY + colName); } List jsonStringList = this.struct.getJsonList(colName); List result = new ArrayList<>(); - jsonStringList.forEach(item -> { - result.add(gson.fromJson(item, colType)); - }); + jsonStringList.forEach(item -> + result.add(gson.fromJson(item, colType))); return result; } @@ -175,13 +175,12 @@ public Object getJsonValue(int colIndex, Class colType) { private List getListJsonValue(int colIndex, Class colType) { if (this.struct.getColumnType(colIndex).getCode() != Code.ARRAY) { - throw new SpannerDataException("Column is not an ARRAY type: " + colIndex); + throw new SpannerDataException(EXCEPTION_NARRATIVE_COL_NOT_ARRAY + colIndex); } List jsonStringList = this.struct.getJsonList(colIndex); List result = new ArrayList<>(); - jsonStringList.forEach(item -> { - result.add(gson.fromJson(item, colType)); - }); + jsonStringList.forEach(item -> + result.add(gson.fromJson(item, colType))); return result; } diff --git a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructPropertyValueProvider.java b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructPropertyValueProvider.java index 7fa0b68f53..fe201b764f 100644 --- a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructPropertyValueProvider.java +++ b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructPropertyValueProvider.java @@ -127,9 +127,8 @@ private Iterable readIterableWithConversion( String colName = spannerPersistentProperty.getColumnName(); Type.Code spannerColumnType = spannerPersistentProperty.getAnnotatedColumnItemType(); if (spannerColumnType == Type.Code.JSON) { - List value = (List) this.structAccessor.getListJsonValue(colName, + return (List) this.structAccessor.getListJsonValue(colName, spannerPersistentProperty.getColumnInnerType()); - return value; } List listValue = this.structAccessor.getListValue(colName); return listValue.stream() From 196b51af8d9631f99598a735e27a905ba5b6268c Mon Sep 17 00:00:00 2001 From: Min Zhu Date: Tue, 7 Jun 2022 14:58:11 +0000 Subject: [PATCH 16/16] minor changes: fix typo in name and change const name. --- .../convert/ConverterAwareMappingSpannerEntityWriter.java | 4 ++-- .../spring/data/spanner/core/convert/StructAccessor.java | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityWriter.java b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityWriter.java index 9f995eb6bc..fc08b52651 100644 --- a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityWriter.java +++ b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityWriter.java @@ -326,7 +326,7 @@ private boolean attemptSetIterableValue( if (spannerPersistentProperty.getAnnotatedColumnItemType() == Type.Code.JSON) { // if column annotated with JSON, convert directly - valueBinder.toJsonArray(this.covertIterableJsonToValue(value)); + valueBinder.toJsonArray(this.convertIterableJsonToValue(value)); valueSet = true; } else if (spannerPersistentProperty.getAnnotatedColumnItemType() != null) { // use the annotated column type if possible. @@ -376,7 +376,7 @@ private Value convertJsonToValue(Object value) { return Value.json(jsonString); } - private Iterable covertIterableJsonToValue(Iterable value) { + private Iterable convertIterableJsonToValue(Iterable value) { List result = new ArrayList<>(); if (value == null) { return null; diff --git a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructAccessor.java b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructAccessor.java index 5cc22052f7..c0309a812d 100644 --- a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructAccessor.java +++ b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/convert/StructAccessor.java @@ -100,7 +100,7 @@ public class StructAccessor { .build(); // @formatter:on - private static final String EXCEPTION_NARRATIVE_COL_NOT_ARRAY = "Column is not an ARRAY type: "; + private static final String EXCEPTION_COL_NOT_ARRAY = "Column is not an ARRAY type: "; private Struct struct; @@ -147,7 +147,7 @@ public Object getSingleValue(int colIndex) { List getListValue(String colName) { if (this.struct.getColumnType(colName).getCode() != Code.ARRAY) { - throw new SpannerDataException(EXCEPTION_NARRATIVE_COL_NOT_ARRAY + colName); + throw new SpannerDataException(EXCEPTION_COL_NOT_ARRAY + colName); } Type.Code innerTypeCode = this.struct.getColumnType(colName).getArrayElementType().getCode(); Class clazz = SpannerTypeMapper.getSimpleJavaClassFor(innerTypeCode); @@ -157,7 +157,7 @@ List getListValue(String colName) { List getListJsonValue(String colName, Class colType) { if (this.struct.getColumnType(colName).getCode() != Code.ARRAY) { - throw new SpannerDataException(EXCEPTION_NARRATIVE_COL_NOT_ARRAY + colName); + throw new SpannerDataException(EXCEPTION_COL_NOT_ARRAY + colName); } List jsonStringList = this.struct.getJsonList(colName); List result = new ArrayList<>(); @@ -175,7 +175,7 @@ public Object getJsonValue(int colIndex, Class colType) { private List getListJsonValue(int colIndex, Class colType) { if (this.struct.getColumnType(colIndex).getCode() != Code.ARRAY) { - throw new SpannerDataException(EXCEPTION_NARRATIVE_COL_NOT_ARRAY + colIndex); + throw new SpannerDataException(EXCEPTION_COL_NOT_ARRAY + colIndex); } List jsonStringList = this.struct.getJsonList(colIndex); List result = new ArrayList<>();