Skip to content

Commit 0c308ed

Browse files
Feature/composite key (#31)
* Composite key support o. Allow NoSqlEntity to have a class as primary key o. User can specify multiple primary keys in the composite key class o. User can specify whether a key is shard key and it's order o. Added new entity validation method which extends validation supported by Spring data BasicPersistentEntity p. Added new annotations @NoSqlKey and @NoSqlKeyClass Signed-off-by: Akshay Sundarraj <akshay.sundarraj@oracle.com> * o. Add derive query tests for composite primary key. o. Add javadoc for newly added classes o. Remove unused code Signed-off-by: Akshay Sundarraj <akshay.sundarraj@oracle.com> * Fix javadoc warning * Update composite key test * o. Fix an issue in table DDL generation for composite key * o. Added tests for table creation for composite primary key o. Added a public method getNosqlClient() in NosqlTemplate All unit tests are passed for both on-prem and cloud * composite key Ordering. o. Added support for ordering on composite key in Repository o. Added a new testcase to verify ordering on composite key member * Unit test improvements o. Added new set of tests for composite key without @NosqlKey o. Updated composite key tests to check the result of repo operations o. Added Pageable and Sort tests o. Fixed a query issue when composite key is not @NosqlKey * o. Fix an issue when findAll(sort) has composite key member o. Refactor composite key unit test to group by tests o. Add new unit tests for interface and DTO projection for composite key All unit tests are passed * o. Fix an issue in ReactiveRepo.save() which was not saving an entity with composite keys o Add new tests for reactive composite keys * Adding missed file * o. Fix an issue when projecting only primary keys o. Added tests for projecting only primary keys * Fix some of the review comments * NosqlKey ordering changes: o. Changes order of NosqlKeys in composite key as discussed in meeting o. Added Javadoc for the same o. Updated tests for ordering of fields o. Fixed some of the review comments * o. Fix review comments o. Move composite key validation to NosqlEntityInformation * o. Added a test to check for collision of keys when sorted o. Updated check for field collision * o. Update javadoc for NosqlKey o. Updated CHANGELOG.md o. Added new test for kv_json_ as composite key * Updated javadoc for NosqlKey and NosqlId classes. * Small update shardKey javadoc. * o. Updated CHANGELOG.md o. Update a unit test * o. Modified composite key ordering to be unique across both shard and non shard keys. o. Updated the javadoc for the same o. Updated the tests --------- Signed-off-by: Akshay Sundarraj <akshay.sundarraj@oracle.com> Co-authored-by: Cezar Andrei <cezar.andrei@oracle.com>
1 parent 80a92fd commit 0c308ed

28 files changed

+2311
-112
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file.
33

44
The format is based on [Keep a Changelog](http://keepachangelog.com/).
55

6+
## [1.6.0] [not yet released]
7+
### Added
8+
- Added support for composite primary keys.
9+
610
## [1.5.0]
711
### Added
812
- Added support for java.util.Map and similar types as mapping types.

src/main/java/com/oracle/nosql/spring/data/Constants.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ public class Constants {
2929
public static final int NOTSET_TABLE_TIMEOUT_MS = 0;
3030
public static final int NOTSET_TABLE_TTL = 0;
3131

32+
public static final boolean NOTSET_SHARD_KEY = true;
33+
public static final int NOTSET_PRIMARY_KEY_ORDER = -1;
34+
3235
public static final String USER_AGENT = "NoSQL-SpringSDK";
3336

3437
private Constants() {}

src/main/java/com/oracle/nosql/spring/data/core/NosqlTemplate.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import java.util.stream.Stream;
1717
import java.util.stream.StreamSupport;
1818

19+
import com.oracle.nosql.spring.data.core.mapping.NosqlKey;
20+
import com.oracle.nosql.spring.data.core.mapping.NosqlPersistentProperty;
1921
import oracle.nosql.driver.NoSQLException;
2022
import oracle.nosql.driver.NoSQLHandle;
2123
import oracle.nosql.driver.ops.DeleteRequest;
@@ -39,7 +41,6 @@
3941
import org.apache.commons.lang3.reflect.FieldUtils;
4042
import org.springframework.beans.BeansException;
4143
import org.springframework.context.ApplicationContext;
42-
import org.springframework.context.ApplicationContextAware;
4344
import org.springframework.data.domain.Page;
4445
import org.springframework.data.domain.PageImpl;
4546
import org.springframework.data.domain.Pageable;
@@ -485,6 +486,24 @@ private <T> String convertProperty(
485486
return property;
486487
}
487488

489+
//field can be composite key
490+
NosqlPersistentProperty pp = mappingNosqlConverter.getMappingContext().
491+
getPersistentPropertyPath(property,
492+
entityInformation.getJavaType()).getLeafProperty();
493+
494+
NosqlPersistentProperty parentPp =
495+
mappingNosqlConverter.getMappingContext().
496+
getPersistentPropertyPath(property,
497+
entityInformation.getJavaType()).getBaseProperty();
498+
if (pp != null) {
499+
if (pp.isAnnotationPresent(NosqlKey.class)) {
500+
return pp.getName();
501+
}
502+
if (parentPp != null && parentPp.isIdProperty()) {
503+
return pp.getName();
504+
}
505+
}
506+
488507
return JSON_COLUMN + "." + property;
489508
}
490509

@@ -582,4 +601,8 @@ public <S, T> Iterable<T> find(
582601

583602
return IterableUtil.getIterableFromStream(resStream);
584603
}
604+
605+
public NoSQLHandle getNosqlClient() {
606+
return nosqlClient;
607+
}
585608
}

src/main/java/com/oracle/nosql/spring/data/core/NosqlTemplateBase.java

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
import org.springframework.context.ApplicationContextAware;
4242
import org.springframework.util.Assert;
4343

44-
4544
public abstract class NosqlTemplateBase
4645
implements ApplicationContextAware {
4746

@@ -116,35 +115,59 @@ protected TableResult doTableRequest(NosqlEntityInformation<?, ?> entityInformat
116115
protected boolean doCreateTableIfNotExists(
117116
NosqlEntityInformation<?, ?> entityInformation) {
118117

119-
String idColName = entityInformation.getIdField().getName();
118+
String tableName = entityInformation.getTableName();
119+
String sql;
120120

121-
String idColType = entityInformation.getIdNosqlType().toString();
122-
if (entityInformation.getIdNosqlType() == FieldValue.Type.TIMESTAMP) {
123-
// For example: CREATE TABLE IF NOT EXISTS SensorIdTimestamp
124-
// (time TIMESTAMP(3) , kv_json_ JSON, PRIMARY KEY( time ))
125-
idColType += "(" + nosqlDbFactory.getTimestampPrecision() + ")";
126-
}
121+
Map<String, FieldValue.Type> shardKeys =
122+
entityInformation.getShardKeys();
127123

128-
String autogen = "";
129-
if (entityInformation.isAutoGeneratedId() ) {
130-
if (entityInformation.getIdNosqlType() == FieldValue.Type.STRING) {
131-
autogen = TEMPLATE_GENERATED_UUID;
132-
} else {
133-
autogen = TEMPLATE_GENERATED_ALWAYS;
124+
Map<String, FieldValue.Type> nonShardKeys =
125+
entityInformation.getNonShardKeys();
126+
127+
StringBuilder tableBuilder = new StringBuilder();
128+
tableBuilder.append("CREATE TABLE IF NOT EXISTS ");
129+
tableBuilder.append(tableName).append("("); //create open (
130+
131+
shardKeys.forEach((key, type) -> {
132+
String keyType = type.name();
133+
if (keyType.equals(FieldValue.Type.TIMESTAMP.toString())) {
134+
keyType += "(" + nosqlDbFactory.getTimestampPrecision() + ")";
135+
}
136+
String autogen = getAutoGenType(entityInformation);
137+
tableBuilder.append(key).append(" ").append(keyType)
138+
.append(" ").append(autogen).append(",");
139+
});
140+
141+
nonShardKeys.forEach((key, type) -> {
142+
String keyType = type.name();
143+
if (keyType.equals(FieldValue.Type.TIMESTAMP.toString())) {
144+
keyType += "(" + nosqlDbFactory.getTimestampPrecision() + ")";
134145
}
146+
String autogen = getAutoGenType(entityInformation);
147+
tableBuilder.append(key).append(" ").append(keyType)
148+
.append(" ").append(autogen).append(",");
149+
});
150+
tableBuilder.append(JSON_COLUMN).append(" ").append("JSON").append(",");
151+
152+
tableBuilder.append("PRIMARY KEY").append("("); //primary key open (
153+
tableBuilder.append("SHARD").append("(");
154+
tableBuilder.append(String.join(",", shardKeys.keySet()));
155+
tableBuilder.append(")");
156+
157+
if (!nonShardKeys.isEmpty()) {
158+
tableBuilder.append(",");
159+
tableBuilder.append(String.join(",", nonShardKeys.keySet()));
135160
}
161+
tableBuilder.append(")"); //primary key close )
162+
tableBuilder.append(")"); //create close )
136163

137-
String ttl = "";
164+
//ttl
138165
if (entityInformation.getTtl() != null &&
139166
entityInformation.getTtl().getValue() != 0) {
140-
ttl = String.format(TEMPLATE_TTL_CREATE,
141-
entityInformation.getTtl().toString());
167+
tableBuilder.append(String.format(TEMPLATE_TTL_CREATE,
168+
entityInformation.getTtl().toString()));
142169
}
143-
144-
String tableName = entityInformation.getTableName();
145-
String sql = String.format(TEMPLATE_CREATE_TABLE,
146-
tableName,
147-
idColName, idColType, autogen, idColName, ttl);
170+
sql = tableBuilder.toString();
148171

149172
TableRequest tableReq = new TableRequest().setStatement(sql)
150173
.setTableLimits(entityInformation.getTableLimits(nosqlDbFactory));
@@ -266,7 +289,6 @@ protected void doUpdate(NosqlEntityInformation<?, ?> entityInformation,
266289
idColumnName);
267290

268291
Map<String, FieldValue> params = new HashMap<>();
269-
//todo implement composite keys
270292
params.put("$id", row.get(idColumnName));
271293
params.put("$json", row.get(JSON_COLUMN));
272294

@@ -328,7 +350,8 @@ protected <T> Iterable<MapValue> doExecuteMapValueQuery(NosqlQuery query,
328350

329351
final Map<String, Object> params = new LinkedHashMap<>();
330352
String sql = query.generateSql(entityInformation.getTableName(), params,
331-
idPropertyName);
353+
idPropertyName, mappingNosqlConverter.
354+
getMappingContext().getPersistentEntity(entityClass));
332355

333356
PreparedStatement pStmt = getPreparedStatement(entityInformation, sql);
334357

@@ -387,4 +410,12 @@ private PreparedStatement getPreparedStatement(
387410
private Iterable<MapValue> doQuery(QueryRequest qReq) {
388411
return new IterableUtil.IterableImpl(nosqlClient, qReq);
389412
}
413+
414+
private String getAutoGenType(NosqlEntityInformation<?, ?> entityInformation) {
415+
if (entityInformation.isAutoGeneratedId()) {
416+
return (entityInformation.getIdNosqlType() == FieldValue.Type.STRING) ?
417+
TEMPLATE_GENERATED_UUID : TEMPLATE_GENERATED_ALWAYS;
418+
}
419+
return "";
420+
}
390421
}

src/main/java/com/oracle/nosql/spring/data/core/convert/MappingNosqlConverter.java

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import com.oracle.nosql.spring.data.core.mapping.BasicNosqlPersistentProperty;
5757
import com.oracle.nosql.spring.data.core.mapping.NosqlPersistentEntity;
5858
import com.oracle.nosql.spring.data.core.mapping.NosqlPersistentProperty;
59+
import com.oracle.nosql.spring.data.repository.support.NosqlEntityInformation;
5960

6061
import org.slf4j.Logger;
6162
import org.slf4j.LoggerFactory;
@@ -211,10 +212,17 @@ public <T> MapValue convertObjToRow(T objectToSave, boolean skipSetId) {
211212
row.put(NosqlTemplateBase.JSON_COLUMN, valueMap);
212213

213214
if (!skipSetId && idProperty != null) {
214-
//todo implement composite key
215-
row.put(idProperty.getName(),
216-
convertObjToFieldValue(accessor.getProperty(idProperty),
217-
idProperty, false));
215+
if (idProperty.isCompositeKey()) {
216+
MapValue ids = convertObjToFieldValue(
217+
accessor.getProperty(idProperty),
218+
idProperty,
219+
false).asMap();
220+
ids.getMap().forEach(row::put);
221+
} else {
222+
row.put(idProperty.getName(),
223+
convertObjToFieldValue(accessor.getProperty(idProperty),
224+
idProperty, false));
225+
}
218226
}
219227

220228
for (NosqlPersistentProperty prop : persistentEntity) {
@@ -568,26 +576,38 @@ private <E> E convertFieldValueToObj(Class<?> type,
568576
FieldValue idFieldValue = null;
569577

570578
if (entity.getIdProperty() != null) {
571-
idFieldValue = nosqlValue.asMap()
572-
.get(entity.getIdProperty().getName());
579+
NosqlPersistentProperty idProperty = entity.getIdProperty();
580+
581+
if (idProperty.isCompositeKey()) {
582+
idFieldValue = new MapValue();
583+
NosqlPersistentEntity<?> idEntity =
584+
mappingContext.getPersistentEntity(idProperty.getType());
585+
for (NosqlPersistentProperty p : idEntity) {
586+
idFieldValue.asMap().put(p.getName(),
587+
nosqlValue.asMap().get(p.getName()));
588+
}
589+
} else {
590+
idFieldValue = nosqlValue.asMap()
591+
.get(entity.getIdProperty().getName());
592+
}
573593
}
574594

575-
MapValue jsonValue;
595+
MapValue jsonValue = null;
576596
if (nosqlValue.asMap().get(NosqlTemplateBase.JSON_COLUMN) !=
577597
null) {
578598
jsonValue = nosqlValue.asMap().
579-
get(NosqlTemplateBase.JSON_COLUMN).asMap();
580-
581-
NosqlPersistentEntity<E> clsEntity =
582-
updateEntity(entity, getInstanceClass(jsonValue));
583-
entityObj = getNewInstance(clsEntity, nosqlValue.asMap(),
584-
jsonValue);
599+
get(NosqlTemplateBase.JSON_COLUMN).asMap();
600+
}
601+
NosqlPersistentEntity<E> clsEntity =
602+
updateEntity(entity, getInstanceClass(jsonValue));
603+
entityObj = getNewInstance(clsEntity, nosqlValue.asMap(),
604+
jsonValue);
585605

586-
if (idFieldValue != null) {
587-
setId(entityObj, idFieldValue);
588-
}
589-
setPojoProperties(clsEntity, entityObj, jsonValue);
606+
if (idFieldValue != null) {
607+
setId(entityObj, idFieldValue);
590608
}
609+
setPojoProperties(clsEntity, entityObj, jsonValue);
610+
591611
} else {
592612
MapValue mapValue = nosqlValue.asMap();
593613
String instClsStr = getInstanceClass(mapValue);
@@ -811,7 +831,7 @@ private <E> List<Object> convertArrayValueToCollection(FieldValue nosqlValue,
811831

812832
private <R> R getNewInstance(NosqlPersistentEntity<R> entity,
813833
MapValue rootFieldValue,
814-
@NonNull MapValue jsonValue) {
834+
@Nullable MapValue jsonValue) {
815835

816836
EntityInstantiator instantiator =
817837
instantiators.getInstantiatorFor(entity);
@@ -828,14 +848,16 @@ public <T> T getParameterValue(
828848
NosqlPersistentProperty prop =
829849
entity.getPersistentProperty(paramName);
830850

831-
FieldValue value;
832-
if (rootFieldValue == null) {
851+
FieldValue value = null;
852+
if (rootFieldValue == null && jsonValue != null) {
833853
value = jsonValue.get(paramName);
834854
} else {
835855
if (prop.isIdProperty()) {
836856
value = rootFieldValue.get(paramName);
837857
} else {
838-
value = jsonValue.get(paramName);
858+
if (jsonValue != null) {
859+
value = jsonValue.get(paramName);
860+
}
839861
if (value == null) {
840862
// if field is not marked id and it's not in
841863
// kv_json_ it may be an unmarked id field
@@ -1115,10 +1137,15 @@ public <ID> MapValue convertIdToPrimaryKey(String idColumnName, ID id) {
11151137
}
11161138

11171139
MapValue row = new MapValue();
1118-
1119-
row.put(idColumnName, convertObjToFieldValue(id, null, false));
1120-
//todo: add support for composite key
1121-
1140+
if (NosqlEntityInformation.isCompositeKeyType(id.getClass())) {
1141+
/*composite key. Here convertObjToFieldValue adds #class that is
1142+
why convertObjToRow is used*/
1143+
MapValue compositeKey = convertObjToRow(id, false);
1144+
compositeKey.get(NosqlTemplateBase.JSON_COLUMN).asMap().
1145+
getMap().forEach(row::put);
1146+
} else {
1147+
row.put(idColumnName, convertObjToFieldValue(id, null, false));
1148+
}
11221149
return row;
11231150
}
11241151

src/main/java/com/oracle/nosql/spring/data/core/mapping/BasicNosqlPersistentProperty.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import oracle.nosql.driver.values.FieldValue;
2121

2222
import com.oracle.nosql.spring.data.Constants;
23+
import com.oracle.nosql.spring.data.repository.support.NosqlEntityInformation;
2324

2425
import org.springframework.data.geo.Point;
2526
import org.springframework.data.geo.Polygon;
@@ -170,4 +171,15 @@ public static TypeCode getCodeForSerialization(Class<?> cls) {
170171
return TypeCode.POJO;
171172
}
172173
}
174+
175+
@Override
176+
public boolean isCompositeKey() {
177+
return isIdProperty() &&
178+
NosqlEntityInformation.isCompositeKeyType(getType());
179+
}
180+
181+
@Override
182+
public boolean isNosqlKey() {
183+
return isAnnotationPresent(NosqlKey.class);
184+
}
173185
}

src/main/java/com/oracle/nosql/spring/data/core/mapping/NosqlId.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,23 @@
1313

1414
import org.springframework.data.annotation.Id;
1515

16+
/**
17+
* Identifies the primary key field of the entity. Primary key can be of a
18+
* basic type or of a type that represents a composite primary key. This
19+
* field corresponds to the {@code PRIMARY KEY} of the corresponding Nosql
20+
* table. Only one field of the entity can be annotated with this annotation.
21+
*
22+
* If using a composite primary key, the fields comprising the composite key
23+
* must have distinct names when viewed in lower case, otherwise an error is
24+
* raised.
25+
*/
1626
@Id
1727
@Retention(RetentionPolicy.RUNTIME)
1828
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
1929
public @interface NosqlId {
30+
/**
31+
* Specifies if values for the field are automatically generated. Valid only
32+
* for int, Integer, long, Long, BigInteger, BigDecimal,
33+
*/
2034
boolean generated() default false;
21-
22-
//todo: will be supported in a future version
23-
//boolean shardKey() default false;
2435
}

0 commit comments

Comments
 (0)