diff --git a/CHANGELOG.md b/CHANGELOG.md index d4666c3..f2c4d27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/). +## [1.6.0] [not yet released] +### Added +- Added support for composite primary keys. + ## [1.5.0] ### Added - Added support for java.util.Map and similar types as mapping types. diff --git a/src/main/java/com/oracle/nosql/spring/data/Constants.java b/src/main/java/com/oracle/nosql/spring/data/Constants.java index 6136dde..652ca6b 100644 --- a/src/main/java/com/oracle/nosql/spring/data/Constants.java +++ b/src/main/java/com/oracle/nosql/spring/data/Constants.java @@ -29,6 +29,9 @@ public class Constants { public static final int NOTSET_TABLE_TIMEOUT_MS = 0; public static final int NOTSET_TABLE_TTL = 0; + public static final boolean NOTSET_SHARD_KEY = true; + public static final int NOTSET_PRIMARY_KEY_ORDER = -1; + public static final String USER_AGENT = "NoSQL-SpringSDK"; private Constants() {} diff --git a/src/main/java/com/oracle/nosql/spring/data/core/NosqlTemplate.java b/src/main/java/com/oracle/nosql/spring/data/core/NosqlTemplate.java index ff39e22..3c7179c 100644 --- a/src/main/java/com/oracle/nosql/spring/data/core/NosqlTemplate.java +++ b/src/main/java/com/oracle/nosql/spring/data/core/NosqlTemplate.java @@ -16,6 +16,8 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; +import com.oracle.nosql.spring.data.core.mapping.NosqlKey; +import com.oracle.nosql.spring.data.core.mapping.NosqlPersistentProperty; import oracle.nosql.driver.NoSQLException; import oracle.nosql.driver.NoSQLHandle; import oracle.nosql.driver.ops.DeleteRequest; @@ -39,7 +41,6 @@ import org.apache.commons.lang3.reflect.FieldUtils; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; @@ -485,6 +486,24 @@ private String convertProperty( return property; } + //field can be composite key + NosqlPersistentProperty pp = mappingNosqlConverter.getMappingContext(). + getPersistentPropertyPath(property, + entityInformation.getJavaType()).getLeafProperty(); + + NosqlPersistentProperty parentPp = + mappingNosqlConverter.getMappingContext(). + getPersistentPropertyPath(property, + entityInformation.getJavaType()).getBaseProperty(); + if (pp != null) { + if (pp.isAnnotationPresent(NosqlKey.class)) { + return pp.getName(); + } + if (parentPp != null && parentPp.isIdProperty()) { + return pp.getName(); + } + } + return JSON_COLUMN + "." + property; } @@ -582,4 +601,8 @@ public Iterable find( return IterableUtil.getIterableFromStream(resStream); } + + public NoSQLHandle getNosqlClient() { + return nosqlClient; + } } diff --git a/src/main/java/com/oracle/nosql/spring/data/core/NosqlTemplateBase.java b/src/main/java/com/oracle/nosql/spring/data/core/NosqlTemplateBase.java index 85ea824..b3f50ea 100644 --- a/src/main/java/com/oracle/nosql/spring/data/core/NosqlTemplateBase.java +++ b/src/main/java/com/oracle/nosql/spring/data/core/NosqlTemplateBase.java @@ -41,7 +41,6 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.util.Assert; - public abstract class NosqlTemplateBase implements ApplicationContextAware { @@ -116,35 +115,59 @@ protected TableResult doTableRequest(NosqlEntityInformation entityInformat protected boolean doCreateTableIfNotExists( NosqlEntityInformation entityInformation) { - String idColName = entityInformation.getIdField().getName(); + String tableName = entityInformation.getTableName(); + String sql; - String idColType = entityInformation.getIdNosqlType().toString(); - if (entityInformation.getIdNosqlType() == FieldValue.Type.TIMESTAMP) { - // For example: CREATE TABLE IF NOT EXISTS SensorIdTimestamp - // (time TIMESTAMP(3) , kv_json_ JSON, PRIMARY KEY( time )) - idColType += "(" + nosqlDbFactory.getTimestampPrecision() + ")"; - } + Map shardKeys = + entityInformation.getShardKeys(); - String autogen = ""; - if (entityInformation.isAutoGeneratedId() ) { - if (entityInformation.getIdNosqlType() == FieldValue.Type.STRING) { - autogen = TEMPLATE_GENERATED_UUID; - } else { - autogen = TEMPLATE_GENERATED_ALWAYS; + Map nonShardKeys = + entityInformation.getNonShardKeys(); + + StringBuilder tableBuilder = new StringBuilder(); + tableBuilder.append("CREATE TABLE IF NOT EXISTS "); + tableBuilder.append(tableName).append("("); //create open ( + + shardKeys.forEach((key, type) -> { + String keyType = type.name(); + if (keyType.equals(FieldValue.Type.TIMESTAMP.toString())) { + keyType += "(" + nosqlDbFactory.getTimestampPrecision() + ")"; + } + String autogen = getAutoGenType(entityInformation); + tableBuilder.append(key).append(" ").append(keyType) + .append(" ").append(autogen).append(","); + }); + + nonShardKeys.forEach((key, type) -> { + String keyType = type.name(); + if (keyType.equals(FieldValue.Type.TIMESTAMP.toString())) { + keyType += "(" + nosqlDbFactory.getTimestampPrecision() + ")"; } + String autogen = getAutoGenType(entityInformation); + tableBuilder.append(key).append(" ").append(keyType) + .append(" ").append(autogen).append(","); + }); + tableBuilder.append(JSON_COLUMN).append(" ").append("JSON").append(","); + + tableBuilder.append("PRIMARY KEY").append("("); //primary key open ( + tableBuilder.append("SHARD").append("("); + tableBuilder.append(String.join(",", shardKeys.keySet())); + tableBuilder.append(")"); + + if (!nonShardKeys.isEmpty()) { + tableBuilder.append(","); + tableBuilder.append(String.join(",", nonShardKeys.keySet())); } + tableBuilder.append(")"); //primary key close ) + tableBuilder.append(")"); //create close ) - String ttl = ""; + //ttl if (entityInformation.getTtl() != null && entityInformation.getTtl().getValue() != 0) { - ttl = String.format(TEMPLATE_TTL_CREATE, - entityInformation.getTtl().toString()); + tableBuilder.append(String.format(TEMPLATE_TTL_CREATE, + entityInformation.getTtl().toString())); } - - String tableName = entityInformation.getTableName(); - String sql = String.format(TEMPLATE_CREATE_TABLE, - tableName, - idColName, idColType, autogen, idColName, ttl); + sql = tableBuilder.toString(); TableRequest tableReq = new TableRequest().setStatement(sql) .setTableLimits(entityInformation.getTableLimits(nosqlDbFactory)); @@ -266,7 +289,6 @@ protected void doUpdate(NosqlEntityInformation entityInformation, idColumnName); Map params = new HashMap<>(); - //todo implement composite keys params.put("$id", row.get(idColumnName)); params.put("$json", row.get(JSON_COLUMN)); @@ -328,7 +350,8 @@ protected Iterable doExecuteMapValueQuery(NosqlQuery query, final Map params = new LinkedHashMap<>(); String sql = query.generateSql(entityInformation.getTableName(), params, - idPropertyName); + idPropertyName, mappingNosqlConverter. + getMappingContext().getPersistentEntity(entityClass)); PreparedStatement pStmt = getPreparedStatement(entityInformation, sql); @@ -387,4 +410,12 @@ private PreparedStatement getPreparedStatement( private Iterable doQuery(QueryRequest qReq) { return new IterableUtil.IterableImpl(nosqlClient, qReq); } + + private String getAutoGenType(NosqlEntityInformation entityInformation) { + if (entityInformation.isAutoGeneratedId()) { + return (entityInformation.getIdNosqlType() == FieldValue.Type.STRING) ? + TEMPLATE_GENERATED_UUID : TEMPLATE_GENERATED_ALWAYS; + } + return ""; + } } diff --git a/src/main/java/com/oracle/nosql/spring/data/core/convert/MappingNosqlConverter.java b/src/main/java/com/oracle/nosql/spring/data/core/convert/MappingNosqlConverter.java index 2d3f4b4..4777e0c 100644 --- a/src/main/java/com/oracle/nosql/spring/data/core/convert/MappingNosqlConverter.java +++ b/src/main/java/com/oracle/nosql/spring/data/core/convert/MappingNosqlConverter.java @@ -56,6 +56,7 @@ import com.oracle.nosql.spring.data.core.mapping.BasicNosqlPersistentProperty; import com.oracle.nosql.spring.data.core.mapping.NosqlPersistentEntity; import com.oracle.nosql.spring.data.core.mapping.NosqlPersistentProperty; +import com.oracle.nosql.spring.data.repository.support.NosqlEntityInformation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -211,10 +212,17 @@ public MapValue convertObjToRow(T objectToSave, boolean skipSetId) { row.put(NosqlTemplateBase.JSON_COLUMN, valueMap); if (!skipSetId && idProperty != null) { - //todo implement composite key - row.put(idProperty.getName(), - convertObjToFieldValue(accessor.getProperty(idProperty), - idProperty, false)); + if (idProperty.isCompositeKey()) { + MapValue ids = convertObjToFieldValue( + accessor.getProperty(idProperty), + idProperty, + false).asMap(); + ids.getMap().forEach(row::put); + } else { + row.put(idProperty.getName(), + convertObjToFieldValue(accessor.getProperty(idProperty), + idProperty, false)); + } } for (NosqlPersistentProperty prop : persistentEntity) { @@ -568,26 +576,38 @@ private E convertFieldValueToObj(Class type, FieldValue idFieldValue = null; if (entity.getIdProperty() != null) { - idFieldValue = nosqlValue.asMap() - .get(entity.getIdProperty().getName()); + NosqlPersistentProperty idProperty = entity.getIdProperty(); + + if (idProperty.isCompositeKey()) { + idFieldValue = new MapValue(); + NosqlPersistentEntity idEntity = + mappingContext.getPersistentEntity(idProperty.getType()); + for (NosqlPersistentProperty p : idEntity) { + idFieldValue.asMap().put(p.getName(), + nosqlValue.asMap().get(p.getName())); + } + } else { + idFieldValue = nosqlValue.asMap() + .get(entity.getIdProperty().getName()); + } } - MapValue jsonValue; + MapValue jsonValue = null; if (nosqlValue.asMap().get(NosqlTemplateBase.JSON_COLUMN) != null) { jsonValue = nosqlValue.asMap(). - get(NosqlTemplateBase.JSON_COLUMN).asMap(); - - NosqlPersistentEntity clsEntity = - updateEntity(entity, getInstanceClass(jsonValue)); - entityObj = getNewInstance(clsEntity, nosqlValue.asMap(), - jsonValue); + get(NosqlTemplateBase.JSON_COLUMN).asMap(); + } + NosqlPersistentEntity clsEntity = + updateEntity(entity, getInstanceClass(jsonValue)); + entityObj = getNewInstance(clsEntity, nosqlValue.asMap(), + jsonValue); - if (idFieldValue != null) { - setId(entityObj, idFieldValue); - } - setPojoProperties(clsEntity, entityObj, jsonValue); + if (idFieldValue != null) { + setId(entityObj, idFieldValue); } + setPojoProperties(clsEntity, entityObj, jsonValue); + } else { MapValue mapValue = nosqlValue.asMap(); String instClsStr = getInstanceClass(mapValue); @@ -811,7 +831,7 @@ private List convertArrayValueToCollection(FieldValue nosqlValue, private R getNewInstance(NosqlPersistentEntity entity, MapValue rootFieldValue, - @NonNull MapValue jsonValue) { + @Nullable MapValue jsonValue) { EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity); @@ -828,14 +848,16 @@ public T getParameterValue( NosqlPersistentProperty prop = entity.getPersistentProperty(paramName); - FieldValue value; - if (rootFieldValue == null) { + FieldValue value = null; + if (rootFieldValue == null && jsonValue != null) { value = jsonValue.get(paramName); } else { if (prop.isIdProperty()) { value = rootFieldValue.get(paramName); } else { - value = jsonValue.get(paramName); + if (jsonValue != null) { + value = jsonValue.get(paramName); + } if (value == null) { // if field is not marked id and it's not in // kv_json_ it may be an unmarked id field @@ -1115,10 +1137,15 @@ public MapValue convertIdToPrimaryKey(String idColumnName, ID id) { } MapValue row = new MapValue(); - - row.put(idColumnName, convertObjToFieldValue(id, null, false)); - //todo: add support for composite key - + if (NosqlEntityInformation.isCompositeKeyType(id.getClass())) { + /*composite key. Here convertObjToFieldValue adds #class that is + why convertObjToRow is used*/ + MapValue compositeKey = convertObjToRow(id, false); + compositeKey.get(NosqlTemplateBase.JSON_COLUMN).asMap(). + getMap().forEach(row::put); + } else { + row.put(idColumnName, convertObjToFieldValue(id, null, false)); + } return row; } diff --git a/src/main/java/com/oracle/nosql/spring/data/core/mapping/BasicNosqlPersistentProperty.java b/src/main/java/com/oracle/nosql/spring/data/core/mapping/BasicNosqlPersistentProperty.java index d2d46c0..9d9886e 100644 --- a/src/main/java/com/oracle/nosql/spring/data/core/mapping/BasicNosqlPersistentProperty.java +++ b/src/main/java/com/oracle/nosql/spring/data/core/mapping/BasicNosqlPersistentProperty.java @@ -20,6 +20,7 @@ import oracle.nosql.driver.values.FieldValue; import com.oracle.nosql.spring.data.Constants; +import com.oracle.nosql.spring.data.repository.support.NosqlEntityInformation; import org.springframework.data.geo.Point; import org.springframework.data.geo.Polygon; @@ -170,4 +171,15 @@ public static TypeCode getCodeForSerialization(Class cls) { return TypeCode.POJO; } } + + @Override + public boolean isCompositeKey() { + return isIdProperty() && + NosqlEntityInformation.isCompositeKeyType(getType()); + } + + @Override + public boolean isNosqlKey() { + return isAnnotationPresent(NosqlKey.class); + } } \ No newline at end of file diff --git a/src/main/java/com/oracle/nosql/spring/data/core/mapping/NosqlId.java b/src/main/java/com/oracle/nosql/spring/data/core/mapping/NosqlId.java index 8c19e64..d84818d 100644 --- a/src/main/java/com/oracle/nosql/spring/data/core/mapping/NosqlId.java +++ b/src/main/java/com/oracle/nosql/spring/data/core/mapping/NosqlId.java @@ -13,12 +13,23 @@ import org.springframework.data.annotation.Id; +/** + * Identifies the primary key field of the entity. Primary key can be of a + * basic type or of a type that represents a composite primary key. This + * field corresponds to the {@code PRIMARY KEY} of the corresponding Nosql + * table. Only one field of the entity can be annotated with this annotation. + * + * If using a composite primary key, the fields comprising the composite key + * must have distinct names when viewed in lower case, otherwise an error is + * raised. + */ @Id @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) public @interface NosqlId { + /** + * Specifies if values for the field are automatically generated. Valid only + * for int, Integer, long, Long, BigInteger, BigDecimal, + */ boolean generated() default false; - - //todo: will be supported in a future version - //boolean shardKey() default false; } diff --git a/src/main/java/com/oracle/nosql/spring/data/core/mapping/NosqlKey.java b/src/main/java/com/oracle/nosql/spring/data/core/mapping/NosqlKey.java new file mode 100644 index 0000000..16dac78 --- /dev/null +++ b/src/main/java/com/oracle/nosql/spring/data/core/mapping/NosqlKey.java @@ -0,0 +1,87 @@ +/*- + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +package com.oracle.nosql.spring.data.core.mapping; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static com.oracle.nosql.spring.data.Constants.NOTSET_PRIMARY_KEY_ORDER; +import static com.oracle.nosql.spring.data.Constants.NOTSET_SHARD_KEY; + +/** + * Identifies the annotated field as a component of the composite + * primary key.

