Skip to content

Commit

Permalink
Feature/composite key (#31)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
Akshay-Sundarraj and cezarfx authored May 5, 2023
1 parent 80a92fd commit 0c308ed
Show file tree
Hide file tree
Showing 28 changed files with 2,311 additions and 112 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/oracle/nosql/spring/data/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -485,6 +486,24 @@ private <T> 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;
}

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

return IterableUtil.getIterableFromStream(resStream);
}

public NoSQLHandle getNosqlClient() {
return nosqlClient;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.Assert;


public abstract class NosqlTemplateBase
implements ApplicationContextAware {

Expand Down Expand Up @@ -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<String, FieldValue.Type> shardKeys =
entityInformation.getShardKeys();

String autogen = "";
if (entityInformation.isAutoGeneratedId() ) {
if (entityInformation.getIdNosqlType() == FieldValue.Type.STRING) {
autogen = TEMPLATE_GENERATED_UUID;
} else {
autogen = TEMPLATE_GENERATED_ALWAYS;
Map<String, FieldValue.Type> 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));
Expand Down Expand Up @@ -266,7 +289,6 @@ protected void doUpdate(NosqlEntityInformation<?, ?> entityInformation,
idColumnName);

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

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

final Map<String, Object> params = new LinkedHashMap<>();
String sql = query.generateSql(entityInformation.getTableName(), params,
idPropertyName);
idPropertyName, mappingNosqlConverter.
getMappingContext().getPersistentEntity(entityClass));

PreparedStatement pStmt = getPreparedStatement(entityInformation, sql);

Expand Down Expand Up @@ -387,4 +410,12 @@ private PreparedStatement getPreparedStatement(
private Iterable<MapValue> 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 "";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -211,10 +212,17 @@ public <T> 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) {
Expand Down Expand Up @@ -568,26 +576,38 @@ private <E> 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<E> clsEntity =
updateEntity(entity, getInstanceClass(jsonValue));
entityObj = getNewInstance(clsEntity, nosqlValue.asMap(),
jsonValue);
get(NosqlTemplateBase.JSON_COLUMN).asMap();
}
NosqlPersistentEntity<E> 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);
Expand Down Expand Up @@ -811,7 +831,7 @@ private <E> List<Object> convertArrayValueToCollection(FieldValue nosqlValue,

private <R> R getNewInstance(NosqlPersistentEntity<R> entity,
MapValue rootFieldValue,
@NonNull MapValue jsonValue) {
@Nullable MapValue jsonValue) {

EntityInstantiator instantiator =
instantiators.getInstantiatorFor(entity);
Expand All @@ -828,14 +848,16 @@ public <T> 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
Expand Down Expand Up @@ -1115,10 +1137,15 @@ public <ID> 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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading

0 comments on commit 0c308ed

Please sign in to comment.