diff --git a/docs/src/main/asciidoc/spanner.adoc b/docs/src/main/asciidoc/spanner.adoc index c31cc15162..bc136337ed 100644 --- a/docs/src/main/asciidoc/spanner.adoc +++ b/docs/src/main/asciidoc/spanner.adoc @@ -478,10 +478,35 @@ Natively supported types: * `java.time.LocalDateTime` +==== 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. + +[source,java] +---- +@Table(name = "traders") +public class Trader { + + @PrimaryKey + @Column(name = "trader_id") + String traderId; + + @Column(spannerType = TypeCode.JSON) + Details details; +} + +public class Details { + String name; + String affiliation; + Boolean isActive; +} +---- + + ==== Lists Spanner supports `ARRAY` types for columns. -`ARRAY` columns are mapped to `List` fields in POJOS. +`ARRAY` columns are mapped to `List` fields in POJOs. Example: diff --git a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/admin/SpannerSchemaUtils.java b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/admin/SpannerSchemaUtils.java index 270e55a85c..0c142c90ee 100644 --- a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/admin/SpannerSchemaUtils.java +++ b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/admin/SpannerSchemaUtils.java @@ -186,8 +186,13 @@ String getColumnDdlString(SpannerPersistentProperty spannerPersistentProperty, spannerPersistentProperty.isGenerateSchemaNotNull(), spannerPersistentProperty.isCommitTimestamp()); } - spannerJavaType = spannerEntityProcessor + if (spannerColumnType == Type.Code.JSON) { + spannerJavaType = columnType; + } + else { + spannerJavaType = spannerEntityProcessor .getCorrespondingSpannerJavaType(columnType, false); + } if (spannerJavaType == null) { throw new SpannerDataException( 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 325bdc31aa..bfb8647fc4 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 @@ -33,12 +33,14 @@ import com.google.cloud.spanner.Key; import com.google.cloud.spanner.Mutation.WriteBuilder; import com.google.cloud.spanner.Struct; +import com.google.cloud.spanner.Type; import com.google.cloud.spanner.Value; import com.google.cloud.spanner.ValueBinder; import com.google.cloud.spring.data.spanner.core.mapping.SpannerDataException; import com.google.cloud.spring.data.spanner.core.mapping.SpannerMappingContext; import com.google.cloud.spring.data.spanner.core.mapping.SpannerPersistentEntity; import com.google.cloud.spring.data.spanner.core.mapping.SpannerPersistentProperty; +import com.google.gson.Gson; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.util.Assert; @@ -68,6 +70,8 @@ public class ConverterAwareMappingSpannerEntityWriter implements SpannerEntityWr static final Map, BiConsumer, Iterable>> iterablePropertyTypeToMethodMap = createIterableTypeMapping(); + private static final Gson gson = new Gson(); + @SuppressWarnings("unchecked") private static Map, BiConsumer, Iterable>> createIterableTypeMapping() { Map, BiConsumer, Iterable>> map = new LinkedHashMap<>(); @@ -339,6 +343,15 @@ private static boolean attemptSetSingleItemValue(Object value, Class sour return true; } + + private static Value covertJsonToValue(Object value) { + if (value == null) { + return Value.json(null); + } + String jsonString = gson.toJson(value); + return Value.json(jsonString); + } + /** *

* For each property this method "set"s the column name and finds the corresponding "to" @@ -393,6 +406,11 @@ private void writeProperty(MultipleValueBinder sink, valueSet = attemptSetSingleItemValue(Value.COMMIT_TIMESTAMP, Timestamp.class, valueBinder, Timestamp.class, this.writeConverter); } + // annotated json column, bind directly + else if (property.getAnnotatedColumnItemType() == Type.Code.JSON) { + valueBinder.to(covertJsonToValue(propertyValue)); + valueSet = true; + } // use the user's annotated column type if possible else if (property.getAnnotatedColumnItemType() != null) { valueSet = attemptSetSingleItemValue(propertyValue, propertyType, 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 095ba09b37..d14ff7c04f 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 @@ -32,6 +32,7 @@ import com.google.cloud.spanner.Type.Code; import com.google.cloud.spring.core.util.MapBuilder; import com.google.cloud.spring.data.spanner.core.mapping.SpannerDataException; +import com.google.gson.Gson; /** * A convenience wrapper class around Struct to make reading columns easier without @@ -107,6 +108,8 @@ public class StructAccessor { private Set columnNamesIndex; + private static final Gson gson = new Gson(); + public StructAccessor(Struct struct) { this.struct = struct; this.columnNamesIndex = indexColumnNames(); @@ -170,4 +173,23 @@ private Class getSingleItemTypeCode(Type colType) { ? SpannerTypeMapper.getArrayJavaClassFor(colType.getArrayElementType().getCode()) : SpannerTypeMapper.getSimpleJavaClassFor(code); } + + T getSingleJsonValue(String colName, Class colType) { + if (this.struct.isNull(colName)) { + return null; + } + String jsonString = this.struct.getJson(colName); + return gson.fromJson(jsonString, colType); + } + + 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."); + } + if (this.struct.isNull(colIndex)) { + return null; + } + String jsonString = this.struct.getJson(colIndex); + return gson.fromJson(jsonString, colType); + } } 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 c225078bb3..ff5b0141a4 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 @@ -20,6 +20,7 @@ import java.util.stream.Collectors; import com.google.cloud.spanner.Struct; +import com.google.cloud.spanner.Type; import com.google.cloud.spring.data.spanner.core.mapping.SpannerDataException; import com.google.cloud.spring.data.spanner.core.mapping.SpannerPersistentProperty; @@ -104,6 +105,11 @@ public T getPropertyValue(SpannerPersistentProperty spannerPersistentPropert private T readSingleWithConversion( SpannerPersistentProperty spannerPersistentProperty) { String colName = spannerPersistentProperty.getColumnName(); + Type.Code spannerColumnType = spannerPersistentProperty.getAnnotatedColumnItemType(); + if (spannerColumnType == Type.Code.JSON) { + Object value = this.structAccessor.getSingleJsonValue(colName, spannerPersistentProperty.getType()); + return (T) value; + } Object value = this.structAccessor.getSingleValue(colName); return (value != null) ? convertOrRead((Class) spannerPersistentProperty.getType(), value) : null; } 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 40e88a5e61..afd675e293 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 @@ -27,6 +27,7 @@ import java.util.regex.Pattern; import com.google.cloud.spanner.Key; +import com.google.cloud.spanner.Type; import com.google.cloud.spring.data.spanner.core.convert.ConversionUtils; import com.google.cloud.spring.data.spanner.core.convert.ConverterAwareMappingSpannerEntityProcessor; import com.google.cloud.spring.data.spanner.core.convert.SpannerEntityProcessor; @@ -96,6 +97,8 @@ public class SpannerPersistentEntityImpl private final String where; + private final Set> jsonProperties = new HashSet<>(); + /** * Creates a {@link SpannerPersistentEntityImpl}. * @param information type information about the underlying entity type. @@ -180,6 +183,10 @@ else if (property.isEagerInterleaved()) { + " in " + getType().getSimpleName() + "."); }); } + + if (property.getAnnotatedColumnItemType() == Type.Code.JSON) { + this.jsonProperties.add(property.getType()); + } } private void addPersistentPropertyToPersistentEntity( @@ -398,6 +405,11 @@ public Set columns() { return Collections.unmodifiableSet(this.columnNames); } + // Lookup whether a particular class is a JSON entity property + public boolean isJsonProperty(Class type) { + return this.jsonProperties.contains(type); + } + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.context.addPropertyAccessor(new BeanFactoryAccessor()); 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 303ccecd86..b9a68c8904 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 @@ -39,6 +39,7 @@ import com.google.cloud.spring.data.spanner.core.mapping.SpannerDataException; import com.google.cloud.spring.data.spanner.core.mapping.SpannerMappingContext; import com.google.cloud.spring.data.spanner.core.mapping.SpannerPersistentEntity; +import com.google.cloud.spring.data.spanner.core.mapping.SpannerPersistentEntityImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -232,15 +233,33 @@ private List executeReadSql(Pageable pageable, Sort sort, QueryTagValue queryTag Statement statement = buildStatementFromQueryAndTags(queryTagValue); - return (getReturnedSimpleConvertableItemType() != null) - ? this.spannerTemplate.query( - struct -> new StructAccessor(struct).getSingleValue(0), statement, - spannerQueryOptions) - : this.spannerTemplate.query(this.entityType, - statement, + if (getReturnedSimpleConvertableItemType() != null) { + return this.spannerTemplate.query( + struct -> new StructAccessor(struct).getSingleValue(0), statement, + spannerQueryOptions); + } + // check if returnedType is a field annotated as json + boolean isJsonField = isJsonFieldType(returnedType); + if (isJsonField) { + return this.spannerTemplate.query( + struct -> new StructAccessor(struct).getSingleJsonValue(0, returnedType), statement, + spannerQueryOptions); + } + + return this.spannerTemplate.query(this.entityType, + statement, spannerQueryOptions); } + private boolean isJsonFieldType(Class returnedType) { + SpannerPersistentEntityImpl persistentEntity = (SpannerPersistentEntityImpl) this.spannerMappingContext + .getPersistentEntity(this.entityType); + if (persistentEntity == null) { + return false; + } + return persistentEntity.isJsonProperty(returnedType); + } + private Statement buildStatementFromQueryAndTags(QueryTagValue queryTagValue) { Map paramMetadataMap = new HashMap<>(); for (java.lang.reflect.Parameter param : getQueryMethod().getMethod().getParameters()) { 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 a1af095176..a05197a643 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 @@ -22,6 +22,7 @@ import com.google.cloud.ByteArray; import com.google.cloud.spanner.Key; +import com.google.cloud.spanner.Type; import com.google.cloud.spring.data.spanner.core.convert.ConverterAwareMappingSpannerEntityProcessor; import com.google.cloud.spring.data.spanner.core.convert.SpannerEntityProcessor; import com.google.cloud.spring.data.spanner.core.mapping.Column; @@ -77,7 +78,7 @@ public void getCreateDdlTest() { + "primitiveIntField INT64 , bigIntField INT64 , bytes BYTES(MAX) , " + "bytesList ARRAY , integerList ARRAY , " + "doubles ARRAY , commitTimestamp TIMESTAMP OPTIONS (allow_commit_timestamp=true) , " - + "bigDecimalField NUMERIC , bigDecimals ARRAY ) " + + + "bigDecimalField NUMERIC , bigDecimals ARRAY , jsonCol JSON ) " + "PRIMARY KEY ( id , id_2 , id3 )"; assertThat(this.spannerSchemaUtils.getCreateTableDdlString(TestEntity.class)) @@ -87,74 +88,81 @@ public void getCreateDdlTest() { @Test public void createDdlString() { assertColumnDdl(String.class, null, - "id", OptionalLong.empty(), + "id", null, OptionalLong.empty(), "id STRING(MAX)"); } @Test public void createDdlStringCustomLength() { assertColumnDdl(String.class, null, - "id", OptionalLong.of(333L), + "id", null, OptionalLong.of(333L), "id STRING(333)"); } @Test public void createDdlBytesMax() { assertColumnDdl(ByteArray.class, null, - "bytes", OptionalLong.empty(), + "bytes", null, OptionalLong.empty(), "bytes BYTES(MAX)"); } @Test public void createDdlBytesCustomLength() { assertColumnDdl(ByteArray.class, null, - "bytes", OptionalLong.of(333L), + "bytes", null, OptionalLong.of(333L), "bytes BYTES(333)"); } @Test public void ddlForListOfByteArray() { assertColumnDdl(List.class, ByteArray.class, - "bytesList", OptionalLong.of(111L), + "bytesList", null, OptionalLong.of(111L), "bytesList ARRAY"); } @Test public void ddlForDoubleArray() { assertColumnDdl(double[].class, null, - "doubles", OptionalLong.of(111L), + "doubles", null, OptionalLong.of(111L), "doubles ARRAY"); } @Test public void ddlForNumericList() { assertColumnDdl(List.class, BigDecimal.class, - "bigDecimals", OptionalLong.empty(), + "bigDecimals", null, OptionalLong.empty(), "bigDecimals ARRAY"); } @Test public void createDdlNumeric() { assertColumnDdl(BigDecimal.class, null, - "bigDecimal", OptionalLong.empty(), + "bigDecimal", null, OptionalLong.empty(), "bigDecimal NUMERIC"); } @Test public void ddlForListOfListOfIntegers() { assertColumnDdl(List.class, Integer.class, - "integerList", OptionalLong.empty(), + "integerList", null, OptionalLong.empty(), "integerList ARRAY"); } @Test public void ddlForListOfListOfDoubles() { assertColumnDdl(List.class, Double.class, - "doubleList", OptionalLong.empty(), + "doubleList", null, OptionalLong.empty(), "doubleList ARRAY"); } - private void assertColumnDdl(Class clazz, Class innerClazz, String name, + @Test + public void createDdlForJson() { + assertColumnDdl(JsonColumn.class, null, + "jsonCol", Type.Code.JSON, OptionalLong.empty(), + "jsonCol JSON"); + } + + private void assertColumnDdl(Class clazz, Class innerClazz, String name, Type.Code code, OptionalLong length, String expectedDDL) { SpannerPersistentProperty spannerPersistentProperty = mock(SpannerPersistentProperty.class); @@ -165,6 +173,9 @@ private void assertColumnDdl(Class clazz, Class innerClazz, String name, when(spannerPersistentProperty.getColumnName()).thenReturn(name); when(spannerPersistentProperty.getMaxColumnLength()).thenReturn(length); + + when(spannerPersistentProperty + .getAnnotatedColumnItemType()).thenReturn(code); assertThat( this.spannerSchemaUtils.getColumnDdlString( spannerPersistentProperty, this.spannerEntityProcessor)) @@ -261,6 +272,13 @@ private static class TestEntity { BigDecimal bigDecimalField; List bigDecimals; + + @Column(spannerType = TypeCode.JSON) + JsonColumn jsonCol; + } + private static class JsonColumn { + String param1; + String param2; } private static class EmbeddedColumns { 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 52d9aa817e..0ba61d10cd 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 @@ -306,4 +306,22 @@ public void zeroArgsListShouldThrowError() { .read(TestEntities.TestEntityWithListWithZeroTypeArgs.class, struct); } + @Test + public void readJsonFieldTest() { + 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("params", Type.json())))); + when(row.getColumnType("id")).thenReturn(Type.string()); + + when(row.getJson("params")).thenReturn("{\"p1\":\"address line\",\"p2\":\"5\"}"); + + TestEntities.TestEntityJson result = this.spannerEntityReader.read(TestEntities.TestEntityJson.class, row); + + assertThat(result.id).isEqualTo("1234"); + + assertThat(result.params.p1).isEqualTo("address line"); + assertThat(result.params.p2).isEqualTo("5"); + } + } 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 141e6b7005..40c4ab5883 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 @@ -343,6 +343,39 @@ public void writeSomeColumnsTest() { verifyNoInteractions(booleanFieldBinder); } + @Test + public void writeJsonTest() { + TestEntities.Params parameters = new TestEntities.Params("some value", "some other value"); + TestEntities.TestEntityJson testEntity = new TestEntities.TestEntityJson("id1", parameters); + + WriteBuilder writeBuilder = mock(WriteBuilder.class); + ValueBinder valueBinder = mock(ValueBinder.class); + + when(writeBuilder.set("id")).thenReturn(valueBinder); + when(writeBuilder.set("params")).thenReturn(valueBinder); + + this.spannerEntityWriter.write(testEntity, writeBuilder::set); + + verify(valueBinder).to(testEntity.id); + verify(valueBinder).to(Value.json("{\"p1\":\"some value\",\"p2\":\"some other value\"}")); + } + + @Test + public void writeNullJsonTest() { + TestEntities.TestEntityJson testEntity = new TestEntities.TestEntityJson("id1", null); + + WriteBuilder writeBuilder = mock(WriteBuilder.class); + ValueBinder valueBinder = mock(ValueBinder.class); + + when(writeBuilder.set("id")).thenReturn(valueBinder); + when(writeBuilder.set("params")).thenReturn(valueBinder); + + this.spannerEntityWriter.write(testEntity, writeBuilder::set); + + verify(valueBinder).to(testEntity.id); + verify(valueBinder).to(Value.json(null)); + } + @Test public void writeUnsupportedTypeIterableTest() { this.expectedEx.expect(SpannerDataException.class); 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 737eb7a311..c33b673f3b 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 @@ -295,4 +295,31 @@ static class PartialConstructor { } } + /** + * A test class with Json field. + */ + @Table(name = "custom_test_table") + static class TestEntityJson { + @PrimaryKey + String id; + + @Column(spannerType = TypeCode.JSON) + Params params; + + TestEntityJson(String id, Params params) { + this.id = id; + this.params = params; + } + } + + static class Params { + String p1; + + String p2; + + Params(String p1, String p2) { + this.p1 = p1; + this.p2 = p2; + } + } } 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 7fe7db4c7a..54bee34fd1 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 @@ -27,6 +27,7 @@ import com.google.cloud.spring.data.spanner.core.SpannerTemplate; import com.google.cloud.spring.data.spanner.core.mapping.SpannerDataException; import com.google.cloud.spring.data.spanner.test.AbstractSpannerIntegrationTest; +import com.google.cloud.spring.data.spanner.test.domain.Details; import com.google.cloud.spring.data.spanner.test.domain.Trade; import org.awaitility.Awaitility; import org.junit.Rule; @@ -105,6 +106,35 @@ public void insertAndDeleteSequence() { assertThat(this.spannerOperations.count(Trade.class)).isZero(); } + @Test + public void insertAndDeleteWithJsonField() { + + this.spannerOperations.delete(Trade.class, KeySet.all()); + assertThat(this.spannerOperations.count(Trade.class)).isZero(); + + Trade trade1 = Trade.aTrade(); + trade1.setOptionalDetails(new Details("abc", "def")); + Trade trade2 = Trade.aTrade(); + trade2.setOptionalDetails(new Details("some context", 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.getOptionalDetails()).isInstanceOf(Details.class); + assertThat(retrievedTrade.getOptionalDetails()).isEqualTo(new Details("abc", "def")); + + this.spannerOperations.deleteAll(Arrays.asList(trade1, trade2)); + assertThat(this.spannerOperations.count(Trade.class)).isZero(); + } + @Test public void readWriteTransactionTest() { Trade trade = Trade.aTrade(); 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 48164aa6ad..cc35d9c01e 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 @@ -330,6 +330,21 @@ public void testParentChildPkNamesMismatch() { .getPersistentEntity(ParentInRelationshipMismatchedKeyName.class); } + @Test + public void testGetJsonPropertyName() { + SpannerPersistentEntityImpl entityWithJsonField = (SpannerPersistentEntityImpl) this.spannerMappingContext + .getPersistentEntity(EntityWithJsonField.class); + + assertThat(entityWithJsonField.isJsonProperty(JsonEntity.class)).isTrue(); + assertThat(entityWithJsonField.isJsonProperty(String.class)).isFalse(); + + SpannerPersistentEntityImpl entityWithNoJsonField = (SpannerPersistentEntityImpl) this.spannerMappingContext + .getPersistentEntity(TestEntity.class); + + assertThat(entityWithNoJsonField.isJsonProperty(String.class)).isFalse(); + assertThat(entityWithNoJsonField.isJsonProperty(long.class)).isFalse(); + } + private static class ParentInRelationship { @PrimaryKey String id; @@ -502,4 +517,17 @@ private static class MultiIdsEntity { @PrimaryKey(keyOrder = 3) Double id3; } + + private static class EntityWithJsonField { + @PrimaryKey + String id; + + @Column(spannerType = TypeCode.JSON) + JsonEntity jsonField; + } + + private static class JsonEntity { + + } + } 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 7029c10065..a0d25739a5 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 @@ -32,6 +32,7 @@ import com.google.cloud.spring.data.spanner.core.mapping.SpannerPersistentEntity; import com.google.cloud.spring.data.spanner.repository.support.SimpleSpannerRepository; import com.google.cloud.spring.data.spanner.test.AbstractSpannerIntegrationTest; +import com.google.cloud.spring.data.spanner.test.domain.Details; import com.google.cloud.spring.data.spanner.test.domain.SubTrade; import com.google.cloud.spring.data.spanner.test.domain.SubTradeComponent; import com.google.cloud.spring.data.spanner.test.domain.SubTradeComponentRepository; @@ -515,6 +516,31 @@ public void testNonNull() { .hasMessageMatching("Result must not be null!"); } + @Test + public void testWithJsonField() { + Trade trade1 = Trade.aTrade(); + trade1.setOptionalDetails(new Details("abc", "def")); + trade1.setBackupDetails(new Details("backup context", "backup context continued")); + Trade trade2 = Trade.aTrade(); + trade2.setOptionalDetails(new Details("some context", null)); + Trade trade3 = Trade.aTrade(); + this.tradeRepository.save(trade1); + this.tradeRepository.save(trade2); + this.tradeRepository.save(trade3); + + assertThat(this.tradeRepository.findAll()).contains(trade1, trade2, trade3); + assertThat(this.tradeRepository.getByDetailP1("abc")).hasSize(1).contains(trade1); + + String traderId = trade1.getTraderId(); + Optional

optionalDetails = this.tradeRepository.getOptionalDetailsById(traderId); + assertThat(optionalDetails).isEqualTo(Optional.of(new Details("abc", "def"))); + + String traderId3 = trade3.getTraderId(); + Optional
empty = this.tradeRepository.getOptionalDetailsById(traderId3); + assertThat(empty).isNotPresent(); + + } + @Test public void testTransaction() { this.tradeRepositoryTransactionalService.testTransactionalAnnotation(2); 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 a8638bb194..5e5aab4459 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 @@ -20,18 +20,22 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.function.Function; import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.Statement; import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.TransactionContext; import com.google.cloud.spanner.TransactionRunner; +import com.google.cloud.spanner.Type; import com.google.cloud.spanner.Value; import com.google.cloud.spring.data.spanner.core.SpannerMutationFactory; import com.google.cloud.spring.data.spanner.core.SpannerQueryOptions; import com.google.cloud.spring.data.spanner.core.SpannerTemplate; import com.google.cloud.spring.data.spanner.core.admin.SpannerSchemaUtils; import com.google.cloud.spring.data.spanner.core.convert.SpannerEntityProcessor; +import com.google.cloud.spring.data.spanner.core.convert.SpannerReadConverter; import com.google.cloud.spring.data.spanner.core.convert.SpannerWriteConverter; import com.google.cloud.spring.data.spanner.core.mapping.Column; import com.google.cloud.spring.data.spanner.core.mapping.Interleaved; @@ -39,11 +43,13 @@ 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.spanner.v1.TypeCode; import org.assertj.core.data.Offset; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.springframework.data.domain.PageRequest; @@ -54,6 +60,8 @@ import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ReturnedType; import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; @@ -110,6 +118,7 @@ public void initMocks() throws NoSuchMethodException { Method method = Object.class.getMethod("toString"); when(this.queryMethod.getMethod()).thenReturn(method); when(this.spannerEntityProcessor.getWriteConverter()).thenReturn(new SpannerWriteConverter()); + when(this.spannerEntityProcessor.getReadConverter()).thenReturn(new SpannerReadConverter()); this.spannerTemplate = spy(new SpannerTemplate(() -> this.databaseClient, this.spannerMappingContext, this.spannerEntityProcessor, mock(SpannerMutationFactory.class), new SpannerSchemaUtils( @@ -484,6 +493,16 @@ public void sqlCountWithWhereTest() throws NoSuchMethodException { SqlSpannerQuery sqlSpannerQuery = createQuery(sql, long.class, false); + Struct row = mock(Struct.class); + when(row.getType()).thenReturn( + Type.struct(Arrays.asList(Type.StructField.of("STRUCT", Type.int64())))); + when(row.getLong(0)).thenReturn(3L); + when(row.getColumnType(0)).thenReturn(Type.int64()); + + ResultSet resultSet = mock(ResultSet.class); + when(resultSet.next()).thenReturn(true).thenReturn(false); + when(resultSet.getCurrentRowAsStruct()).thenReturn(row); + doAnswer(invocation -> { Statement statement = invocation.getArgument(0); SpannerQueryOptions queryOptions = invocation.getArgument(1); @@ -495,8 +514,7 @@ public void sqlCountWithWhereTest() throws NoSuchMethodException { assertThat(paramMap.get("id").getString()).isEqualTo(params[0]); assertThat(paramMap.get("trader_id").getString()).isEqualTo(params[1]); - - return null; + return resultSet; }).when(this.spannerTemplate).executeQuery(any(), any()); // This dummy method was created so the metadata for the ARRAY param inner type is @@ -505,9 +523,89 @@ public void sqlCountWithWhereTest() throws NoSuchMethodException { when(this.queryMethod.getMethod()).thenReturn(method); Mockito.when(this.queryMethod.getParameters()).thenReturn(new DefaultParameters(method)); + when(sqlSpannerQuery.getReturnedSimpleConvertableItemType()).thenReturn(long.class); + sqlSpannerQuery.execute(params); - verify(this.spannerTemplate, times(1)).executeQuery(any(), any()); + verify(this.spannerTemplate).query((Function) any(), any(), any()); + verify(this.spannerTemplate).executeQuery(any(), any()); + } + + @Test + public void sqlReturnTypeIsJsonFieldTest() throws NoSuchMethodException { + String sql = "SELECT details 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.getMethod()).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("details", Type.json())))); + when(row.getColumnType(0)).thenReturn(Type.json()); + when(row.getJson(0)).thenReturn("{\"p1\":\"address line\",\"p2\":\"5\"}"); + + Object result = rowFunc.apply(row); + + assertThat(result).isInstanceOf(Detail.class); + assertThat(((Detail) result).p1).isEqualTo("address line"); + assertThat(((Detail) result).p2).isEqualTo("5"); + } + + private static class Singer { + @PrimaryKey + String id; + + String stageName; + + @Column(spannerType = TypeCode.JSON) + Detail details; + } + + private class Detail { + String p1; + + String p2; + + Detail(String p1, String p2) { + this.p1 = p1; + this.p2 = p2; + } } private static class SymbolAction { @@ -601,6 +699,10 @@ public List dummyMethod5(String id, String trader_id, Sort param3) { return null; } + public Detail dummyMethod6(String stageName) { + return null; + } + public List pageableAndSort(Pageable param1, Sort param2) { return null; } diff --git a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/test/domain/Details.java b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/test/domain/Details.java new file mode 100644 index 0000000000..72bfbff99b --- /dev/null +++ b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/test/domain/Details.java @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spring.data.spanner.test.domain; + +import java.util.Objects; + +public class Details { + String p1; + + String p2; + + public Details(String p1, String p2) { + this.p1 = p1; + this.p2 = p2; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Details details = (Details) o; + return Objects.equals(p1, details.p1) && Objects.equals(p2, details.p2); + } + + @Override + public int hashCode() { + return Objects.hash(p1, p2); + } +} 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 8569448bc8..f89f573c98 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 @@ -36,6 +36,7 @@ import com.google.cloud.spring.data.spanner.core.mapping.PrimaryKey; import com.google.cloud.spring.data.spanner.core.mapping.Table; import com.google.cloud.spring.data.spanner.core.mapping.Where; +import com.google.spanner.v1.TypeCode; /** * A test domain object using many features. @@ -80,6 +81,12 @@ public class Trade { private List bigDecimals; + @Column(spannerType = TypeCode.JSON) + private Details optionalDetails; + + @Column(spannerType = TypeCode.JSON) + private Details backupDetails; + /** * Partial constructor. Intentionally tests a field that is left null sometimes. * @param symbol the symbol. @@ -285,4 +292,16 @@ public List getBigDecimals() { public void setBigDecimals(List bigDecimals) { this.bigDecimals = bigDecimals; } + + public Details getOptionalDetails() { + return optionalDetails; + } + + public void setOptionalDetails(Details optionalDetails) { + this.optionalDetails = optionalDetails; + } + + public void setBackupDetails(Details backupDetails) { + this.backupDetails = backupDetails; + } } 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 c13e43561c..1c8c8f31c7 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 @@ -125,6 +125,14 @@ public interface TradeRepository extends SpannerRepository { List findByActionIn(Set action); + @Query("SELECT * from :com.google.cloud.spring.data.spanner.test.domain.Trade:" + + " where JSON_VALUE(optionalDetails, '$.p1') = @p1") + List getByDetailP1(@Param("p1") String p1); + + @Query("SELECT optionalDetails from :com.google.cloud.spring.data.spanner.test.domain.Trade:" + + " where trader_id = @trader_id") + Optional
getOptionalDetailsById(@Param("trader_id") String traderId); + @NonNull Trade getByAction(String s); } diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-sample/src/main/java/com/example/Address.java b/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-sample/src/main/java/com/example/Address.java new file mode 100644 index 0000000000..33cb3fb12e --- /dev/null +++ b/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-sample/src/main/java/com/example/Address.java @@ -0,0 +1,59 @@ +/* + * Copyright 2017-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example; + +import java.util.Objects; + +public class Address { + private String streetName; + + private Long streetNumber; + + private Boolean active; + + public Address(Long streetNumber, String streetName, Boolean active) { + this.streetName = streetName; + this.streetNumber = streetNumber; + this.active = active; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Address that = (Address) o; + return active == that.active && Objects.equals(streetName, that.streetName) && Objects.equals(streetNumber, that.streetNumber); + } + + @Override + public int hashCode() { + return Objects.hash(streetName, streetNumber, active); + } + + @Override + public String toString() { + return "Address{" + + "number =" + streetNumber + + ", street Name='" + streetName + '\'' + + ", active=" + active + + '}'; + } +} diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-sample/src/main/java/com/example/SpannerRepositoryExample.java b/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-sample/src/main/java/com/example/SpannerRepositoryExample.java index a795320894..51e6b23a69 100644 --- a/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-sample/src/main/java/com/example/SpannerRepositoryExample.java +++ b/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-sample/src/main/java/com/example/SpannerRepositoryExample.java @@ -17,6 +17,7 @@ package com.example; import java.util.Arrays; +import java.util.List; import com.google.cloud.spanner.Key; import com.google.cloud.spring.data.spanner.core.admin.SpannerDatabaseAdminTemplate; @@ -108,6 +109,35 @@ public void runExample() { LOGGER.info(this.traderRepository.findById("demo_trader1").get().getTrades()); 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 class."); + + Trader trader1 = new Trader("demo_trader_json1", "John", "Doe", + new Address(5L, "fake address 1", true)); + Trader trader2 = new Trader("demo_trader_json2", "Mary", "Jane", + new Address(8L, "fake address 2", true)); + 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)); + + this.traderRepository.save(trader1); + this.traderRepository.save(trader2); + this.traderRepository.save(trader3); + + LOGGER.info("Find trader by Id and print out JSON field 'workAddress' as string: " + + this.traderRepository.findById("demo_trader_json1").get().getWorkAddress()); + + LOGGER.info("Find trader by Id and print out JSON field 'unusedDetails' as string: " + + this.traderRepository.findById("demo_trader_json3").get().getHomeAddress()); + + 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 + ". "); + + List
details = this.traderRepository.getTraderWorkAddressByActive("true"); + LOGGER.info("A query method can return a list of the JSON field values in POJO."); + LOGGER.info("Work addresses with active = true: "); + details.forEach(x -> LOGGER.info(x.toString())); } void createTablesIfNotExists() { diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-sample/src/main/java/com/example/Trader.java b/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-sample/src/main/java/com/example/Trader.java index 3630cb6e76..0f73b6d43f 100644 --- a/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-sample/src/main/java/com/example/Trader.java +++ b/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-sample/src/main/java/com/example/Trader.java @@ -25,6 +25,7 @@ import com.google.cloud.spring.data.spanner.core.mapping.Interleaved; import com.google.cloud.spring.data.spanner.core.mapping.PrimaryKey; import com.google.cloud.spring.data.spanner.core.mapping.Table; +import com.google.spanner.v1.TypeCode; /** * A sample entity. @@ -54,6 +55,12 @@ public class Trader { @Interleaved(lazy = true) private List trades; + @Column(name = "work_address", spannerType = TypeCode.JSON) + private Address workAddress; + + @Column(name = "home_address", spannerType = TypeCode.JSON) + private Address homeAddress; + public Trader() { } @@ -63,6 +70,13 @@ public Trader(String traderId, String firstName, String lastName) { this.lastName = lastName; } + public Trader(String traderId, String firstName, String lastName, Address workAddress) { + this.traderId = traderId; + this.firstName = firstName; + this.lastName = lastName; + this.workAddress = workAddress; + } + public Trader(String traderId, String firstName, String lastName, Timestamp createdOn, List modifiedOn) { this.traderId = traderId; this.firstName = firstName; @@ -103,6 +117,18 @@ public void setTrades(List trades) { this.trades = trades; } + public Address getWorkAddress() { + return workAddress; + } + + public void setHomeAddress(Address homeAddress) { + this.homeAddress = homeAddress; + } + + public Address getHomeAddress() { + return homeAddress; + } + @Override public String toString() { return "Trader{" + diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-sample/src/main/java/com/example/TraderRepository.java b/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-sample/src/main/java/com/example/TraderRepository.java index 79165911e3..f0fbb3c293 100644 --- a/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-sample/src/main/java/com/example/TraderRepository.java +++ b/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-sample/src/main/java/com/example/TraderRepository.java @@ -16,8 +16,12 @@ package com.example; +import java.util.List; + import com.google.cloud.spring.data.spanner.repository.SpannerRepository; +import com.google.cloud.spring.data.spanner.repository.query.Query; +import org.springframework.data.repository.query.Param; import org.springframework.data.rest.core.annotation.RepositoryRestResource; /** @@ -28,5 +32,10 @@ */ @RepositoryRestResource(collectionResourceRel = "traders", path = "traders") public interface TraderRepository extends SpannerRepository { + @Query("SELECT count(1) from traders where JSON_VALUE(work_address, '$.active') = @active") + long getCountActive(@Param("active") String active); + + @Query("SELECT work_address from traders where JSON_VALUE(work_address, '$.active') = @active") + List
getTraderWorkAddressByActive(@Param("active") String active); } diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-sample/src/test/java/com/example/SpannerRepositoryIntegrationTests.java b/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-sample/src/test/java/com/example/SpannerRepositoryIntegrationTests.java index 53dcf0337e..e0dce92092 100644 --- a/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-sample/src/test/java/com/example/SpannerRepositoryIntegrationTests.java +++ b/spring-cloud-gcp-samples/spring-cloud-gcp-data-spanner-sample/src/test/java/com/example/SpannerRepositoryIntegrationTests.java @@ -142,7 +142,8 @@ public void testLoadsCorrectData() { this.spannerRepositoryExample.runExample(); List traderIds = new ArrayList<>(); this.traderRepository.findAll().forEach(t -> traderIds.add(t.getTraderId())); - assertThat(traderIds).containsExactlyInAnyOrder("demo_trader1", "demo_trader2", "demo_trader3"); + assertThat(traderIds).containsExactlyInAnyOrder("demo_trader1", "demo_trader2", "demo_trader3", + "demo_trader_json1", "demo_trader_json2", "demo_trader_json3"); assertThat(this.tradeRepository.findAll()).hasSize(8); @@ -164,4 +165,16 @@ public void testLoadsCorrectData() { assertThat(this.traderRepository.findById("demo_trader1").get().getTrades()).hasSize(3); } + + @Test + public void testJsonFieldReadWrite() { + + Address workAddress = new Address(5L, "address line", true); + Trader trader = new Trader("demo_trader1", "John", "Doe", workAddress); + this.traderRepository.save(trader); + + Trader traderFound = this.traderRepository.findById("demo_trader1").get(); + assertThat(traderFound.getTraderId()).isEqualTo(trader.getTraderId()); + assertThat(traderFound.getWorkAddress()).isEqualTo(workAddress); + } }