+ * + * Order of the fields affects index selection on queries, and + * data distribution. Query performance is improved when field 1 + * through field N appears as query predicates where N is less than + * or equal to the total fields in the composite key.

+ * + * It is recommended to provide both {@code shardKey} and {@code order} options + * with this annotation. + *

+ *     Example creating the composite key (country, city, street):
+ *     class CompositeKey {
+ *          @NosqlKey(shardKey = true, order = 1)
+ *          private String country;
+ *
+ *          @NosqlKey(shardKey = false, order = 2)
+ *          private String city;
+ *
+ *          @NosqlKey(shardKey = false, order = 3)
+ *          private String street;
+ *     }
+ * 
+ * Queries using country, or country and city, or country and city and street + * as filtering predicates are faster than queries specifying only street. + * + * @since 1.6.0 + */ +@Documented +@Retention(value = RetentionPolicy.RUNTIME) +@Target(value = {ElementType.ANNOTATION_TYPE, ElementType.FIELD, + ElementType.METHOD}) +public @interface NosqlKey { + /** + * Specifies whether the field is part of the shard key or not. Default value + * is {@link com.oracle.nosql.spring.data.Constants#NOTSET_SHARD_KEY}. + * Shard keys affect distribution of rows across shards and atomicity of + * operations on a single shard. + * @since 1.6.0 + */ + boolean shardKey() default NOTSET_SHARD_KEY; + + /** + * Specifies the order of the field related to other fields listed in the + * composite key class.

+ * This ordering is used in table creation DDL to specify PRIMARY KEY and + * SHARD KEY ordering.

+ * Ordering is done based on below rules:

  • + * Shard keys are placed at the beginning.
  • + * When using the order option:
    • + * It must be specified on all the fields otherwise it is an error. + *
    • + * It must be unique otherwise it is an error. + *
    • + * Order of the shard keys must be less than the order of non + * shard keys otherwise it is an error. + *
    • + *
  • + * If {@code order} is not specified then fields are sorted + * alphabetically using lower case field names for fields grouped by the + * same shardKey value.

+ * Default value is + * {@link com.oracle.nosql.spring.data.Constants#NOTSET_PRIMARY_KEY_ORDER} + * + * @since 1.6.0 + */ + int order() default NOTSET_PRIMARY_KEY_ORDER; +} diff --git a/src/main/java/com/oracle/nosql/spring/data/core/mapping/NosqlPersistentProperty.java b/src/main/java/com/oracle/nosql/spring/data/core/mapping/NosqlPersistentProperty.java index ae1c08f..959c687 100644 --- a/src/main/java/com/oracle/nosql/spring/data/core/mapping/NosqlPersistentProperty.java +++ b/src/main/java/com/oracle/nosql/spring/data/core/mapping/NosqlPersistentProperty.java @@ -62,4 +62,18 @@ public boolean isAtomic() { * @return The property type code */ TypeCode getTypeCode(); + + /** + * Whether the property is a composite primary key. + * + * @since 1.6.0 + */ + boolean isCompositeKey(); + + /** + * Whether the property is a {@link NosqlKey} + * + * @since 1.6.0 + */ + boolean isNosqlKey(); } diff --git a/src/main/java/com/oracle/nosql/spring/data/core/query/CriteriaQuery.java b/src/main/java/com/oracle/nosql/spring/data/core/query/CriteriaQuery.java index e9f4054..c40e179 100644 --- a/src/main/java/com/oracle/nosql/spring/data/core/query/CriteriaQuery.java +++ b/src/main/java/com/oracle/nosql/spring/data/core/query/CriteriaQuery.java @@ -134,7 +134,8 @@ private Optional getSubjectCriteria(@NonNull Criteria criteria, @Override public String generateSql(String tableName, - final Map params, String idPropertyName) { + final Map params, String idPropertyName, + NosqlPersistentEntity entity) { String sql = "select " + (isDistinct ? "distinct " : "") + @@ -150,9 +151,14 @@ public String generateSql(String tableName, if (getSort().isSorted()) { sql += " ORDER BY "; sql += getSort().stream().map(order -> ( - getSqlField(order.getProperty(), - order.getProperty().equals(idPropertyName)) + - (order.isAscending() ? " ASC" : " DESC"))) + getSqlField(order.getProperty(), + mappingContext.getPersistentPropertyPath( + order.getProperty(), entity.getType()). + getRequiredLeafProperty(), + mappingContext.getPersistentPropertyPath( + order.getProperty(), entity.getType()). + getBaseProperty() + ) + (order.isAscending() ? " ASC" : " DESC"))) .collect(Collectors.joining(",")); } @@ -381,13 +387,22 @@ private String getSqlFieldWithCast(@NonNull Criteria crt) { PersistentPropertyPath path = mappingContext.getPersistentPropertyPath(crt.getPart().getProperty()); NosqlPersistentProperty property = path.getLeafProperty(); - - return getSqlFieldWithCast(crt.getSubject(), property); + NosqlPersistentProperty parentProperty = path.getBaseProperty(); + return getSqlFieldWithCast(crt.getSubject(), property, parentProperty); } private String getSqlFieldWithCast(@NonNull String field, - NosqlPersistentProperty property) { - String result = getSqlField(field, property.isIdProperty()); + NosqlPersistentProperty property, + NosqlPersistentProperty parentProperty) { + String result; + /* If property is part of composite key use property name instead of + hierarchical name*/ + if (property.isNosqlKey() || + parentProperty.isIdProperty()) { + result = getSqlField(property.getName(), true); + } else { + result = getSqlField(field, property.isIdProperty()); + } if (requiresTimestampCast(property)) { result = "cast(" + result + " as " + @@ -400,12 +415,18 @@ private String getSqlField(@NonNull Criteria crt) { PersistentPropertyPath path = mappingContext.getPersistentPropertyPath(crt.getPart().getProperty()); NosqlPersistentProperty property = path.getLeafProperty(); + NosqlPersistentProperty parentProperty = path.getBaseProperty(); - return getSqlField(crt.getSubject(), property); + return getSqlField(crt.getSubject(), property, parentProperty); } private String getSqlField(@NonNull String field, - NosqlPersistentProperty property) { + @NonNull NosqlPersistentProperty property, + @Nullable NosqlPersistentProperty parentProperty) { + if (property.isNosqlKey() || + (parentProperty != null && parentProperty.isIdProperty())) { + return getSqlField(property.getName(), true); + } return getSqlField(field, property.isIdProperty()); } @@ -484,19 +505,30 @@ private String generateProjection(String idPropertyName) { List nonKeyFields = new ArrayList<>(); inputProperties - .stream() - .forEach( prop -> { - NosqlPersistentProperty pp = mappingContext - .getPersistentPropertyPath(prop, - returnedType.getReturnedType()).getBaseProperty(); - - String field = getSqlField(prop, pp); - if (pp.getName().equals(idPropertyName)) { - keyFields.add(getSqlField(pp.getName(), true)); - } else { - nonKeyFields.add(field); - } - }); + .stream() + .forEach(prop -> { + NosqlPersistentProperty pp = mappingContext + .getPersistentPropertyPath(prop, + returnedType.getDomainType()).getBaseProperty(); + + if (pp.isCompositeKey()) { + NosqlPersistentEntity compositeEntity = + (NosqlPersistentEntity) mappingContext.getRequiredPersistentEntity(pp); + compositeEntity.forEach(idProperty -> { + if (idProperty.isWritable()) { + keyFields.add(getSqlField(idProperty.getName(), + true)); + } + }); + } else { + String field = getSqlField(prop, pp, null); + if (pp.getName().equals(idPropertyName)) { + keyFields.add(getSqlField(pp.getName(), true)); + } else { + nonKeyFields.add(field); + } + } + }); String nonKeysProj = nonKeyFields.stream() .map(f -> "'" + f.substring(f.lastIndexOf('.') + 1) + diff --git a/src/main/java/com/oracle/nosql/spring/data/core/query/NosqlQuery.java b/src/main/java/com/oracle/nosql/spring/data/core/query/NosqlQuery.java index b3af79c..510186d 100644 --- a/src/main/java/com/oracle/nosql/spring/data/core/query/NosqlQuery.java +++ b/src/main/java/com/oracle/nosql/spring/data/core/query/NosqlQuery.java @@ -8,6 +8,7 @@ import java.util.Map; +import com.oracle.nosql.spring.data.core.mapping.NosqlPersistentEntity; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.lang.NonNull; @@ -55,7 +56,8 @@ public Sort getSort() { } public abstract String generateSql(String tableName, - final Map params, String idPropertyName); + final Map params, String idPropertyName, + NosqlPersistentEntity entity); public NosqlQuery setCount(boolean isCount) { this.isCount = isCount; diff --git a/src/main/java/com/oracle/nosql/spring/data/core/query/StringQuery.java b/src/main/java/com/oracle/nosql/spring/data/core/query/StringQuery.java index 045a8e7..0a2cb94 100644 --- a/src/main/java/com/oracle/nosql/spring/data/core/query/StringQuery.java +++ b/src/main/java/com/oracle/nosql/spring/data/core/query/StringQuery.java @@ -8,6 +8,7 @@ import java.util.Map; +import com.oracle.nosql.spring.data.core.mapping.NosqlPersistentEntity; import com.oracle.nosql.spring.data.repository.query.NosqlParameterAccessor; import com.oracle.nosql.spring.data.repository.query.NosqlQueryMethod; @@ -28,7 +29,8 @@ public StringQuery(NosqlQueryMethod method, String query, @Override public String generateSql(String tableName, - Map params, String idPropertyName) { + Map params, String idPropertyName, + NosqlPersistentEntity entity) { Parameters methodParams = method.getParameters(); int i = 0; diff --git a/src/main/java/com/oracle/nosql/spring/data/repository/support/NosqlEntityInformation.java b/src/main/java/com/oracle/nosql/spring/data/repository/support/NosqlEntityInformation.java index dc32781..b11210e 100644 --- a/src/main/java/com/oracle/nosql/spring/data/repository/support/NosqlEntityInformation.java +++ b/src/main/java/com/oracle/nosql/spring/data/repository/support/NosqlEntityInformation.java @@ -11,6 +11,7 @@ import com.oracle.nosql.spring.data.core.NosqlTemplateBase; import com.oracle.nosql.spring.data.core.mapping.NosqlCapacityMode; import com.oracle.nosql.spring.data.core.mapping.NosqlId; +import com.oracle.nosql.spring.data.core.mapping.NosqlKey; import com.oracle.nosql.spring.data.core.mapping.NosqlTable; import oracle.nosql.driver.Consistency; import oracle.nosql.driver.Durability; @@ -21,6 +22,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.core.env.Environment; import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Transient; import org.springframework.data.repository.core.support.AbstractEntityInformation; import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider; @@ -31,13 +33,23 @@ import org.springframework.util.ReflectionUtils; import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.math.BigDecimal; import java.math.BigInteger; import java.sql.Timestamp; import java.time.Instant; +import java.util.ArrayList; import java.util.Collections; import java.util.Date; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +import static com.oracle.nosql.spring.data.Constants.NOTSET_PRIMARY_KEY_ORDER; +import static com.oracle.nosql.spring.data.Constants.NOTSET_SHARD_KEY; public class NosqlEntityInformation extends AbstractEntityInformation { @@ -54,7 +66,8 @@ public class NosqlEntityInformation extends private final FieldValue.Type idNosqlType; private boolean useDefaultTableLimits = false; private TimeToLive ttl; -// private boolean isComposite; + private Map shardKeys; + private Map nonShardKeys; public NosqlEntityInformation(ApplicationContext applicationContext, Class domainClass) { @@ -63,7 +76,7 @@ public NosqlEntityInformation(ApplicationContext applicationContext, this.applicationContext = applicationContext; this.id = getIdField(domainClass); ReflectionUtils.makeAccessible(this.id); - idNosqlType = findIdNosqlType(); + idNosqlType = findIdNosqlType(getIdType()); final NosqlId nosqlIdAnn = id.getAnnotation(NosqlId.class); if (nosqlIdAnn != null && nosqlIdAnn.generated()) { @@ -110,8 +123,7 @@ public FieldValue.Type getIdNosqlType() { return idNosqlType; } - private FieldValue.Type findIdNosqlType() { - Class idClass = getIdType(); + public static FieldValue.Type findIdNosqlType(Class idClass) { if (idClass == String.class) { return FieldValue.Type.STRING; } @@ -132,7 +144,8 @@ private FieldValue.Type findIdNosqlType() { Instant.class) { return FieldValue.Type.TIMESTAMP; } - throw new IllegalStateException("Unsupported ID type."); + //Might be composite key. return MAP type + return FieldValue.Type.MAP; } public String getTableName() { @@ -183,37 +196,83 @@ private Field getIdField(Class domainClass) { throw new IllegalArgumentException("Entity should contain @Id or " + "@NosqlId annotated field or field named id: " + domainClass.getName()); - } else if (idField.getType() != String.class && - idField.getType() != Integer.class && - idField.getType() != int.class && - idField.getType() != Long.class && - idField.getType() != long.class && - idField.getType() != Float.class && - idField.getType() != float.class && - idField.getType() != Double.class && - idField.getType() != double.class && - idField.getType() != BigInteger.class && - idField.getType() != BigDecimal.class && - idField.getType() != Timestamp.class && - idField.getType() != Date.class && - idField.getType() != Instant.class - //todo: implement composite keys - ) { - throw new IllegalArgumentException("Id field must be of " + - "type java.lang.String, int, java.lang.Integer, long, " + - "java.lang.Long, java.math.BigInteger, java.math.BigDecimal, " + - "java.sql.Timestamp, java.util.Date or java.time.Instant in " + - domainClass.getName()); } - if (NosqlTemplateBase.JSON_COLUMN.equals(idField.getName())) { throw new IllegalArgumentException("Id field can not be named '" + NosqlTemplateBase.JSON_COLUMN + "' in " + domainClass.getName()); } + //If composite key class check it has only primitive types + if (isCompositeKeyType(idField.getType())) { + for (Field primaryKey : idField.getType().getDeclaredFields()) { + if (!primaryKey.isAnnotationPresent(Transient.class) && + !Modifier.isStatic(primaryKey.getModifiers())) { + if (isCompositeKeyType(primaryKey.getType())) { + throw new IllegalArgumentException(String.format( + "field '%s' must be one of type java.lang" + + ".String," + + " int, java.lang.Integer, long, java" + + ".lang" + + ".Long," + + " java.math.BigInteger, java.math" + + ".BigDecimal," + + " java.sql.Timestamp, java.util.Date " + + "or" + + " java.time.Instant in %s", + primaryKey.getName(), + idField.getType().getName()) + ); + } + if (NosqlTemplateBase.JSON_COLUMN.equals(primaryKey.getName())) { + throw new IllegalArgumentException("primary key " + + "field of the composite key can not be named " + + "'" + NosqlTemplateBase.JSON_COLUMN + "' in " + + idField.getType().getName()); + } + } + } + } + + ProcessPrimaryKeys ppKeys = new ProcessPrimaryKeys(idField); + shardKeys = ppKeys.shardKeys; + nonShardKeys = ppKeys.nonShardKeys; + + for (String nkey : nonShardKeys.keySet()) { + for (String sKey : shardKeys.keySet()) { + if (nkey.equalsIgnoreCase(sKey)) { + throw new IllegalArgumentException(String.format( + "Conflicting name %s " + + "for primary key in " + + "composite key class " + + "%s", nkey, + idField.getType().getName()) + ); + } + } + } return idField; } + public static boolean isAllowedKeyType(Class type) { + return type == String.class || + type == Integer.class || + type == int.class || + type == Long.class || + type == long.class || + type == Float.class || + type == float.class || + type == Double.class || + type == double.class || + type == BigInteger.class || + type == BigDecimal.class || + type == Timestamp.class || + type == Date.class || + type == Instant.class; + } + + public static boolean isCompositeKeyType(Class type) { + return !isAllowedKeyType(type); + } private void setTableOptions(Class domainClass) { autoCreateTable = Constants.DEFAULT_AUTO_CREATE_TABLE; @@ -241,8 +300,7 @@ private void setTableOptions(Class domainClass) { annotation.writeUnits(), annotation.storageGB()); } else if (annotation.capacityMode() == NosqlCapacityMode.ON_DEMAND && (annotation.storageGB() > 0 || annotation.storageGB() == - Constants.NOTSET_TABLE_STORAGE_GB )) - { + Constants.NOTSET_TABLE_STORAGE_GB )) { tableLimits = new TableLimits(annotation.storageGB()); } @@ -395,4 +453,165 @@ public void setTimeout(int milliseconds) { public TimeToLive getTtl() { return ttl; } + + public Map getShardKeys() { + return shardKeys; + } + + public Map getNonShardKeys() { + return nonShardKeys; + } + + private static class ProcessPrimaryKeys { + private Map shardKeys; + private Map nonShardKeys; + + public ProcessPrimaryKeys(Field idField) { + shardKeys = new LinkedHashMap<>(); + nonShardKeys = new LinkedHashMap<>(); + process(idField); + } + + private void process(Field idField) { + Class idFieldClass = idField.getType(); + + if (isCompositeKeyType(idField.getType())) { + //composite key + Map> shardMap = new TreeMap<>(); + Map> nonShardMap = new TreeMap<>(); + + for (Field primaryKey : idField.getType().getDeclaredFields()) { + if (!primaryKey.isAnnotationPresent(Transient.class) && + !Modifier.isStatic(primaryKey.getModifiers())) { + + int order = NOTSET_PRIMARY_KEY_ORDER; + boolean isShard = NOTSET_SHARD_KEY; + + if (primaryKey.isAnnotationPresent(NosqlKey.class)) { + NosqlKey nosqlKey = + primaryKey.getAnnotation(NosqlKey.class); + order = nosqlKey.order(); + isShard = nosqlKey.shardKey(); + } + //If shard save in shardMap + if (isShard) { + SortedSet ss = shardMap.getOrDefault(order, + new TreeSet<>(String.CASE_INSENSITIVE_ORDER)); + if (ss.contains(primaryKey.getName())) { + throw new IllegalArgumentException( + String.format("Conflicting name %s " + + "for primary key in " + + "composite key class " + + "%s", + primaryKey.getName(), + idFieldClass) + ); + } + ss.add(primaryKey.getName()); + shardMap.put(order, ss); + } else { + //save in nonShardMap + SortedSet ss = + nonShardMap.getOrDefault(order, + new TreeSet<>(String.CASE_INSENSITIVE_ORDER)); + if (ss.contains(primaryKey.getName())) { + throw new IllegalArgumentException( + String.format("Conflicting name %s " + + "for primary key in " + + "composite key class " + + "%s", + primaryKey.getName(), + idFieldClass) + ); + } + ss.add(primaryKey.getName()); + nonShardMap.put(order, ss); + } + } + } + + List sortedShardKeys = new ArrayList<>(); + List sortedNonShardKeys = new ArrayList<>(); + + shardMap.forEach((order, keys) -> { + //order should be specified on all fields if at all is used + if (order != NOTSET_PRIMARY_KEY_ORDER && + shardMap.get(NOTSET_PRIMARY_KEY_ORDER) != null) { + throw new IllegalArgumentException("If order is " + + "specified, it must be specified on all key" + + " fields of the composite key class " + + idField.getType().getName()); + } + + //order value must be unique + if (order != NOTSET_PRIMARY_KEY_ORDER && keys.size() > 1) { + throw new IllegalArgumentException("Order of " + + "keys must be unique in composite key " + + "class " + idFieldClass.getName()); + } + sortedShardKeys.addAll(keys); + }); + + nonShardMap.forEach((order, keys) -> { + //order should be specified on all fields if at all is used + if (order != NOTSET_PRIMARY_KEY_ORDER && + nonShardMap.get(NOTSET_PRIMARY_KEY_ORDER) != null) { + throw new IllegalArgumentException("If order is " + + "specified, it must be specified on all key" + + " fields of the composite key class " + + idField.getType().getName()); + } + + //order value must be unique + if (order != NOTSET_PRIMARY_KEY_ORDER && keys.size() > 1) { + throw new IllegalArgumentException("Order of " + + "keys must be unique in composite key " + + "class " + idFieldClass.getName()); + } + sortedNonShardKeys.addAll(keys); + }); + + if (sortedShardKeys.isEmpty()) { + throw new IllegalArgumentException("At least one of the " + + "@NosqlKey must be shard key in class " + + idFieldClass.getName()); + } + + if (!nonShardMap.isEmpty()) { + int shardMaxOrder = + (int) ((TreeMap) shardMap).lastKey(); + int nonShardMinOrder = + (int) ((TreeMap) nonShardMap).firstKey(); + + if (shardMaxOrder != -1 && nonShardMinOrder <= shardMaxOrder) { + throw new IllegalArgumentException("Order of non " + + "shard " + + "keys must be greater than all the shard keys" + + " in " + + "the composite key class " + idFieldClass.getName()); + } + } + + sortedShardKeys.forEach(keyName -> { + Field field = ReflectionUtils.findField(idField.getType(), + keyName); + shardKeys.put(keyName, + NosqlEntityInformation.findIdNosqlType( + field.getType())); + }); + + sortedNonShardKeys.forEach(keyName -> { + Field field = ReflectionUtils.findField(idField.getType(), + keyName); + nonShardKeys.put(keyName, + NosqlEntityInformation.findIdNosqlType( + field.getType())); + }); + } else { //simple key + shardKeys.put(idField.getName(), + NosqlEntityInformation.findIdNosqlType(idField.getType()) + ); + } + } + } } diff --git a/src/main/java/com/oracle/nosql/spring/data/repository/support/SimpleReactiveNosqlRepository.java b/src/main/java/com/oracle/nosql/spring/data/repository/support/SimpleReactiveNosqlRepository.java index eb16a92..4156928 100644 --- a/src/main/java/com/oracle/nosql/spring/data/repository/support/SimpleReactiveNosqlRepository.java +++ b/src/main/java/com/oracle/nosql/spring/data/repository/support/SimpleReactiveNosqlRepository.java @@ -55,10 +55,17 @@ private void createTableIfNotExists() { public Mono save(S entity) { Assert.notNull(entity, "Entity must not be null!"); - if (entityInformation.isNew(entity)) { - return nosqlOperations.insert(entityInformation, entity); + if (entityInformation.isAutoGeneratedId()) { + // Must use update if !new + boolean isNew = entityInformation.isNew(entity); + if (isNew) { + return nosqlOperations.insert(entityInformation, entity); + } else { + return nosqlOperations.update(entityInformation, entity); + } } else { - return nosqlOperations.update(entityInformation, entity); + // do a put (set or insert) if !isAutoGeneratedId + return nosqlOperations.insert(entityInformation, entity); } } @@ -191,7 +198,7 @@ public Flux findAll(Sort sort) { final NosqlQuery query = new CriteriaQuery(Criteria.getInstance(CriteriaType.ALL), - null).with(sort); + nosqlOperations.getConverter().getMappingContext()).with(sort); return nosqlOperations.find(query, entityInformation); } diff --git a/src/test/java/com/oracle/nosql/spring/data/test/composite/IpAddress.java b/src/test/java/com/oracle/nosql/spring/data/test/composite/IpAddress.java new file mode 100644 index 0000000..7c418c6 --- /dev/null +++ b/src/test/java/com/oracle/nosql/spring/data/test/composite/IpAddress.java @@ -0,0 +1,35 @@ +/*- + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +package com.oracle.nosql.spring.data.test.composite; + +import java.util.Objects; + +public class IpAddress { + private final String ip; + + public IpAddress(String ip) { + this.ip = ip; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof IpAddress)) { + return false; + } + IpAddress ipAddress = (IpAddress) o; + return Objects.equals(ip, ipAddress.ip); + } + + @Override + public int hashCode() { + return Objects.hash(ip); + } +} diff --git a/src/test/java/com/oracle/nosql/spring/data/test/composite/Machine.java b/src/test/java/com/oracle/nosql/spring/data/test/composite/Machine.java new file mode 100644 index 0000000..7a397ca --- /dev/null +++ b/src/test/java/com/oracle/nosql/spring/data/test/composite/Machine.java @@ -0,0 +1,101 @@ +/*- + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +package com.oracle.nosql.spring.data.test.composite; + +import com.oracle.nosql.spring.data.core.mapping.NosqlId; +import com.oracle.nosql.spring.data.core.mapping.NosqlTable; +import org.springframework.data.annotation.Transient; + +import java.util.Date; +import java.util.List; +import java.util.Objects; + +@NosqlTable(autoCreateTable = true, readUnits = 100, writeUnits = 100, + storageGB = 1) +public class Machine { + @NosqlId + MachineId machineId; + private String location; + private Date creationDate = new Date(); + private IpAddress hostAddress; + private List routeAddress; + private int version = -1; //version as both top level property and + // property in MachineId class + @Transient + private final String transientString = "temp"; + + public Machine(MachineId machineId, String location, + IpAddress hostAddress, List routeAddress) { + this.machineId = machineId; + this.location = location; + this.hostAddress = hostAddress; + this.routeAddress = routeAddress; + } + + public MachineId getMachineId() { + return machineId; + } + + public void setMachineId(MachineId machineId) { + this.machineId = machineId; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + public Date getCreationDate() { + return creationDate; + } + + public void setCreationDate(Date creationDate) { + this.creationDate = creationDate; + } + + public IpAddress getHostAddress() { + return hostAddress; + } + + public List getRouteAddress() { + return routeAddress; + } + + public void setHostAddress(IpAddress hostAddress) { + this.hostAddress = hostAddress; + } + + public void setRouteAddress(List routeAddress) { + this.routeAddress = routeAddress; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Machine)) { + return false; + } + Machine machine = (Machine) o; + return Objects.equals(machineId, machine.machineId) && + Objects.equals(location, machine.location) && + Objects.equals(creationDate, machine.creationDate) && + Objects.equals(hostAddress, machine.hostAddress) && + Objects.equals(routeAddress, machine.routeAddress) && + Objects.equals(version, machine.version); + } + + @Override + public int hashCode() { + return Objects.hash(machineId, location, creationDate, hostAddress, routeAddress, version); + } +} diff --git a/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineApp.java b/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineApp.java new file mode 100644 index 0000000..93634bb --- /dev/null +++ b/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineApp.java @@ -0,0 +1,314 @@ +/*- + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +package com.oracle.nosql.spring.data.test.composite; + +import com.oracle.nosql.spring.data.core.NosqlTemplate; +import com.oracle.nosql.spring.data.test.app.AppConfig; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertNotNull; +import static junit.framework.TestCase.assertNull; +import static junit.framework.TestCase.assertTrue; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = AppConfig.class) +public class MachineApp { + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") + @Autowired + private MachineRepository repo; + private Map machineCache; + + + private static NosqlTemplate template; + + @BeforeClass + public static void staticSetup() throws ClassNotFoundException { + template = NosqlTemplate.create(AppConfig.nosqlDBConfig); + } + + @Before + public void setup() { + template.dropTableIfExists(Machine.class.getSimpleName()); + + machineCache = new HashMap<>(); + List routeAddress = new ArrayList<>(); + routeAddress.add(new IpAddress("127.0.0.1")); + routeAddress.add(new IpAddress("host1")); + routeAddress.add(new IpAddress("host2")); + + //create machines + for (int i = 1; i <= 4; i++) { + for (int j = 1; j <= 4; j++) { + MachineId machineId = new MachineId(); + machineId.setName("name" + i); + machineId.setVersion("version" + j); + Machine machine = new Machine(machineId, (i % 2 == 0) ? + "london" : "newyork", routeAddress.get(0), + routeAddress); + repo.save(machine); + machineCache.put(machineId, machine); + } + } + + //get total count of records + assertEquals(16, repo.count()); + + } + + @After + public void teardown() { + template.dropTableIfExists(Machine.class.getSimpleName()); + } + + @Test + public void testCRUD() { + //get machines + machineCache.forEach((machineId, machine) -> assertEquals(machine, + repo.findById(machineId).orElse(null))); + + //update a machine + Machine updateMachine = machineCache.get(new MachineId("version1", + "name1")); + updateMachine.setLocation("mumbai"); + repo.save(updateMachine); + assertEquals(updateMachine, + repo.findById(new MachineId("version1", "name1")).orElse(null)); + + //find bases on machineId + MachineId machineId = new MachineId("version1", "name1"); + Optional row = repo.findById(machineId); + assertTrue(row.isPresent()); + assertEquals(machineCache.get(machineId), row.get()); + + //delete some rows + repo.deleteById(new MachineId("version1", "name1")); + assertNull(repo.findById(new MachineId("version1", "name1")).orElse(null)); + + repo.deleteById(new MachineId("version2", "name2")); + assertNull(repo.findById(new MachineId("version2", "name2")).orElse(null)); + + assertEquals(14, repo.count()); + } + + @Test + public void testCompositeKeyGet() { + //find all machines with machineId.version=1 + List machines = repo.findByMachineIdVersion("version1"); + assertEquals(4, machines.size()); + machines.forEach(m -> assertEquals("version1", + m.getMachineId().getVersion())); + machines.forEach(m -> assertEquals(machineCache.get(m.getMachineId()) + , m)); + + + //find all machines with machineID.name=name3 + machines = repo.findByMachineIdName("name3"); + assertEquals(4, machines.size()); + machines.forEach(m -> assertEquals("name3", + m.getMachineId().getName())); + machines.forEach(m -> assertEquals(machineCache.get(m.getMachineId()) + , m)); + + //find all rows located in london + machines = repo.findByLocation("london"); + assertEquals(8, machines.size()); + machines.forEach(m -> assertEquals("london", m.getLocation())); + machines.forEach(m -> assertEquals(machineCache.get(m.getMachineId()) + , m)); + } + + @Test + public void testCompositeKeyLogical() { + //find all machines name=name1 and version=1 + List machines = repo.findByMachineIdNameAndMachineIdVersion( + "name1", + "version1"); + assertEquals(1, machines.size()); + machines.forEach(m -> assertTrue( + m.getMachineId().getName().equals("name1") && + m.getMachineId().getVersion().equals("version1") + )); + machines.forEach(m -> assertEquals(machineCache.get(m.getMachineId()) + , m)); + + //find all machines name=name1 or version=1 + machines = repo.findByMachineIdNameOrMachineIdVersion( + "name1", + "version1"); + assertEquals(7, machines.size()); + machines.forEach(m -> assertTrue( + m.getMachineId().getName().equals("name1") || + m.getMachineId().getVersion().equals("version1") + )); + machines.forEach(m -> assertEquals(machineCache.get(m.getMachineId()) + , m)); + + //find all machines name=name1 and location=london + machines = repo.findByMachineIdNameAndLocation( + "name1", + "newyork"); + assertEquals(4, machines.size()); + machines.forEach(m -> assertTrue( + m.getMachineId().getName().equals("name1") && + m.getLocation().equals("newyork") + )); + machines.forEach(m -> assertEquals(machineCache.get(m.getMachineId()) + , m)); + } + + @Test + public void testCompositeSortingAndPaging() { + //find all machines with machineId.version=1 with sort by name + List machines = + repo.findByMachineIdVersionOrderByMachineIdNameAsc("version1"); + assertEquals(4, machines.size()); + machines.forEach(m -> assertEquals("version1", + m.getMachineId().getVersion())); + machines.forEach(m -> assertEquals(machineCache.get(m.getMachineId()) + , m)); + //check sort by name is correct + String prev = ""; + for (Machine m : machines) { + String cur = m.getMachineId().getName(); + assertTrue(cur.compareTo(prev) >= 0); + prev = cur; + } + + + Sort sort = Sort.by(Sort.Direction.DESC, "machineId.version"); + Pageable pageable = PageRequest.of(0, 2, sort); + Page pageByNameQuery = repo.findByMachineIdName("name1", + pageable); + for (int page = 1; !pageByNameQuery.isEmpty(); page++) { + assertEquals(2, pageByNameQuery.getTotalElements()); + for (Machine m : pageByNameQuery) { + assertEquals("name1", m.getMachineId().getName()); + assertEquals(machineCache.get(m.getMachineId()), m); + } + pageable = PageRequest.of(page, 2, sort); + pageByNameQuery = repo.findByMachineIdName("linux", pageable); + } + } + + @Test + public void testFindAllSort() { + Iterable machines = repo.findAll( + Sort.by("machineId.name", "machineId.version")); + List machineList = new ArrayList<>(); + machines.forEach(machineList::add); + machineList.forEach(m -> assertEquals(machineCache.get(m.getMachineId()), m)); + + List expectedNames = Arrays.asList( + "name1", "name1", "name1", "name1", + "name2", "name2", "name2", "name2", + "name3", "name3", "name3", "name3", + "name4", "name4", "name4", "name4"); + List actualNames = new ArrayList<>(); + machineList.forEach(m -> actualNames.add(m.getMachineId().getName())); + assertEquals(expectedNames, actualNames); + + List locationList = repo.findAllByOrderByLocation(); + List expectedLocations = Arrays.asList( + "london", "london", "london", "london", + "london", "london", "london", "london", + "newyork", "newyork", "newyork", "newyork", + "newyork", "newyork", "newyork", "newyork" + ); + assertEquals(16, locationList.size()); + List actualLocations = new ArrayList<>(); + locationList.forEach(machine -> actualLocations.add(machine.getLocation())); + assertEquals(expectedLocations, actualLocations); + } + + @Test + public void testNative() { + //native query + List machines = repo.findByVersionNative(); + assertEquals(4, machines.size()); + machines.forEach(m -> assertEquals("version1", + m.getMachineId().getVersion())); + machines.forEach(m -> assertEquals(machineCache.get(m.getMachineId()) + , m)); + } + + @Test + public void testIgnoreCase() { + //ignore case + List machines = repo.findByMachineIdNameIgnoreCase("NaMe1"); + assertEquals(4, machines.size()); + machines.forEach(m -> assertEquals("name1", + m.getMachineId().getName())); + machines.forEach(m -> assertEquals(machineCache.get(m.getMachineId()) + , m)); + + machines = repo.findByMachineIdNameRegexIgnoreCase("NaMe1"); + assertEquals(4, machines.size()); + machines.forEach(m -> assertEquals("name1", + m.getMachineId().getName())); + machines.forEach(m -> assertEquals(machineCache.get(m.getMachineId()) + , m)); + } + + @Test + public void testInterfaceProjection() { + List projections = repo.findAllByLocation("london"); + projections.forEach(m -> { + assertNotNull(m.getMachineId().getName()); + assertNotNull(m.getMachineId().getVersion()); + assertEquals("london", m.getLocation()); + }); + + List projectionOnlyIds = repo. + findAllByLocationNativeProjection(); + assertEquals(8, projectionOnlyIds.size()); + projectionOnlyIds.forEach(m -> { + assertNotNull(m.getMachineId()); + assertNotNull(m.getMachineId().getName()); + assertNotNull(m.getMachineId().getVersion()); + }); + } + + @Test + public void testDTOProjection() { + List projections = repo.findAllByMachineIdName( + "name1"); + projections.forEach(m -> { + assertEquals("name1", m.getMachineId().getName()); + assertNotNull(m.getMachineId().getVersion()); + assertNotNull(m.getLocation()); + }); + + List projectionDTOOnlyIds = repo. + findAllByLocationNativeDTOProjection(); + assertEquals(8, projectionDTOOnlyIds.size()); + projectionDTOOnlyIds.forEach(m -> { + assertNotNull(m.getMachineId()); + assertNotNull(m.getMachineId().getName()); + assertNotNull(m.getMachineId().getVersion()); + }); + } +} diff --git a/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineAppWithoutAnnotation.java b/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineAppWithoutAnnotation.java new file mode 100644 index 0000000..13fa84f --- /dev/null +++ b/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineAppWithoutAnnotation.java @@ -0,0 +1,281 @@ +/*- + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +package com.oracle.nosql.spring.data.test.composite; + +import com.oracle.nosql.spring.data.core.NosqlTemplate; +import com.oracle.nosql.spring.data.core.mapping.NosqlId; +import com.oracle.nosql.spring.data.core.mapping.NosqlTable; +import com.oracle.nosql.spring.data.repository.NosqlRepository; +import com.oracle.nosql.spring.data.repository.Query; +import com.oracle.nosql.spring.data.test.app.AppConfig; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertNull; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = AppConfig.class) +public class MachineAppWithoutAnnotation { + @Autowired + private MachineRepositoryWithoutAnnotation repo; + + private static NosqlTemplate template; + + @BeforeClass + public static void staticSetup() throws ClassNotFoundException { + template = NosqlTemplate.create(AppConfig.nosqlDBConfig); + } + + @Before + public void setup() { + template.dropTableIfExists(MachineWithoutAnnotation.class.getSimpleName()); + } + + @After + public void teardown() { + template.dropTableIfExists(MachineWithoutAnnotation.class.getSimpleName()); + } + + @Test + public void testCRUD() { + Map map = + new HashMap<>(); + + //create machines + for (int i = 1; i <= 4; i++) { + for (int j = 1; j <= 4; j++) { + MachineIdWithoutAnnotation machineId = + new MachineIdWithoutAnnotation(); + machineId.setName("name" + i); + machineId.setVersion("version" + j); + MachineWithoutAnnotation machine = + new MachineWithoutAnnotation(); + machine.setMachineId(machineId); + machine.setLocation((i % 2 == 0) ? "london" : "newyork"); + repo.save(machine); + map.put(machineId, machine); + } + } + + //get total count of records + assertEquals(16, repo.count()); + + //get machines + map.forEach((machineId, machine) -> assertEquals(machine, + repo.findById(machineId).orElse(null))); + + + //update a machine + MachineWithoutAnnotation updateMachine = + map.get(new MachineIdWithoutAnnotation( + "version1", + "name1")); + updateMachine.setLocation("mumbai"); + repo.save(updateMachine); + assertEquals(updateMachine, + repo.findById(new MachineIdWithoutAnnotation("version1", + "name1")).orElse(null)); + + + //find all machines with machineId.version=1 + List res = + repo.findByMachineIdVersionOrderByMachineIdNameAsc("version1"); + assertEquals(4, res.size()); + res.forEach(m -> assertEquals(map.get(m.getMachineId()), m)); + + //find all machines with machineId.version=2 + res = repo.findByMachineIdVersionOrderByMachineIdNameAsc("version2"); + assertEquals(4, res.size()); + res.forEach(m -> assertEquals(map.get(m.getMachineId()), m)); + + + //find all machines with machineID.name=name3 + res = repo.findByMachineIdName("name3"); + assertEquals(4, res.size()); + res.forEach(m -> assertEquals(map.get(m.getMachineId()), m)); + + + //find all rows located in mumbai + res = repo.findByLocation("mumbai"); + assertEquals(1, res.size()); + res.forEach(m -> assertEquals(map.get(m.getMachineId()), m)); + + //find all machines name=name1 and version=1 + res = repo.findByMachineIdNameAndMachineIdVersion( + "name1", + "version1"); + assertEquals(1, res.size()); + res.forEach(m -> assertEquals(map.get(m.getMachineId()), m)); + + //native query + res = repo.findByVersionNative(); + assertEquals(4, res.size()); + res.forEach(m -> assertEquals(map.get(m.getMachineId()), m)); + + //ignore case + res = repo.findByMachineIdNameIgnoreCase("NaMe1"); + assertEquals(4, res.size()); + res.forEach(m -> assertEquals(map.get(m.getMachineId()), m)); + + Sort sort = Sort.by(Sort.Direction.DESC, "machineId.version"); + Pageable pageable = PageRequest.of(0, 2, sort); + Page pageByNameQuery = + repo.findByMachineIdName( + "name1", + pageable); + for (int page = 1; !pageByNameQuery.isEmpty(); page++) { + for (MachineWithoutAnnotation m : pageByNameQuery) { + assertEquals(map.get(m.getMachineId()), m); + } + pageable = PageRequest.of(page, 2, sort); + pageByNameQuery = repo.findByMachineIdName("linux", pageable); + } + + //delete some rows + repo.deleteById(new MachineIdWithoutAnnotation("version1", "name1")); + assertNull(repo.findById(new MachineIdWithoutAnnotation("version1", + "name1")).orElse(null)); + + repo.deleteById(new MachineIdWithoutAnnotation("version2", "name2")); + assertNull(repo.findById(new MachineIdWithoutAnnotation("version2", + "name2")).orElse(null)); + + assertEquals(14, repo.count()); + } + + @NosqlTable(autoCreateTable = true, readUnits = 100, writeUnits = 100, + storageGB = 1) + public static class MachineWithoutAnnotation { + @NosqlId + MachineIdWithoutAnnotation machineId; + private String location; + + public MachineWithoutAnnotation() { + } + + public MachineIdWithoutAnnotation getMachineId() { + return machineId; + } + + public void setMachineId(MachineIdWithoutAnnotation machineId) { + this.machineId = machineId; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MachineWithoutAnnotation)) { + return false; + } + MachineWithoutAnnotation that = (MachineWithoutAnnotation) o; + return Objects.equals(machineId, that.machineId) && + Objects.equals(location, that.location); + } + + @Override + public int hashCode() { + return Objects.hash(machineId, location); + } + } + + public static class MachineIdWithoutAnnotation implements Serializable { + private String version; + private String name; + + public MachineIdWithoutAnnotation() { + } + + public MachineIdWithoutAnnotation(String version, String name) { + this.version = version; + this.name = name; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MachineIdWithoutAnnotation)) { + return false; + } + MachineIdWithoutAnnotation that = (MachineIdWithoutAnnotation) o; + return Objects.equals(version, that.version) && + Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(version, name); + } + } + + public static interface MachineRepositoryWithoutAnnotation extends + NosqlRepository { + List findByLocation(String location); + + List findByMachineIdVersionOrderByMachineIdNameAsc(String version); + + List findByMachineIdName(String name); + + Page findByMachineIdName(String name, + Pageable pageable); + + List findByMachineIdNameAndMachineIdVersion(String name, + String version); + + @Query("SELECT * FROM MachineWithoutAnnotation m WHERE m" + + ".VERSION='version1'") + List findByVersionNative(); + + List findByMachineIdNameIgnoreCase(String name); + + } +} diff --git a/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineId.java b/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineId.java new file mode 100644 index 0000000..d8ef86b --- /dev/null +++ b/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineId.java @@ -0,0 +1,74 @@ +/*- + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +package com.oracle.nosql.spring.data.test.composite; + +import com.oracle.nosql.spring.data.core.mapping.NosqlKey; +import org.springframework.data.annotation.Transient; + +import java.io.Serializable; +import java.util.Objects; + +public class MachineId implements Serializable { + private static final long serialVersionUID = 1L; + @NosqlKey(shardKey = true, order = 0) + private String version; + @NosqlKey(shardKey = false, order = 1) + private String name; + @Transient + private final String temp = "temp"; + + public MachineId() { + } + + public MachineId(String version, String name) { + this.version = version; + this.name = name; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MachineId)) { + return false; + } + MachineId machineId = (MachineId) o; + return Objects.equals(version, machineId.version) && + Objects.equals(name, machineId.name); + } + + @Override + public int hashCode() { + return Objects.hash(version, name); + } + + @Override + public String toString() { + return "MachineId{" + + "version='" + version + '\'' + + ", name='" + name + '\'' + + '}'; + } +} diff --git a/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineProjection.java b/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineProjection.java new file mode 100644 index 0000000..f92a63e --- /dev/null +++ b/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineProjection.java @@ -0,0 +1,13 @@ +/*- + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +package com.oracle.nosql.spring.data.test.composite; + +public interface MachineProjection { + MachineId getMachineId(); + String getLocation(); +} diff --git a/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineProjectionDTO.java b/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineProjectionDTO.java new file mode 100644 index 0000000..d4feb09 --- /dev/null +++ b/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineProjectionDTO.java @@ -0,0 +1,67 @@ +/*- + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +package com.oracle.nosql.spring.data.test.composite; + +import com.oracle.nosql.spring.data.core.mapping.NosqlId; + +import java.util.Objects; + +public class MachineProjectionDTO { + @NosqlId + private MachineId machineId; + private String location; + + public MachineProjectionDTO(MachineId machineId, String location) { + this.machineId = machineId; + this.location = location; + } + + public MachineProjectionDTO() { + } + + public MachineId getMachineId() { + return machineId; + } + + public void setMachineId(MachineId machineId) { + this.machineId = machineId; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MachineProjectionDTO that = (MachineProjectionDTO) o; + return Objects.equals(machineId, that.machineId) && Objects.equals(location, that.location); + } + + @Override + public int hashCode() { + return Objects.hash(machineId, location); + } + + @Override + public String toString() { + return "MachineProjectionDTO{" + + "machineId=" + machineId + + ", location='" + location + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineProjectionDTOOnlyId.java b/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineProjectionDTOOnlyId.java new file mode 100644 index 0000000..5843823 --- /dev/null +++ b/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineProjectionDTOOnlyId.java @@ -0,0 +1,49 @@ +/*- + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +package com.oracle.nosql.spring.data.test.composite; + +import com.oracle.nosql.spring.data.core.mapping.NosqlId; + +import java.util.Objects; + +public class MachineProjectionDTOOnlyId { + @NosqlId + private MachineId machineId; + + public MachineProjectionDTOOnlyId() { + } + + public MachineProjectionDTOOnlyId(MachineId machineId) { + this.machineId = machineId; + } + + public MachineId getMachineId() { + return machineId; + } + + public void setMachineId(MachineId machineId) { + this.machineId = machineId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MachineProjectionDTOOnlyId that = (MachineProjectionDTOOnlyId) o; + return Objects.equals(machineId, that.machineId); + } + + @Override + public int hashCode() { + return Objects.hash(machineId); + } +} diff --git a/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineProjectionOnlyId.java b/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineProjectionOnlyId.java new file mode 100644 index 0000000..70cc58e --- /dev/null +++ b/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineProjectionOnlyId.java @@ -0,0 +1,12 @@ +/*- + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +package com.oracle.nosql.spring.data.test.composite; + +public interface MachineProjectionOnlyId { + MachineId getMachineId(); +} diff --git a/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineRepository.java b/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineRepository.java new file mode 100644 index 0000000..312cc18 --- /dev/null +++ b/src/test/java/com/oracle/nosql/spring/data/test/composite/MachineRepository.java @@ -0,0 +1,55 @@ +/*- + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +package com.oracle.nosql.spring.data.test.composite; + +import com.oracle.nosql.spring.data.repository.NosqlRepository; +import com.oracle.nosql.spring.data.repository.Query; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +public interface MachineRepository extends NosqlRepository { + //basic + List findByMachineIdVersion(String version); + List findByMachineIdName(String name); + List findByLocation(String location); + + //and + List findByMachineIdNameAndMachineIdVersion(String name, + String version); + List findByMachineIdNameAndLocation(String name, String location); + //or + List findByMachineIdNameOrMachineIdVersion(String name, + String version); + + //sorting and paging + List findByMachineIdVersionOrderByMachineIdNameAsc(String version); + Page findByMachineIdName(String name, Pageable pageable); + List findAllByOrderByLocation(); + + //native + @Query("SELECT * FROM Machine m WHERE m.VERSION='version1'") + List findByVersionNative(); + + //Ignore case + List findByMachineIdNameIgnoreCase(String name); + List findByMachineIdNameRegexIgnoreCase(String regex); + + //projection + List findAllByLocation(String location); + List findAllByMachineIdName(String name); + + @Query("SELECT m.version,m.name FROM Machine m WHERE m" + + ".kv_json_.location='newyork'") + List findAllByLocationNativeProjection(); + + @Query("SELECT m.version,m.name FROM Machine m WHERE m" + + ".kv_json_.location='newyork'") + List findAllByLocationNativeDTOProjection(); +} diff --git a/src/test/java/com/oracle/nosql/spring/data/test/composite/ReactiveMachineApp.java b/src/test/java/com/oracle/nosql/spring/data/test/composite/ReactiveMachineApp.java new file mode 100644 index 0000000..bc1b3aa --- /dev/null +++ b/src/test/java/com/oracle/nosql/spring/data/test/composite/ReactiveMachineApp.java @@ -0,0 +1,198 @@ +/*- + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ +package com.oracle.nosql.spring.data.test.composite; + +import com.oracle.nosql.spring.data.core.NosqlTemplate; +import com.oracle.nosql.spring.data.test.app.AppConfig; +import com.oracle.nosql.spring.data.test.reactive.ReactiveAppConfig; +import junit.framework.TestCase; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = ReactiveAppConfig.class) + +public class ReactiveMachineApp { + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") + @Autowired + private ReactiveMachineRepository repo; + private Map machineCache; + + private static NosqlTemplate template; + + @BeforeClass + public static void staticSetup() throws ClassNotFoundException { + template = NosqlTemplate.create(AppConfig.nosqlDBConfig); + } + + @Before + public void setup() { + template.dropTableIfExists(Machine.class.getSimpleName()); + machineCache = new HashMap<>(); + List routeAddress = new ArrayList<>(); + routeAddress.add(new IpAddress("127.0.0.1")); + routeAddress.add(new IpAddress("host1")); + routeAddress.add(new IpAddress("host2")); + + //create machines + for (int i = 1; i <= 4; i++) { + for (int j = 1; j <= 4; j++) { + MachineId machineId = new MachineId(); + machineId.setName("name" + i); + machineId.setVersion("version" + j); + Machine machine = new Machine(machineId, (i % 2 == 0) ? + "london" : "newyork", routeAddress.get(0), + routeAddress); + Mono mono = repo.save(machine); + StepVerifier.create(mono).expectNext(machine).verifyComplete(); + machineCache.put(machineId, machine); + } + } + StepVerifier.create(repo.count()).expectNext(Long.valueOf(16)).verifyComplete(); + } + + @After + public void teardown() { + template.dropTableIfExists(Machine.class.getSimpleName()); + } + + @Test + public void testCRUD() { + //get machines + machineCache.forEach(((machineId, machine) -> { + StepVerifier.create(repo.findById(machineId)).expectNext(machine).verifyComplete(); + })); + + //update a machine + Machine updateMachine = machineCache.get(new MachineId("version1", + "name1")); + updateMachine.setLocation("mumbai"); + repo.save(updateMachine); + Mono mono = repo.save(updateMachine); + StepVerifier.create(mono).expectNext(updateMachine); + + //find by machineId + MachineId machineId = new MachineId("version1", "name1"); + Machine machine = repo.findById(machineId).block(); + assertNotNull(machine); + assertEquals(machineId, machine.getMachineId()); + + //delete a row + repo.deleteById(machineId).subscribe(); + StepVerifier.create(repo.existsById(machineId)). + expectNext(Boolean.FALSE).verifyComplete(); + + StepVerifier.create(repo.count()).expectNext(Long.valueOf(15)).verifyComplete(); + } + + @Test + public void testCompositeKeyGet() { + //find all machines with machineId.version=1 + List machines = + repo.findByMachineIdVersion("version1").collectList().block(); + TestCase.assertEquals(4, machines.size()); + machines.forEach(m -> TestCase.assertEquals(machineCache.get(m.getMachineId()), m)); + + //find all machines with machineID.name=name3 + machines = repo.findByMachineIdName("name3").collectList().block(); + TestCase.assertEquals(4, machines.size()); + machines.forEach(m -> TestCase.assertEquals(machineCache.get(m.getMachineId()), m)); + + //find all rows located in london + machines = repo.findByLocation("london").collectList().block(); + TestCase.assertEquals(8, machines.size()); + machines.forEach(m -> TestCase.assertEquals(machineCache.get(m.getMachineId()), m)); + } + + @Test + public void testCompositeKeyLogical() { + //find all machines name=name1 and version=1 + List machines = repo.findByMachineIdNameAndMachineIdVersion( + "name1", + "version1").collectList().block(); + TestCase.assertEquals(1, machines.size()); + machines.forEach(m -> TestCase.assertEquals(machineCache.get(m.getMachineId()) + , m)); + + //find all machines name=name1 or version=1 + machines = repo.findByMachineIdNameOrMachineIdVersion( + "name1", + "version1").collectList().block(); + TestCase.assertEquals(7, machines.size()); + machines.forEach(m -> TestCase.assertEquals(machineCache.get(m.getMachineId()) + , m)); + + //find all machines name=name1 and location=london + machines = repo.findByMachineIdNameAndLocation( + "name1", + "newyork").collectList().block(); + TestCase.assertEquals(4, machines.size()); + machines.forEach(m -> TestCase.assertEquals(machineCache.get(m.getMachineId()) + , m)); + } + + @Test + public void testCompositeSortingAndPaging() { + //find all machines with machineId.version=1 with sort by name + List machines = repo. + findByMachineIdVersionOrderByMachineIdNameAsc("version1"). + collectList().block(); + TestCase.assertEquals(4, machines.size()); + machines.forEach(m -> TestCase.assertEquals(machineCache.get(m.getMachineId()), m)); + //check sort by name is correct + String prev = ""; + for (Machine m : machines) { + String cur = m.getMachineId().getName(); + assertTrue(cur.compareTo(prev) >= 0); + prev = cur; + } + machines.forEach(m -> TestCase.assertEquals(machineCache.get(m.getMachineId()), m)); + + + machines = repo.findAll(Sort.by("machineId.name", "machineId" + + ".version")) + .collectList().block(); + List expectedNames = Arrays.asList( + "name1", "name1", "name1", "name1", + "name2", "name2", "name2", "name2", + "name3", "name3", "name3", "name3", + "name4", "name4", "name4", "name4"); + + List actualNames = new ArrayList<>(); + machines.forEach(m -> actualNames.add(m.getMachineId().getName())); + TestCase.assertEquals(expectedNames, actualNames); + } + + @Test + public void testIgnoreCase() { + //ignore case + List machines = repo. + findByMachineIdNameIgnoreCase("NaMe1").collectList().block(); + TestCase.assertEquals(4, machines.size()); + machines.forEach(m -> TestCase.assertEquals(machineCache.get(m.getMachineId()) + , m)); + } +} diff --git a/src/test/java/com/oracle/nosql/spring/data/test/composite/ReactiveMachineRepository.java b/src/test/java/com/oracle/nosql/spring/data/test/composite/ReactiveMachineRepository.java new file mode 100644 index 0000000..dcc568d --- /dev/null +++ b/src/test/java/com/oracle/nosql/spring/data/test/composite/ReactiveMachineRepository.java @@ -0,0 +1,36 @@ +/*- + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +package com.oracle.nosql.spring.data.test.composite; + +import com.oracle.nosql.spring.data.repository.Query; +import com.oracle.nosql.spring.data.repository.ReactiveNosqlRepository; +import reactor.core.publisher.Flux; + +public interface ReactiveMachineRepository extends ReactiveNosqlRepository { + Flux findByMachineIdVersion(String version); + + Flux findByMachineIdName(String name); + + Flux findByLocation(String location); + + //and + Flux findByMachineIdNameAndMachineIdVersion(String name, + String version); + + Flux findByMachineIdNameAndLocation(String name, String location); + + //or + Flux findByMachineIdNameOrMachineIdVersion(String name, + String version); + + //sorting and paging + Flux findByMachineIdVersionOrderByMachineIdNameAsc(String version); + + //Ignore case + Flux findByMachineIdNameIgnoreCase(String name); +} diff --git a/src/test/java/com/oracle/nosql/spring/data/test/composite/TestTableCreation.java b/src/test/java/com/oracle/nosql/spring/data/test/composite/TestTableCreation.java new file mode 100644 index 0000000..8828618 --- /dev/null +++ b/src/test/java/com/oracle/nosql/spring/data/test/composite/TestTableCreation.java @@ -0,0 +1,487 @@ +/*- + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +package com.oracle.nosql.spring.data.test.composite; + +import com.oracle.nosql.spring.data.core.NosqlTemplate; +import com.oracle.nosql.spring.data.core.mapping.NosqlId; +import com.oracle.nosql.spring.data.core.mapping.NosqlKey; +import com.oracle.nosql.spring.data.core.mapping.NosqlTable; +import com.oracle.nosql.spring.data.repository.support.NosqlEntityInformation; +import com.oracle.nosql.spring.data.test.app.AppConfig; +import oracle.nosql.driver.ops.GetTableRequest; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.fail; + +/** + * Tests for composite key table creation DDL + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = AppConfig.class) +public class TestTableCreation { + private static final String CLOUD_SIM_NAMESPACE = "in.valid.iac.name" + + ".space:"; + private static NosqlTemplate template; + + @BeforeClass + public static void staticSetup() throws ClassNotFoundException { + template = NosqlTemplate.create(AppConfig.nosqlDBConfig); + } + + @Test + public void testCompositeEntityWithNoKeys() { + Class domainClass = CompositeEntityWithNoKeys.class; + NosqlEntityInformation entityInformation = + template.getNosqlEntityInformation(domainClass); + + template.dropTableIfExists(domainClass.getSimpleName()); + template.createTableIfNotExists(entityInformation); + + String tableDDL = template.getNosqlClient(). + getTable(new GetTableRequest(). + setTableName(domainClass.getSimpleName())).getDdl(); + if (tableDDL != null) { + tableDDL = tableDDL.replaceAll(CLOUD_SIM_NAMESPACE, ""); + assertEquals(CompositeEntityWithNoKeys.DDL, tableDDL); + } + template.dropTableIfExists(domainClass.getSimpleName()); + } + + @Test + public void testCompositeEntityWithAllKeys() { + Class domainClass = CompositeEntityWithAllKeys.class; + NosqlEntityInformation entityInformation = + template.getNosqlEntityInformation(domainClass); + + template.dropTableIfExists(domainClass.getSimpleName()); + template.createTableIfNotExists(entityInformation); + + String tableDDL = template.getNosqlClient(). + getTable(new GetTableRequest(). + setTableName(domainClass.getSimpleName())).getDdl(); + if (tableDDL != null) { + tableDDL = tableDDL.replaceAll(CLOUD_SIM_NAMESPACE, ""); + assertEquals(CompositeEntityWithAllKeys.DDL, tableDDL); + } + template.dropTableIfExists(domainClass.getSimpleName()); + } + + @Test + public void testCompositeEntityWithShardKey() { + Class domainClass = CompositeEntityWithShardKey.class; + NosqlEntityInformation entityInformation = + template.getNosqlEntityInformation(domainClass); + + template.dropTableIfExists(domainClass.getSimpleName()); + template.createTableIfNotExists(entityInformation); + + String tableDDL = template.getNosqlClient(). + getTable(new GetTableRequest(). + setTableName(domainClass.getSimpleName())).getDdl(); + if (tableDDL != null) { + tableDDL = tableDDL.replaceAll(CLOUD_SIM_NAMESPACE, ""); + assertEquals(CompositeEntityWithShardKey.DDL, tableDDL); + } + template.dropTableIfExists(domainClass.getSimpleName()); + } + + @Test + public void testCompositeEntityWithNoShardKey() { + Class domainClass = CompositeEntityWithNoShardKey.class; + try { + NosqlEntityInformation entityInformation = + template.getNosqlEntityInformation(domainClass); + template.createTableIfNotExists(entityInformation); + fail("Expecting IllegalArgumentException but didn't get"); + } catch (IllegalArgumentException ignored) { + + } + } + + @Test + public void testCompositeEntityWithOrder() { + Class domainClass = CompositeEntityWithOrder.class; + NosqlEntityInformation entityInformation = + template.getNosqlEntityInformation(domainClass); + + template.dropTableIfExists(domainClass.getSimpleName()); + template.createTableIfNotExists(entityInformation); + + String tableDDL = template.getNosqlClient(). + getTable(new GetTableRequest(). + setTableName(domainClass.getSimpleName())).getDdl(); + if (tableDDL != null) { + tableDDL = tableDDL.replaceAll(CLOUD_SIM_NAMESPACE, ""); + assertEquals(CompositeEntityWithOrder.DDL, tableDDL); + } + template.dropTableIfExists(domainClass.getSimpleName()); + } + + @Test + public void testCompositeEntityWithMultipleKeys() { + Class domainClass = CompositeEntityWithMultipleKeys.class; + try { + NosqlEntityInformation entityInformation = + template.getNosqlEntityInformation(domainClass); + fail("Expecting IllegalArgumentException but didn't get"); + } catch (IllegalArgumentException ignored) { + + } + } + + @Test + public void testCompositeEntityWithRepeatingOrder() { + Class domainClass = CompositeEntityWithRepeatingOrder.class; + try { + NosqlEntityInformation entityInformation = + template.getNosqlEntityInformation(domainClass); + template.createTableIfNotExists(entityInformation); + fail("Expecting IllegalArgumentException but didn't get"); + } catch (IllegalArgumentException ignored) { + + } + } + + @Test + public void testCompositeEntityWithMissingOrder() { + Class domainClass = CompositeEntityWithMissingOrder.class; + try { + NosqlEntityInformation entityInformation = + template.getNosqlEntityInformation(domainClass); + template.createTableIfNotExists(entityInformation); + fail("Expecting IllegalArgumentException but didn't get"); + } catch (IllegalArgumentException ignored) { + + } + } + + @Test + public void testCompositeEntityWithMissingNonShardOrder() { + Class domainClass = CompositeEntityWithMissingNonShardOrder.class; + try { + NosqlEntityInformation entityInformation = + template.getNosqlEntityInformation(domainClass); + template.createTableIfNotExists(entityInformation); + fail("Expecting IllegalArgumentException but didn't get"); + } catch (IllegalArgumentException ignored) { + + } + } + + @Test + public void testCompositeEntityRecommended() { + Class domainClass = CompositeEntityRecommended.class; + NosqlEntityInformation entityInformation = + template.getNosqlEntityInformation(domainClass); + + template.dropTableIfExists(domainClass.getSimpleName()); + template.createTableIfNotExists(entityInformation); + + String tableDDL = template.getNosqlClient(). + getTable(new GetTableRequest(). + setTableName(domainClass.getSimpleName())).getDdl(); + if (tableDDL != null) { + tableDDL = tableDDL.replaceAll(CLOUD_SIM_NAMESPACE, ""); + assertEquals(CompositeEntityRecommended.DDL, tableDDL); + } + template.dropTableIfExists(domainClass.getSimpleName()); + } + + @Test + public void testCompositeEntityCaseInsensitive() { + Class domainClass = CompositeEntityCaseInsensitive.class; + NosqlEntityInformation entityInformation = + template.getNosqlEntityInformation(domainClass); + + template.dropTableIfExists(domainClass.getSimpleName()); + template.createTableIfNotExists(entityInformation); + + String tableDDL = template.getNosqlClient(). + getTable(new GetTableRequest(). + setTableName(domainClass.getSimpleName())).getDdl(); + if (tableDDL != null) { + tableDDL = tableDDL.replaceAll(CLOUD_SIM_NAMESPACE, ""); + assertEquals(CompositeEntityCaseInsensitive.DDL, tableDDL); + } + template.dropTableIfExists(domainClass.getSimpleName()); + } + + @Test + public void testCompositeKeyCollision() { + //shard and non shard key collision + Class domainClass = CompositeEntityFieldCollision.class; + try { + template.getNosqlEntityInformation(domainClass); + fail("Expecting IllegalArgumentException but didn't get"); + } catch (IllegalArgumentException ignored) { + + } + } + + @Test + public void testCompositeEntityKvJsonField() { + //shard and non shard key collision + Class domainClass = CompositeEntityKvJsonField.class; + try { + template.getNosqlEntityInformation(domainClass); + fail("Expecting IllegalArgumentException but didn't get"); + } catch (IllegalArgumentException ignored) { + } + } + + @NosqlTable + public static class CompositeEntityWithNoKeys { + @NosqlId + private CompositeKeyNoKeys id; + String value; + private static final String DDL = String.format( + "CREATE TABLE IF NOT EXISTS %s (id1 STRING, id2 STRING, " + + "kv_json_ JSON, PRIMARY KEY(SHARD(id1, id2)))", + CompositeEntityWithNoKeys.class.getSimpleName()); + } + + public static class CompositeKeyNoKeys { + String id2; + String id1; + } + + @NosqlTable + public static class CompositeEntityWithAllKeys { + @NosqlId + private CompositeKeyAllKeys id; + String value; + private static final String DDL = String.format( + "CREATE TABLE IF NOT EXISTS %s (id1 STRING, id2 STRING, " + + "kv_json_ JSON, PRIMARY KEY(SHARD(id1, id2)))", + CompositeEntityWithAllKeys.class.getSimpleName()); + } + + public static class CompositeKeyAllKeys { + @NosqlKey + String id1; + @NosqlKey + String id2; + } + + @NosqlTable + public static class CompositeEntityWithShardKey { + @NosqlId + private CompositeKeyShardKeys id; + String value; + private static final String DDL = String.format( + "CREATE TABLE IF NOT EXISTS %s (id1 STRING, id2 STRING, " + + "kv_json_ JSON, PRIMARY KEY(SHARD(id1, id2)))", + CompositeEntityWithShardKey.class.getSimpleName()); + } + + public static class CompositeKeyShardKeys { + @NosqlKey(shardKey = true) + String id1; + @NosqlKey + String id2; + } + + @NosqlTable + public static class CompositeEntityWithNoShardKey { + @NosqlId + private CompositeKeyNoShardKeys id; + String value; + private static final String DDL = String.format( + "CREATE TABLE IF NOT EXISTS %s (id1 STRING, id2 STRING, " + + "kv_json_ JSON, PRIMARY KEY(id1, id2))", + CompositeEntityWithNoShardKey.class.getSimpleName()); + } + + public static class CompositeKeyNoShardKeys { + @NosqlKey(shardKey = false) + String id1; + @NosqlKey(shardKey = false) + String id2; + } + + @NosqlTable + public static class CompositeEntityWithOrder { + @NosqlId + private CompositeKeyWithOrder id; + String value; + private static final String DDL = String.format( + "CREATE TABLE IF NOT EXISTS %s (id2 STRING, id1 STRING, " + + "kv_json_ JSON, PRIMARY KEY(SHARD(id2, id1)))", + CompositeEntityWithOrder.class.getSimpleName()); + } + + public static class CompositeKeyWithOrder { + @NosqlKey(shardKey = true, order = 0) + String id2; + @NosqlKey(shardKey = true, order = 1) + String id1; + } + + @NosqlTable + public static class CompositeEntityWithMultipleKeys { + @NosqlId + private CompositeKeyMulti id; + String value; + + private static final String DDL = String.format( + "CREATE TABLE IF NOT EXISTS %s (id2 STRING, id1 STRING, id3 " + + "STRING, id5 STRING, id4 STRING, " + + "kv_json_ JSON, PRIMARY KEY(SHARD(id2, id1, id3), " + + "id5, id4))", + CompositeEntityWithMultipleKeys.class.getSimpleName()); + } + + public static class CompositeKeyMulti { + @NosqlKey(shardKey = true, order = 0) + String id2; + @NosqlKey(shardKey = true, order = 1) + String id1; + @NosqlKey(shardKey = true, order = 2) + String id3; + @NosqlKey(shardKey = false, order = 0) + String id5; + @NosqlKey(shardKey = false, order = 1) + String id4; + } + + @NosqlTable + public static class CompositeEntityWithRepeatingOrder { + @NosqlId + private CompositeKeyWithRepeatingOrder id; + String value; + } + + public static class CompositeKeyWithRepeatingOrder { + @NosqlKey(shardKey = true, order = 0) + String id2; + @NosqlKey(shardKey = true, order = 1) + String id1; + @NosqlKey(shardKey = true, order = 1) + String id3; + @NosqlKey(shardKey = false, order = 1) + String id5; + @NosqlKey(shardKey = false, order = 1) + String id4; + } + + @NosqlTable + public static class CompositeEntityWithMissingOrder { + @NosqlId + private CompositeKeyWithMissingOrder id; + String value; + } + + public static class CompositeKeyWithMissingOrder { + @NosqlKey(shardKey = true, order = 1) + String id1; + + @NosqlKey(shardKey = true) + String id2; + } + + @NosqlTable + public static class CompositeEntityWithMissingNonShardOrder { + @NosqlId + private CompositeKeyWithMissingNonShardOrder id; + String value; + } + + public static class CompositeKeyWithMissingNonShardOrder { + @NosqlKey(shardKey = true) + String id1; + + @NosqlKey(shardKey = false, order = 1) + String id2; + + @NosqlKey(shardKey = false) + String id3; + } + + @NosqlTable + public static class CompositeEntityRecommended { + @NosqlId + private CompositeKeyRecommended id; + String value; + + private static final String DDL = String.format( + "CREATE TABLE IF NOT EXISTS %s (id2 STRING, id1 STRING, id4 " + + "STRING, id3 STRING, " + + "kv_json_ JSON, PRIMARY KEY(SHARD(id2, id1), " + + "id4, id3))", + CompositeEntityRecommended.class.getSimpleName()); + } + public static class CompositeKeyRecommended { + @NosqlKey(shardKey = true, order = 1) + String id2; + + @NosqlKey(shardKey = true, order = 2) + String id1; + + @NosqlKey(shardKey = false, order = 3) + String id4; + + @NosqlKey(shardKey = false, order = 4) + String id3; + + } + + @NosqlTable + public static class CompositeEntityCaseInsensitive { + @NosqlId + private CompositeKeyCaseInsensitive id; + String value; + + private static final String DDL = String.format( + "CREATE TABLE IF NOT EXISTS %s (abcd STRING, id1 STRING, ID2 " + + "STRING, kv_json_ JSON, PRIMARY KEY(SHARD(abcd, id1, " + + "ID2)))", + CompositeEntityCaseInsensitive.class.getSimpleName() + ); + } + + public static class CompositeKeyCaseInsensitive { + @NosqlKey + String ID2; + + @NosqlKey + String id1; + + @NosqlKey + String abcd; + } + + @NosqlTable + public static class CompositeEntityFieldCollision { + @NosqlId + private CompositeKeyFieldCollision id; + } + + public static class CompositeKeyFieldCollision { + @NosqlKey + String id1; + + @NosqlKey + String ID1; + } + + @NosqlTable + public static class CompositeEntityKvJsonField { + @NosqlId + private CompositeKeyKvJsonField id; + } + + public static class CompositeKeyKvJsonField { + @NosqlKey(shardKey = true, order = 1) + private String kv_json_; + } +} diff --git a/src/test/java/com/oracle/nosql/spring/data/test/reactive/ReactiveAppConfig.java b/src/test/java/com/oracle/nosql/spring/data/test/reactive/ReactiveAppConfig.java index e2ed77b..44a7b81 100644 --- a/src/test/java/com/oracle/nosql/spring/data/test/reactive/ReactiveAppConfig.java +++ b/src/test/java/com/oracle/nosql/spring/data/test/reactive/ReactiveAppConfig.java @@ -15,7 +15,10 @@ import org.springframework.context.annotation.Configuration; @Configuration -@EnableReactiveNosqlRepositories +@EnableReactiveNosqlRepositories(basePackages = { + "com.oracle.nosql.spring.data.test.reactive", + "com.oracle.nosql.spring.data.test.composite" +}) public class ReactiveAppConfig extends AbstractNosqlConfiguration { @Bean public NosqlDbConfig nosqlDbConfig() {