diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index f457ea7d3..e229a7d06 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -12,6 +12,11 @@ and this project adheres to https://semver.org/spec/v2.0.0.html[Semantic Version - Update query at Oracle NoSQL to support parameter with enum type +=== Changes + +- Changes on the document entities persistence operations at the Eclipse JNoSQL DynamoDB Database + + == [1.1.8] - 2025-05-21 === Changed diff --git a/README.adoc b/README.adoc index 0d5d3f15d..d46a67e5f 100644 --- a/README.adoc +++ b/README.adoc @@ -597,27 +597,6 @@ jnosql.keyvalue.database=heroes === Using the Document API -The DynamoDB's Document API implementation follows the *SINGLE TABLE* strategy, it means, the table will store multiple entity types. To satisfy this strategy, the implementation assumes that the target table will have a composed primary key: - -- The `entityType` field as the partitioning key; -- The `id` field as the sort key; - -To customize the partitioning key field name, you can define the following configuration - -[source,properties] ----- -jnosql.dynamodb.entity.pk=entityType ----- - -By default, the implementation doesn't create the table on-the-fly, letting this requirement for the users. If you prefer, the implementation is able to create the table on-the-fly as well. To activate this capability you should define explicitly the following configuration: - -[source,properties] ----- -jnosql.dynamodb.create.tables=true ----- - -The table will be created with the composed primary key mentioned previously. - Here's an example using DynamoDB's Document API with MicroProfile Config. [source,properties] @@ -648,7 +627,7 @@ public class ManagerSupplier implements Supplier { === Repository -The ```DynamoDBRepository``` interface is an extension of the ```Repository``` interface that allows execution of PartiQL via the ```@PartiQL``` annotation. +The `DynamoDBRepository` interface is an extension of the `Repository` interface that allows execution of PartiQL via the `@PartiQL` annotation. WARNING: DynamoDB supports a limited subset of https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ql-reference.html[PartiQL]. @@ -669,10 +648,9 @@ List findByName(@Param("") String name); } ---- - === Template -The ```DynamoDBTemplate``` interface is a specialization of the ```DocumentTemplate``` interface that allows using PartiQL queries. +The `DynamoDBTemplate` interface is a specialization of the `DocumentTemplate` interface that allows using PartiQL queries. WARNING: DynamoDB supports a limited subset of https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ql-reference.html[PartiQL]. @@ -681,9 +659,44 @@ NOTE: This implementation doesn't provide pagination on the queries. [source,java] ---- -List people = template.partiQL("select * from Person where name = ? ", params); +List people = template.partiQL("select * from Person where name = ? ", Person.class, params); ---- +==== Creating the tables on-the-fly + +[IMPORTANT] +==== +It's highly recommended to create the tables in a proper way, paying attention to the partition key and sort key, as well as the indexes. +==== + +The DynamoDB implementation allows you to create tables on-the-fly, which can be useful for development and testing purposes. However, this feature should be used with caution in production environments, as it may lead to unexpected behavior or performance issues if not properly configured. + +To create tables on-the-fly, you need to define the following properties: + +Please note that you can establish properties using the https://microprofile.io/microprofile-config/[MicroProfile Config] specification. + +[cols="DynamoDB"] +|=== +|Configuration property |Description | Default value + +|`jnosql.dynamodb.create.tables` +| If set to true, the implementation will create the tables on-the-fly when the application starts. This is useful for development and testing purposes, but should be used with caution in production environments. +| false + +|`jnosql.dynamodb..pk` +| The partition key field name for the table. This is used to define the primary key of the table. The `
` part should be replaced with the actual table name. +| _id + +|`jnosql.dynamodb.
.read.capacity.units` +| The read capacity units for the table. This defines the number of strongly consistent reads per second that the table can support.The `
` part should be replaced with the actual table name. It's optional. +| none + +|`jnosql.dynamodb.
.write.capacity.units` +| The write capacity units for the table. This defines the number of strongly consistent writes per second that the table can support.The `
` part should be replaced with the actual table name. It's optional. +| none + +|=== + == Elasticsearch image::https://jnosql.github.io/img/logos/elastic.svg[Elasticsearch Project,align="center"width=25%,height=25%] diff --git a/jnosql-arangodb/src/main/java/org/eclipse/jnosql/databases/arangodb/mapping/ArangoDBDocumentRepositoryProxy.java b/jnosql-arangodb/src/main/java/org/eclipse/jnosql/databases/arangodb/mapping/ArangoDBDocumentRepositoryProxy.java index b68ab196d..13a6a089e 100644 --- a/jnosql-arangodb/src/main/java/org/eclipse/jnosql/databases/arangodb/mapping/ArangoDBDocumentRepositoryProxy.java +++ b/jnosql-arangodb/src/main/java/org/eclipse/jnosql/databases/arangodb/mapping/ArangoDBDocumentRepositoryProxy.java @@ -48,6 +48,8 @@ class ArangoDBDocumentRepositoryProxy extends AbstractSemiStructuredReposi private final EntityMetadata entityMetadata; + private final EntitiesMetadata entitiesMetadata; + ArangoDBDocumentRepositoryProxy(ArangoDBTemplate template, Class type, Converters converters, @@ -57,6 +59,7 @@ class ArangoDBDocumentRepositoryProxy extends AbstractSemiStructuredReposi .getActualTypeArguments()[0]); this.type = type; this.converters = converters; + this.entitiesMetadata = entitiesMetadata; this.entityMetadata = entitiesMetadata.get(typeClass); this.repository = SemiStructuredRepositoryProxy.SemiStructuredRepository.of(template, entityMetadata); } @@ -82,6 +85,11 @@ protected EntityMetadata entityMetadata() { return entityMetadata; } + @Override + protected EntitiesMetadata entitiesMetadata() { + return this.entitiesMetadata; + } + @Override protected DocumentTemplate template() { return template; diff --git a/jnosql-cassandra/src/main/java/org/eclipse/jnosql/databases/cassandra/mapping/CassandraRepositoryProxy.java b/jnosql-cassandra/src/main/java/org/eclipse/jnosql/databases/cassandra/mapping/CassandraRepositoryProxy.java index 6bba34896..cdaf487ed 100644 --- a/jnosql-cassandra/src/main/java/org/eclipse/jnosql/databases/cassandra/mapping/CassandraRepositoryProxy.java +++ b/jnosql-cassandra/src/main/java/org/eclipse/jnosql/databases/cassandra/mapping/CassandraRepositoryProxy.java @@ -43,6 +43,8 @@ class CassandraRepositoryProxy extends AbstractSemiStructuredRepositoryPro private final Converters converters; + private final EntitiesMetadata entitiesMetadata; + private final EntityMetadata entityMetadata; private final Class repositoryType; @@ -55,6 +57,7 @@ class CassandraRepositoryProxy extends AbstractSemiStructuredRepositoryPro .getActualTypeArguments()[0]); this.converters = converters; + this.entitiesMetadata = entitiesMetadata; this.entityMetadata = entitiesMetadata.get(typeClass); this.repositoryType = repositoryType; this.repository = SemiStructuredRepositoryProxy.SemiStructuredRepository.of(template, entityMetadata); @@ -80,6 +83,11 @@ protected EntityMetadata entityMetadata() { return entityMetadata; } + @Override + protected EntitiesMetadata entitiesMetadata() { + return this.entitiesMetadata; + } + @Override protected ColumnTemplate template() { return template; diff --git a/jnosql-couchbase/src/main/java/org/eclipse/jnosql/databases/couchbase/mapping/CouchbaseDocumentRepositoryProxy.java b/jnosql-couchbase/src/main/java/org/eclipse/jnosql/databases/couchbase/mapping/CouchbaseDocumentRepositoryProxy.java index c68bdedc3..4d9c747bb 100644 --- a/jnosql-couchbase/src/main/java/org/eclipse/jnosql/databases/couchbase/mapping/CouchbaseDocumentRepositoryProxy.java +++ b/jnosql-couchbase/src/main/java/org/eclipse/jnosql/databases/couchbase/mapping/CouchbaseDocumentRepositoryProxy.java @@ -43,6 +43,8 @@ class CouchbaseDocumentRepositoryProxy extends AbstractSemiStructuredRepos private final Converters converters; + private final EntitiesMetadata entitiesMetadata; + private final EntityMetadata entityMetadata; private final Class repositoryType; @@ -54,6 +56,7 @@ class CouchbaseDocumentRepositoryProxy extends AbstractSemiStructuredRepos this.typeClass = Class.class.cast(ParameterizedType.class.cast(repositoryType.getGenericInterfaces()[0]) .getActualTypeArguments()[0]); this.converters = converters; + this.entitiesMetadata = entitiesMetadata; this.entityMetadata = entitiesMetadata.get(typeClass); this.repositoryType = repositoryType; this.repository = SemiStructuredRepositoryProxy.SemiStructuredRepository.of(template, entityMetadata); @@ -80,6 +83,11 @@ protected EntityMetadata entityMetadata() { return entityMetadata; } + @Override + protected EntitiesMetadata entitiesMetadata() { + return entitiesMetadata; + } + @Override protected DocumentTemplate template() { return template; diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DefaultDynamoDBDatabaseManager.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DefaultDynamoDBDatabaseManager.java index 518e2fb45..672c557bb 100644 --- a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DefaultDynamoDBDatabaseManager.java +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DefaultDynamoDBDatabaseManager.java @@ -57,7 +57,6 @@ import java.util.stream.StreamSupport; import static java.util.Objects.requireNonNull; -import static org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBConverter.entityAttributeName; import static org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBConverter.toAttributeValue; import static org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBConverter.toCommunicationEntity; import static org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBConverter.toItem; @@ -81,10 +80,6 @@ public DefaultDynamoDBDatabaseManager(String database, DynamoDbClient dynamoDbCl this.dynamoDbClient = dynamoDbClient; } - private String resolveEntityNameAttributeName(String entityName) { - return this.settings.get(DynamoDBConfigurations.ENTITY_PARTITION_KEY, String.class).orElse(entityName); - } - public DynamoDbClient dynamoDbClient() { return dynamoDbClient; } @@ -99,7 +94,7 @@ public CommunicationEntity insert(CommunicationEntity documentEntity) { requireNonNull(documentEntity, "documentEntity is required"); dynamoDbClient().putItem(PutItemRequest.builder() .tableName(createTableIfNeeded(documentEntity.name()).table().tableName()) - .item(toItem(this::resolveEntityNameAttributeName, documentEntity)) + .item(toItem(documentEntity)) .build()); return documentEntity; } @@ -141,11 +136,12 @@ private DescribeTableResponse getDescribeTableResponse(String tableName) { private DescribeTableResponse createTable(String tableName) { try (var waiter = dynamoDbClient().waiter()) { + dynamoDbClient().createTable(CreateTableRequest.builder() .tableName(tableName) - .keySchema(defaultKeySchemaFor()) - .attributeDefinitions(defaultAttributeDefinitionsFor()) - .provisionedThroughput(defaultProvisionedThroughputFor()) + .keySchema(defaultKeySchemaFor(tableName)) + .attributeDefinitions(defaultAttributeDefinitionsFor(tableName)) + .provisionedThroughput(defaultProvisionedThroughputFor(tableName)) .build()); var tableRequest = DescribeTableRequest.builder().tableName(tableName).build(); @@ -154,34 +150,40 @@ private DescribeTableResponse createTable(String tableName) { } } - private ProvisionedThroughput defaultProvisionedThroughputFor() { - return DynamoTableUtils.createProvisionedThroughput(null, null); + private ProvisionedThroughput defaultProvisionedThroughputFor(String tableName) { + return DynamoTableUtils.createProvisionedThroughput( + this.settings.get(DynamoDBConfigurations.ENTITY_READ_CAPACITY_UNITS.get().formatted(tableName), Long.class) + .orElse(null), + this.settings.get(DynamoDBConfigurations.ENTITY_WRITE_CAPACITY_UNITS.get().formatted(tableName), Long.class) + .orElse(null)); } - private Collection defaultAttributeDefinitionsFor() { + private Collection defaultAttributeDefinitionsFor(String tableName) { return List.of( - AttributeDefinition.builder().attributeName(getEntityAttributeName()).attributeType(ScalarAttributeType.S).build(), - AttributeDefinition.builder().attributeName(DynamoDBConverter.ID).attributeType(ScalarAttributeType.S).build() + AttributeDefinition.builder() + .attributeName(partitionKeyName(tableName, DynamoDBConverter.ID)).attributeType(ScalarAttributeType.S).build() ); } - private Collection defaultKeySchemaFor() { + private Collection defaultKeySchemaFor(String tableName) { return List.of( - KeySchemaElement.builder().attributeName(getEntityAttributeName()).keyType(KeyType.HASH).build(), - KeySchemaElement.builder().attributeName(DynamoDBConverter.ID).keyType(KeyType.RANGE).build() + KeySchemaElement.builder() + .attributeName(partitionKeyName(tableName, DynamoDBConverter.ID)).keyType(KeyType.HASH).build() ); } + private String partitionKeyName(String table, String defaultName) { + return this.settings + .get(DynamoDBConfigurations.ENTITY_PARTITION_KEY.name().formatted(table), String.class) + .orElse(defaultName); + } + private boolean shouldCreateTables() { return this.settings .get(DynamoDBConfigurations.CREATE_TABLES, Boolean.class) .orElse(false); } - private String getEntityAttributeName() { - return entityAttributeName(this::resolveEntityNameAttributeName); - } - @Override public CommunicationEntity insert(CommunicationEntity documentEntity, Duration ttl) { requireNonNull(documentEntity, "documentEntity is required"); @@ -233,12 +235,11 @@ private Map getItemKey(CommunicationEntity documentEntit a.putAll(b); return a; }); - itemKey.put(getEntityAttributeName(), toAttributeValue(documentEntity.name())); return itemKey; } private Map asItemToUpdate(CommunicationEntity documentEntity) { - return toItemUpdate(this::resolveEntityNameAttributeName, documentEntity); + return toItemUpdate(documentEntity); } @Override @@ -278,22 +279,29 @@ public void delete(DeleteQuery deleteQuery) { public Stream select(SelectQuery query) { Objects.requireNonNull(query, "query is required"); DynamoDBQuery dynamoDBQuery = DynamoDBQuery - .builderOf(query.name(), getEntityAttributeName(), query) + .builderOf(query.name(), query) .get(); ScanRequest.Builder selectRequest = ScanRequest.builder() .consistentRead(true) - .tableName(dynamoDBQuery.table()) + .tableName(createTableIfNeeded(dynamoDBQuery.table()).table().tableName()) .projectionExpression(dynamoDBQuery.projectionExpression()) - .filterExpression(dynamoDBQuery.filterExpression()) - .expressionAttributeNames(dynamoDBQuery.expressionAttributeNames()) - .expressionAttributeValues(dynamoDBQuery.expressionAttributeValues()) .select(dynamoDBQuery.projectionExpression() != null ? Select.SPECIFIC_ATTRIBUTES : Select.ALL_ATTRIBUTES); + if (!dynamoDBQuery.filterExpression().isBlank()) { + selectRequest = selectRequest.filterExpression(dynamoDBQuery.filterExpression()); + } + + if (!dynamoDBQuery.expressionAttributeNames().isEmpty()) { + selectRequest = selectRequest + .expressionAttributeNames(dynamoDBQuery.expressionAttributeNames()) + .expressionAttributeValues(dynamoDBQuery.expressionAttributeValues()); + } + return StreamSupport .stream(dynamoDbClient().scanPaginator(selectRequest.build()).spliterator(), false) .flatMap(scanResponse -> scanResponse.items().stream() - .map(item -> toCommunicationEntity(this::resolveEntityNameAttributeName, item))); + .map(item -> toCommunicationEntity(dynamoDBQuery.table(), item))); } @Override @@ -314,7 +322,7 @@ public void close() { } @Override - public Stream partiQL(String query, Object... params) { + public Stream partiQL(String query, String entityName, Object... params) { Objects.requireNonNull(query, "query is required"); List parameters = Stream.of(params).map(DynamoDBConverter::toAttributeValue).toList(); ExecuteStatementResponse executeStatementResponse = dynamoDbClient() @@ -323,7 +331,7 @@ public Stream partiQL(String query, Object... params) { .parameters(parameters) .build()); List result = new LinkedList<>(); - executeStatementResponse.items().forEach(item -> result.add(toCommunicationEntity(this::resolveEntityNameAttributeName, item))); + executeStatementResponse.items().forEach(item -> result.add(toCommunicationEntity(entityName, item))); while (executeStatementResponse.nextToken() != null) { executeStatementResponse = dynamoDbClient() .executeStatement(ExecuteStatementRequest.builder() @@ -331,7 +339,7 @@ public Stream partiQL(String query, Object... params) { .parameters(parameters) .nextToken(executeStatementResponse.nextToken()) .build()); - executeStatementResponse.items().forEach(item -> result.add(toCommunicationEntity(this::resolveEntityNameAttributeName, item))); + executeStatementResponse.items().forEach(item -> result.add(toCommunicationEntity(entityName, item))); } return result.stream(); } diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBConfigurations.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBConfigurations.java index 34d03ec8c..0ad75a02b 100644 --- a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBConfigurations.java +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBConfigurations.java @@ -23,8 +23,11 @@ public enum DynamoDBConfigurations implements Supplier { PROFILE("jnosql.dynamodb.profile"), AWS_ACCESSKEY("jnosql.dynamodb.awsaccesskey"), AWS_SECRET_ACCESS("jnosql.dynamodb.secretaccess"), - ENTITY_PARTITION_KEY("jnosql.dynamodb.entity.pk"), - CREATE_TABLES("jnosql.dynamodb.create.tables"); + CREATE_TABLES("jnosql.dynamodb.create.tables"), + ENTITY_PARTITION_KEY("jnosql.dynamodb.%s.pk"), + ENTITY_READ_CAPACITY_UNITS("jnosql.dynamodb.%s.read.capacity.units"), + ENTITY_WRITE_CAPACITY_UNITS("jnosql.dynamodb.%s.write.capacity.units"), + ; private final String configuration; diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBConverter.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBConverter.java index cad3665f2..7df4350e4 100644 --- a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBConverter.java +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBConverter.java @@ -29,17 +29,14 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.function.Consumer; -import java.util.function.UnaryOperator; import java.util.stream.StreamSupport; import static java.util.Collections.singletonMap; class DynamoDBConverter { - static final String ENTITY = "@entity"; - static final String ID = "id"; + static final String ID = "_id"; private DynamoDBConverter() { } @@ -64,18 +61,12 @@ private static Object convertValue(Object value) { return value; } - static Map getMap(UnaryOperator entityNameResolver, CommunicationEntity entity) { - var nameResolver = Optional.ofNullable(entityNameResolver).orElse(UnaryOperator.identity()); + static Map getMap(CommunicationEntity entity) { Map jsonObject = new HashMap<>(); entity.elements().forEach(feedJSON(jsonObject)); - jsonObject.put(entityAttributeName(nameResolver), entity.name()); return jsonObject; } - public static String entityAttributeName(UnaryOperator nameResolver) { - return Optional.ofNullable(nameResolver.apply(ENTITY)).orElse(ENTITY); - } - @SuppressWarnings({"rawtypes", "unchecked"}) private static Consumer feedJSON(Map jsonObject) { return d -> { @@ -111,9 +102,8 @@ private static boolean isSudDocumentList(Object value) { allMatch(d -> d instanceof Iterable && isSudDocument(d)); } - public static Map toItem(UnaryOperator entityNameResolver, CommunicationEntity entity) { - UnaryOperator resolver = Optional.ofNullable(entityNameResolver).orElse(UnaryOperator.identity()); - Map documentAttributes = getMap(resolver, entity); + public static Map toItem(CommunicationEntity entity) { + Map documentAttributes = getMap(entity); return toItem(documentAttributes); } @@ -156,9 +146,8 @@ public static AttributeValue toAttributeValue(Object value) { return AttributeValue.builder().s(String.valueOf(value)).build(); } - public static Map toItemUpdate(UnaryOperator entityNameResolver, CommunicationEntity entity) { - UnaryOperator resolver = Optional.ofNullable(entityNameResolver).orElse(UnaryOperator.identity()); - Map documentAttributes = getMap(resolver, entity); + public static Map toItemUpdate(CommunicationEntity entity) { + Map documentAttributes = getMap(entity); return toItemUpdate(documentAttributes); } @@ -178,19 +167,15 @@ public static AttributeValueUpdate toAttributeValueUpdate(Object value) { } - public static CommunicationEntity toCommunicationEntity(UnaryOperator entityNameResolver, Map item) { + public static CommunicationEntity toCommunicationEntity(String entityName, Map item) { if (item == null) { return null; } if (item.isEmpty()) { return null; } - UnaryOperator resolver = Optional.ofNullable(entityNameResolver).orElse(UnaryOperator.identity()); - String entityAttribute = resolver.apply(ENTITY); - var entityName = item.containsKey(entityAttribute) ? item.get(entityAttribute).s() : entityAttribute; var elements = item.entrySet() .stream() - .filter(entry -> !Objects.equals(entityAttribute, entry.getKey())) .map(entry -> Element.of(entry.getKey(), convertValue(entry.getValue()))) .toList(); return CommunicationEntity.of(entityName, elements); diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBDatabaseManager.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBDatabaseManager.java index fca88ca2f..5a3768ed9 100644 --- a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBDatabaseManager.java +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBDatabaseManager.java @@ -35,7 +35,7 @@ public interface DynamoDBDatabaseManager extends DatabaseManager { * @return a {@link Stream} of {@link CommunicationEntity} representing the query result * @throws NullPointerException when the query is null */ - Stream partiQL(String query, Object... params); + Stream partiQL(String query, String entityName, Object... params); /** diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBQuery.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBQuery.java index bad0a0a06..df75ac9ab 100644 --- a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBQuery.java +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBQuery.java @@ -28,8 +28,7 @@ public record DynamoDBQuery(String table, Map expressionAttributeValues) { public static Supplier builderOf(String table, - String partitionKey, SelectQuery query) { - return new DynamoDBQuerySelectBuilder(table, partitionKey, query); + return new DynamoDBQuerySelectBuilder(table, query); } } diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBQuerySelectBuilder.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBQuerySelectBuilder.java index c0ae3d83b..85a1a1c06 100644 --- a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBQuerySelectBuilder.java +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBQuerySelectBuilder.java @@ -15,26 +15,23 @@ package org.eclipse.jnosql.databases.dynamodb.communication; -import org.eclipse.jnosql.communication.semistructured.CriteriaCondition; -import org.eclipse.jnosql.communication.semistructured.Element; import org.eclipse.jnosql.communication.semistructured.SelectQuery; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; class DynamoDBQuerySelectBuilder extends DynamoDBQueryBuilder { private final String table; - private final String partitionKey; - private final SelectQuery selectQuery; public DynamoDBQuerySelectBuilder(String table, - String partitionKey, SelectQuery selectQuery) { this.table = table; - this.partitionKey = partitionKey; this.selectQuery = selectQuery; } @@ -45,33 +42,33 @@ public DynamoDBQuery get() { var expressionAttributeNames = new HashMap(); var expressionAttributeValues = new HashMap(); - super.condition( - CriteriaCondition.eq(Element.of(partitionKey, selectQuery.name())), - filterExpression, expressionAttributeNames, expressionAttributeValues); - this.selectQuery.condition().ifPresent(c -> { - filterExpression.append(" AND "); super.condition(c, filterExpression, expressionAttributeNames, expressionAttributeValues); }); - return new DynamoDBQuery( table, - projectionExpression(), + projectionExpression(expressionAttributeNames), filterExpression.toString(), expressionAttributeNames, expressionAttributeValues); } - String projectionExpression() { + String projectionExpression(HashMap expressionAttributeNames) { var columns = selectQuery.columns(); if (columns.isEmpty()) { return null; } - return String.join(", ", columns); + List projectionAttributes = new ArrayList<>(columns.size()); + columns.forEach(column -> { + var alias = "#%s".formatted(column); + expressionAttributeNames.computeIfAbsent(alias, k -> column); + projectionAttributes.add(alias); + }); + return projectionAttributes.stream().collect(Collectors.joining(",")); } diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBUtils.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBUtils.java index dd1eb065f..33b76aa0b 100644 --- a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBUtils.java +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBUtils.java @@ -128,4 +128,24 @@ public static GetItemRequest createGetItemRequest(K key, String tableName) { GetItemRequest.Builder getItemRequest = GetItemRequest.builder(); return getItemRequest.tableName(tableName).key(createKeyAttributeValues(key)).build(); } + + public static String replaceInvalidCharactersForKey(String attributeName) { + if (attributeName == null || attributeName.isEmpty()) + return attributeName; + StringBuilder sb = new StringBuilder(); + for (char c : attributeName.trim().toCharArray()) { + if (isEnglishLetterOrDigit(c) || c == '_') { + sb.append(c); + } else { + sb.append("_").append((int) c); + } + } + return sb.toString(); + } + + private static boolean isEnglishLetterOrDigit(char c) { + return (c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9'); + } } diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DefaultDynamoDBTemplate.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DefaultDynamoDBTemplate.java index 86db83600..dd8581365 100644 --- a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DefaultDynamoDBTemplate.java +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DefaultDynamoDBTemplate.java @@ -27,9 +27,10 @@ import org.eclipse.jnosql.mapping.semistructured.EntityConverter; import org.eclipse.jnosql.mapping.semistructured.EventPersistManager; -import java.util.Objects; import java.util.stream.Stream; +import static java.util.Objects.requireNonNull; + @Typed(DynamoDBTemplate.class) @ApplicationScoped class DefaultDynamoDBTemplate extends AbstractSemiStructuredTemplate implements DynamoDBTemplate { @@ -92,16 +93,12 @@ protected Converters converters() { @Override @SuppressWarnings("unchecked") - public Stream partiQL(String query) { - Objects.requireNonNull(query, "query is required"); - return manager.get().partiQL(query).map(converter::toEntity).map(d -> (T) d); - } - - @Override - @SuppressWarnings("unchecked") - public Stream partiQL(String query, Object... params) { - Objects.requireNonNull(query, "query is required"); - Objects.requireNonNull(params, "params is required"); - return manager.get().partiQL(query, params).map(converter::toEntity).map(d -> (T) d); + public Stream partiQL(String query, Class entityType, Object... params) { + requireNonNull(query, "query is required"); + requireNonNull(entityType, "entityType is required"); + requireNonNull(params, "params is required"); + var entityMetadata = entities().findByClassName(entityType.getName()) + .orElseThrow(() -> new IllegalArgumentException("Entity type not found: " + entityType.getName())); + return manager.get().partiQL(query, entityMetadata.name(), params).map(converter::toEntity).map(d -> (T) d); } } diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBRepositoryProxy.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBRepositoryProxy.java index e61a193c6..97dd805d6 100644 --- a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBRepositoryProxy.java +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBRepositoryProxy.java @@ -33,6 +33,7 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.function.Supplier; import java.util.stream.Stream; import static org.eclipse.jnosql.mapping.core.repository.DynamicReturn.toSingleResult; @@ -47,6 +48,8 @@ class DynamoDBRepositoryProxy extends AbstractSemiStructuredRepositoryProx private final Class typeClass; + private final EntitiesMetadata entitiesMetadata; + private final EntityMetadata entityMetadata; private final AbstractRepository repository; @@ -62,6 +65,7 @@ class DynamoDBRepositoryProxy extends AbstractSemiStructuredRepositoryProx this.type = type; this.typeClass = (Class) ((ParameterizedType) type.getGenericInterfaces()[0]).getActualTypeArguments()[0]; this.converters = converters; + this.entitiesMetadata = entitiesMetadata; this.entityMetadata = entitiesMetadata.get(typeClass); this.repository = SemiStructuredRepositoryProxy.SemiStructuredRepository.of(template, entityMetadata); } @@ -75,6 +79,7 @@ class DynamoDBRepositoryProxy extends AbstractSemiStructuredRepositoryProx this.type = null; this.typeClass = null; this.converters = null; + this.entitiesMetadata = null; this.entityMetadata = null; this.repository = null; } @@ -84,18 +89,16 @@ class DynamoDBRepositoryProxy extends AbstractSemiStructuredRepositoryProx public Object invoke(Object instance, Method method, Object[] args) throws Throwable { PartiQL sql = method.getAnnotation(PartiQL.class); if (Objects.nonNull(sql)) { - Stream result; + + Class returnType = (Class) method.getReturnType(); List params = getParams(args, method); - if (params.isEmpty()) { - result = template.partiQL(sql.value()); - } else { - result = template.partiQL(sql.value(), params.toArray()); - } + Supplier> resultSupplier = () -> template.partiQL(sql.value(), typeClass, params.toArray()); + return DynamicReturn.builder() .classSource(typeClass) .methodSource(method) - .result(() -> result) - .singleResult(toSingleResult(method).apply(() -> result)) + .result(resultSupplier) + .singleResult(toSingleResult(method).apply(resultSupplier::get)) .build().execute(); } return super.invoke(instance, method, args); @@ -127,7 +130,7 @@ protected Converters converters() { } @Override - @SuppressWarnings({"rawtypes","unchecked"}) + @SuppressWarnings({"rawtypes", "unchecked"}) protected AbstractRepository repository() { return repository; } @@ -137,6 +140,11 @@ protected Class repositoryType() { return type; } + @Override + protected EntitiesMetadata entitiesMetadata() { + return entitiesMetadata; + } + @Override protected EntityMetadata entityMetadata() { return entityMetadata; diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBTemplate.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBTemplate.java index 57bd7c557..6998379fa 100644 --- a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBTemplate.java +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBTemplate.java @@ -39,10 +39,13 @@ public interface DynamoDBTemplate extends DocumentTemplate { * PartiQL. * * @param query the PartiQL query + * @param entityType the class of the result entity type * @return a {@link Stream} of results representing the query result * @throws NullPointerException when the query is null */ - Stream partiQL(String query); + default Stream partiQL(String query, Class entityType){ + return partiQL(query, entityType, new Object[0]); + } /** * Executes a DynamoDB query using @@ -50,8 +53,9 @@ public interface DynamoDBTemplate extends DocumentTemplate { *

Example query: {@code SELECT * FROM users WHERE status = ?}

* * @param query the PartiQL query + * @param entityType the class of the result entity type * @return a {@link Stream} of results representing the query result * @throws NullPointerException when the query is null */ - Stream partiQL(String query, Object... params); + Stream partiQL(String query, Class entityType, Object... params); } diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DefaultDynamoDBDatabaseManagerTest.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DefaultDynamoDBDatabaseManagerTest.java index 4bbb3c644..bf3688580 100644 --- a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DefaultDynamoDBDatabaseManagerTest.java +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DefaultDynamoDBDatabaseManagerTest.java @@ -55,7 +55,7 @@ @EnabledIfSystemProperty(named = NAMED, matches = MATCHES) class DefaultDynamoDBDatabaseManagerTest { - private static Faker faker = new Faker(); + private static final Faker faker = new Faker(); private DynamoDbClient dynamoDbClient; @@ -431,13 +431,14 @@ void shouldExecutePartiQL() { if (manager instanceof DynamoDBDatabaseManager partiManager) { - assertSoftly(softly -> softly.assertThat(partiManager.partiQL("SELECT * FROM " + entity1.name())) + assertSoftly(softly -> softly.assertThat(partiManager.partiQL("SELECT * FROM " + entity1.name(), entity1.name())) .as("the returned count number of items from a given DocumentQuery is incorrect") .hasSize(3)); assertSoftly(softly -> softly.assertThat(partiManager.partiQL(""" SELECT * FROM %s WHERE %s = ? """.formatted(entity1.name(), ID), + entity1.name(), entity1.find(ID).orElseThrow().get())) .as("the returned count number of items from a given DocumentQuery is incorrect") .hasSize(1)); diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBConverterTest.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBConverterTest.java index 67f3c56ee..a9f1bc046 100644 --- a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBConverterTest.java +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBConverterTest.java @@ -27,7 +27,6 @@ import java.io.StringReader; import java.util.List; import java.util.UUID; -import java.util.function.UnaryOperator; import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.eclipse.jnosql.communication.driver.IntegrationTest.MATCHES; @@ -40,8 +39,6 @@ class DynamoDBConverterTest { private static final Jsonb JSONB = JsonbSupplier.getInstance().get(); - private static final UnaryOperator entityNameResolver = UnaryOperator.identity(); - @Test void shouldConvertToItemRequest() { @@ -57,13 +54,13 @@ void shouldConvertToItemRequest() { Element.of("phones", List.of(faker.name().firstName(), faker.name().firstName(), faker.name().firstName())) )); - var item = DynamoDBConverter.toItem(entityNameResolver, entity); + var item = DynamoDBConverter.toItem(entity); - var entityFromItem = DynamoDBConverter.toCommunicationEntity(entityNameResolver, item); + var entityFromItem = DynamoDBConverter.toCommunicationEntity("entityA", item); - var expected = Json.createReader(new StringReader(JSONB.toJson(DynamoDBConverter.getMap(entityNameResolver, entity)))).readObject(); + var expected = Json.createReader(new StringReader(JSONB.toJson(DynamoDBConverter.getMap(entity)))).readObject(); - var actual = Json.createReader(new StringReader(JSONB.toJson(DynamoDBConverter.getMap(entityNameResolver, entityFromItem)))).readObject(); + var actual = Json.createReader(new StringReader(JSONB.toJson(DynamoDBConverter.getMap(entityFromItem)))).readObject(); softly.assertThat(actual).as("cannot convert a simple DocumentEntity") .isEqualTo(expected); diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBTestUtils.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBTestUtils.java index 66c593c4c..53725cf75 100644 --- a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBTestUtils.java +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBTestUtils.java @@ -14,6 +14,7 @@ */ package org.eclipse.jnosql.databases.dynamodb.communication; +import jakarta.nosql.DiscriminatorColumn; import org.eclipse.jnosql.communication.Settings; import org.eclipse.jnosql.communication.SettingsBuilder; import org.eclipse.jnosql.communication.keyvalue.BucketManagerFactory; @@ -31,7 +32,7 @@ public enum DynamoDBTestUtils { private final GenericContainer dynamodb = new GenericContainer("amazon/dynamodb-local:latest") - .withReuse(true) + //.withReuse(true) .withExposedPorts(8000) .waitingFor(Wait.defaultWaitStrategy()); @@ -84,7 +85,7 @@ public static SettingsBuilder getSettingsBuilder(UnaryOperator .put(DynamoDBConfigurations.AWS_SECRET_ACCESS, System.getProperty("AWS_SECRET_ACCESS_KEY", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY")) .put(DynamoDBConfigurations.PROFILE, System.getProperty("AWS_PROFILE", "default")) .put(DynamoDBConfigurations.REGION, "us-west-2") - .put(DynamoDBConfigurations.ENTITY_PARTITION_KEY, "entityType"); + .put(DynamoDBConfigurations.ENTITY_PARTITION_KEY, DiscriminatorColumn.DEFAULT_DISCRIMINATOR_COLUMN); } public void setupSystemProperties(SettingsBuilder builder) { diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/DefaultDynamoDBTemplateTest.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/DefaultDynamoDBTemplateTest.java index 16f095a3a..2faaa9ceb 100644 --- a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/DefaultDynamoDBTemplateTest.java +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/DefaultDynamoDBTemplateTest.java @@ -75,14 +75,14 @@ void setup() { @Test void shouldFindSQL() { - template.partiQL("select from database"); - Mockito.verify(manager).partiQL("select from database"); + template.partiQL("select from human", Human.class); + Mockito.verify(manager).partiQL("select from human", "Human"); } @Test void shouldFindSQLWithTypeAndParameters() { - template.partiQL("select from database where content.name = ?", List.of("Ada"), String.class); - Mockito.verify(manager).partiQL("select from database where content.name = ?", List.of("Ada"), String.class); + template.partiQL("select from database where content.name = ?", Human.class, List.of("Ada"), String.class); + Mockito.verify(manager).partiQL("select from database where content.name = ?", "Human", List.of("Ada"), String.class); } @Test diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBRepositoryProxyTest.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBRepositoryProxyTest.java index 279d1a214..6441c904d 100644 --- a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBRepositoryProxyTest.java +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBRepositoryProxyTest.java @@ -15,6 +15,7 @@ package org.eclipse.jnosql.databases.dynamodb.mapping; +import ee.jakarta.tck.nosql.entities.Person; import jakarta.data.repository.Param; import jakarta.inject.Inject; import org.assertj.core.api.Assertions; @@ -79,14 +80,14 @@ void setUp() { @Test public void shouldFindAll() { personRepository.findAllQuery(); - verify(template).partiQL("select * from Person"); + verify(template).partiQL(eq("select * from Person"), eq(Human.class), eq(new Object[0])); } @Test public void shouldFindByNameSQL() { ArgumentCaptor captor = ArgumentCaptor.forClass(Object[].class); personRepository.findByName("Ada"); - verify(template).partiQL(eq("select * from Person where name= ?"), captor.capture()); + verify(template).partiQL(eq("select * from Person where name= ?"), eq(Human.class), captor.capture()); Object[] value = captor.getValue(); Assertions.assertThat(value).hasSize(1).contains("Ada"); @@ -109,14 +110,14 @@ public void shouldSaveUsingUpdate() { } @Test - public void shouldDelete(){ + public void shouldDelete() { personRepository.deleteById("id"); verify(template).delete(Human.class, "id"); } @Test - public void shouldDeleteEntity(){ + public void shouldDeleteEntity() { Human human = Human.of("Ada", 10); personRepository.delete(human); verify(template).delete(Human.class, human.getName()); diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/BigProject.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/BigProject.java new file mode 100644 index 000000000..11e90740b --- /dev/null +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/BigProject.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ +package org.eclipse.jnosql.databases.dynamodb.mapping.inheritance; + +import jakarta.nosql.Column; +import jakarta.nosql.DiscriminatorValue; +import jakarta.nosql.Entity; + +import java.util.Objects; +import java.util.UUID; + +@Entity +@DiscriminatorValue("big_project") +public class BigProject extends Project { + + @Column + private String bigProjectName; + + public static BigProject of(String name, String bigProjectName) { + return of(UUID.randomUUID().toString(), name, bigProjectName); + } + + public static BigProject of(String id, String name, String bigProjectName) { + return new BigProject(id, name, bigProjectName); + } + + public BigProject() {} + + public BigProject(String id, String name, String bigProjectName) { + super(id, name); + this.bigProjectName = bigProjectName; + } + + public String getBigProjectName() { + return bigProjectName; + } + + public void setBigProjectName(String bigProjectName) { + this.bigProjectName = bigProjectName; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + return super.equals(o); + } + + @Override + public int hashCode() { + return Objects.hash(getClass().hashCode(), super.hashCode()); + } + +} diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/BigProjects.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/BigProjects.java new file mode 100644 index 000000000..664447cbb --- /dev/null +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/BigProjects.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ +package org.eclipse.jnosql.databases.dynamodb.mapping.inheritance; + +import jakarta.data.repository.*; +import org.eclipse.jnosql.mapping.NoSQLRepository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface BigProjects extends NoSQLRepository { + +} diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/InheritanceWithCustomRepositoryTest.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/InheritanceWithCustomRepositoryTest.java new file mode 100644 index 000000000..3c8ae020d --- /dev/null +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/InheritanceWithCustomRepositoryTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ +package org.eclipse.jnosql.databases.dynamodb.mapping.inheritance; + +import jakarta.inject.Inject; +import org.assertj.core.api.SoftAssertions; +import org.eclipse.jnosql.communication.Settings; +import org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBConfigurations; +import org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBTestUtils; +import org.eclipse.jnosql.databases.dynamodb.mapping.DynamoDBExtension; +import org.eclipse.jnosql.databases.dynamodb.mapping.DynamoDBTemplate; +import org.eclipse.jnosql.databases.dynamodb.mapping.PartiQL; +import org.eclipse.jnosql.mapping.Database; +import org.eclipse.jnosql.mapping.DatabaseType; +import org.eclipse.jnosql.mapping.core.Converters; +import org.eclipse.jnosql.mapping.document.DocumentTemplate; +import org.eclipse.jnosql.mapping.document.spi.DocumentExtension; +import org.eclipse.jnosql.mapping.reflection.Reflections; +import org.eclipse.jnosql.mapping.reflection.spi.ReflectionEntityMetadataExtension; +import org.eclipse.jnosql.mapping.semistructured.EntityConverter; +import org.jboss.weld.junit5.auto.AddExtensions; +import org.jboss.weld.junit5.auto.AddPackages; +import org.jboss.weld.junit5.auto.EnableAutoWeld; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +@EnableAutoWeld +@AddPackages({Database.class, + EntityConverter.class, + DocumentTemplate.class, + DynamoDBTemplate.class}) +@AddPackages(PartiQL.class) +@AddPackages(Project.class) +@AddPackages(Reflections.class) +@AddPackages(Converters.class) +@AddExtensions({ReflectionEntityMetadataExtension.class, DocumentExtension.class, DynamoDBExtension.class}) +public class InheritanceWithCustomRepositoryTest { + + static { + DynamoDBTestUtils.CONFIG.setupSystemProperties(Settings.builder() + .put(DynamoDBConfigurations.CREATE_TABLES, "true")); + } + + @Inject + @Database(DatabaseType.DOCUMENT) + Projects projects; + + @BeforeEach + @AfterEach + void clearData() { + projects.findAll().forEach(projects::delete); + } + + @Test + void shouldInsert() { + List smallProjectList = new ArrayList<>(); + SmallProject smallProjectA = Project.smallProject("s-project A", "Small Project A"); + smallProjectList.add(smallProjectA); + smallProjectList.add(Project.smallProject("s-project B", "Small Project B")); + smallProjectList.add(Project.smallProject("s-project C", "Small Project C")); + + smallProjectList.forEach(projects::save); + + List bigProjectList = new ArrayList<>(); + bigProjectList.add(Project.bigProject("b-project A", "Big Project A")); + BigProject bigProjectB = Project.bigProject("b-project B", "Big Project B"); + bigProjectList.add(bigProjectB); + + bigProjectList.forEach(projects::save); + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(projects.findById(smallProjectA.getId())) + .isNotEmpty() + .contains(smallProjectA); + + softly.assertThat(projects.findById(bigProjectB.getId())) + .isNotEmpty() + .contains(bigProjectB); + + softly.assertThat(projects.findAllSmallProjects()) + .isNotEmpty() + .hasSize(smallProjectList.size()) + .containsExactlyInAnyOrderElementsOf(smallProjectList); + + softly.assertThat(projects.findAllBigProjects()) + .isNotEmpty() + .hasSize(bigProjectList.size()) + .containsExactlyInAnyOrderElementsOf(bigProjectList); + + Stream all = projects.findAll(); + softly.assertThat(all) + .isNotEmpty() + .hasSize(smallProjectList.size() + bigProjectList.size()) + .containsExactlyInAnyOrderElementsOf(Stream.concat(smallProjectList.stream(), bigProjectList.stream()).toList()); + + }); + + + } + + +} + diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/InheritanceWithDifferentRepositoriesTest.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/InheritanceWithDifferentRepositoriesTest.java new file mode 100644 index 000000000..6fd6b3a90 --- /dev/null +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/InheritanceWithDifferentRepositoriesTest.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ +package org.eclipse.jnosql.databases.dynamodb.mapping.inheritance; + +import jakarta.inject.Inject; +import org.assertj.core.api.SoftAssertions; +import org.eclipse.jnosql.communication.Settings; +import org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBConfigurations; +import org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBTestUtils; +import org.eclipse.jnosql.databases.dynamodb.mapping.DynamoDBExtension; +import org.eclipse.jnosql.databases.dynamodb.mapping.DynamoDBTemplate; +import org.eclipse.jnosql.databases.dynamodb.mapping.PartiQL; +import org.eclipse.jnosql.mapping.Database; +import org.eclipse.jnosql.mapping.DatabaseType; +import org.eclipse.jnosql.mapping.core.Converters; +import org.eclipse.jnosql.mapping.document.DocumentTemplate; +import org.eclipse.jnosql.mapping.document.spi.DocumentExtension; +import org.eclipse.jnosql.mapping.reflection.Reflections; +import org.eclipse.jnosql.mapping.reflection.spi.ReflectionEntityMetadataExtension; +import org.eclipse.jnosql.mapping.semistructured.EntityConverter; +import org.jboss.weld.junit5.auto.AddExtensions; +import org.jboss.weld.junit5.auto.AddPackages; +import org.jboss.weld.junit5.auto.EnableAutoWeld; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +@EnableAutoWeld +@AddPackages({Database.class, + EntityConverter.class, + DocumentTemplate.class, + DynamoDBTemplate.class}) +@AddPackages(PartiQL.class) +@AddPackages(Project.class) +@AddPackages(Reflections.class) +@AddPackages(Converters.class) +@AddExtensions({ReflectionEntityMetadataExtension.class, DocumentExtension.class, DynamoDBExtension.class}) +public class InheritanceWithDifferentRepositoriesTest { + + static { + DynamoDBTestUtils.CONFIG.setupSystemProperties(Settings.builder() + .put(DynamoDBConfigurations.CREATE_TABLES, "true")); + } + + @Inject + @Database(DatabaseType.DOCUMENT) + SmallProjects smallProjects; + + @Inject + @Database(DatabaseType.DOCUMENT) + BigProjects bigProjects; + + @BeforeEach + @AfterEach + void clearData() { + smallProjects.findAll().forEach(smallProjects::delete); + bigProjects.findAll().forEach(bigProjects::delete); + } + + @Test + void shouldInsert() { + List smallProjectList = new ArrayList<>(); + SmallProject smallProjectA = Project.smallProject("s-project A", "Small Project A"); + smallProjectList.add(smallProjectA); + smallProjectList.add(Project.smallProject("s-project B", "Small Project B")); + smallProjectList.add(Project.smallProject("s-project C", "Small Project C")); + + smallProjects.saveAll(smallProjectList); + + List bigProjectList = new ArrayList<>(); + bigProjectList.add(Project.bigProject("b-project A", "Big Project A")); + BigProject bigProjectB = Project.bigProject("b-project B", "Big Project B"); + bigProjectList.add(bigProjectB); + + bigProjects.saveAll(bigProjectList); + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(smallProjects.findById(smallProjectA.getId())) + .isNotEmpty() + .contains(smallProjectA); + + softly.assertThat(bigProjects.findById(bigProjectB.getId())) + .isNotEmpty() + .contains(bigProjectB); + + softly.assertThat(smallProjects.findAll()) + .isNotEmpty() + .hasSize(smallProjectList.size()) + .containsExactlyInAnyOrderElementsOf(smallProjectList); + + softly.assertThat(bigProjects.findAll()) + .isNotEmpty() + .hasSize(bigProjectList.size()) + .containsExactlyInAnyOrderElementsOf(bigProjectList); + + }); + + + } + + +} + diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/Project.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/Project.java new file mode 100644 index 000000000..40a76e9ee --- /dev/null +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/Project.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ +package org.eclipse.jnosql.databases.dynamodb.mapping.inheritance; + + +import jakarta.nosql.*; + +import java.util.Objects; + +@Entity +@Inheritance +@DiscriminatorColumn +public abstract class Project { + + @Id + private String id; + + @Column + private String name; + + public static SmallProject smallProject(String name, String smallProjectName) { + return SmallProject.of(name, smallProjectName); + } + + public static BigProject bigProject(String name, String smallProjectName) { + return BigProject.of(name, smallProjectName); + } + + public Project() { + } + + public Project(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Project project = (Project) o; + return Objects.equals(id, project.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } +} diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/Projects.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/Projects.java new file mode 100644 index 000000000..75c95390b --- /dev/null +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/Projects.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ +package org.eclipse.jnosql.databases.dynamodb.mapping.inheritance; + +import jakarta.data.repository.By; +import jakarta.data.repository.Delete; +import jakarta.data.repository.Find; +import jakarta.data.repository.Repository; +import jakarta.data.repository.Save; + +import java.util.Optional; +import java.util.stream.Stream; + +@Repository +public interface Projects { + + @Save + T save(T project); + + @Delete + void delete(Project project); + + @Find + Stream findAll(); + + @Find + Stream findAllSmallProjects(); + + @Find + Stream findAllBigProjects(); + + @Find + Optional findById(@By("_id") String id); +} diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/SmallProject.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/SmallProject.java new file mode 100644 index 000000000..55a7ea02f --- /dev/null +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/SmallProject.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ +package org.eclipse.jnosql.databases.dynamodb.mapping.inheritance; + +import jakarta.nosql.Column; +import jakarta.nosql.DiscriminatorValue; +import jakarta.nosql.Entity; + +import java.util.Objects; +import java.util.UUID; + +@Entity +@DiscriminatorValue("small_project") +public class SmallProject extends Project { + + @Column + private String smallProjectName; + + public static SmallProject of(String name, String smallProjectName) { + return of(UUID.randomUUID().toString(), name, smallProjectName); + } + + public static SmallProject of(String id, String name, String smallProjectName) { + return new SmallProject(id, name, smallProjectName); + } + + public SmallProject() { + } + + public SmallProject(String id, String name, String smallProjectName) { + super(id, name); + this.smallProjectName = smallProjectName; + } + + public String getSmallProjectName() { + return smallProjectName; + } + + public void setSmallProjectName(String smallProjectName) { + this.smallProjectName = smallProjectName; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + SmallProject that = (SmallProject) o; + return Objects.equals(this.getId(), that.getId()); + } + + @Override + public int hashCode() { + return Objects.hash(getClass().hashCode(), super.hashCode()); + } +} diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/SmallProjects.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/SmallProjects.java new file mode 100644 index 000000000..f5b7f2199 --- /dev/null +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/inheritance/SmallProjects.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ +package org.eclipse.jnosql.databases.dynamodb.mapping.inheritance; + +import jakarta.data.repository.Repository; +import org.eclipse.jnosql.mapping.NoSQLRepository; + +@Repository +public interface SmallProjects extends NoSQLRepository { + +} diff --git a/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JRepositoryProxy.java b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JRepositoryProxy.java index 97df827c5..a44633de2 100644 --- a/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JRepositoryProxy.java +++ b/jnosql-neo4j/src/main/java/org/eclipse/jnosql/databases/neo4j/mapping/Neo4JRepositoryProxy.java @@ -41,6 +41,8 @@ class Neo4JRepositoryProxy extends AbstractSemiStructuredRepositoryProxy< private final Converters converters; + private final EntitiesMetadata entitiesMetadata; + private final EntityMetadata entityMetadata; private final Class repositoryType; @@ -53,6 +55,7 @@ class Neo4JRepositoryProxy extends AbstractSemiStructuredRepositoryProxy< .getActualTypeArguments()[0]); this.converters = converters; + this.entitiesMetadata = entitiesMetadata; this.entityMetadata = entitiesMetadata.get(typeClass); this.repositoryType = repositoryType; this.repository = SemiStructuredRepositoryProxy.SemiStructuredRepository.of(template, entityMetadata); @@ -73,6 +76,11 @@ protected Class repositoryType() { return repositoryType; } + @Override + protected EntitiesMetadata entitiesMetadata() { + return entitiesMetadata; + } + @Override protected EntityMetadata entityMetadata() { return entityMetadata; diff --git a/jnosql-oracle-nosql/src/main/java/org/eclipse/jnosql/databases/oracle/mapping/OracleDocumentRepositoryProxy.java b/jnosql-oracle-nosql/src/main/java/org/eclipse/jnosql/databases/oracle/mapping/OracleDocumentRepositoryProxy.java index 1381118a6..04a1be2e1 100644 --- a/jnosql-oracle-nosql/src/main/java/org/eclipse/jnosql/databases/oracle/mapping/OracleDocumentRepositoryProxy.java +++ b/jnosql-oracle-nosql/src/main/java/org/eclipse/jnosql/databases/oracle/mapping/OracleDocumentRepositoryProxy.java @@ -45,6 +45,8 @@ class OracleDocumentRepositoryProxy extends AbstractSemiStructuredReposito private final Converters converters; + private final EntitiesMetadata entitiesMetadata; + private final EntityMetadata entityMetadata; @Inject @@ -57,6 +59,7 @@ class OracleDocumentRepositoryProxy extends AbstractSemiStructuredReposito .getActualTypeArguments()[0]); this.type = type; this.converters = converters; + this.entitiesMetadata = entitiesMetadata; this.entityMetadata = entitiesMetadata.get(typeClass); this.repository = SemiStructuredRepositoryProxy.SemiStructuredRepository.of(template, entityMetadata); } @@ -67,6 +70,7 @@ class OracleDocumentRepositoryProxy extends AbstractSemiStructuredReposito this.repository = null; this.type = null; this.converters = null; + this.entitiesMetadata = null; this.entityMetadata = null; } @@ -85,6 +89,11 @@ protected Converters converters() { return converters; } + @Override + protected EntitiesMetadata entitiesMetadata() { + return entitiesMetadata; + } + @Override protected EntityMetadata entityMetadata() { return entityMetadata; diff --git a/jnosql-orientdb/src/main/java/org/eclipse/jnosql/databases/orientdb/mapping/OrientDBDocumentRepositoryProxy.java b/jnosql-orientdb/src/main/java/org/eclipse/jnosql/databases/orientdb/mapping/OrientDBDocumentRepositoryProxy.java index f2cef97eb..0c4817011 100644 --- a/jnosql-orientdb/src/main/java/org/eclipse/jnosql/databases/orientdb/mapping/OrientDBDocumentRepositoryProxy.java +++ b/jnosql-orientdb/src/main/java/org/eclipse/jnosql/databases/orientdb/mapping/OrientDBDocumentRepositoryProxy.java @@ -46,6 +46,8 @@ class OrientDBDocumentRepositoryProxy extends AbstractSemiStructuredReposi private final Converters converters; + private final EntitiesMetadata entitiesMetadata; + private final EntityMetadata entityMetadata; @@ -57,6 +59,7 @@ class OrientDBDocumentRepositoryProxy extends AbstractSemiStructuredReposi .getActualTypeArguments()[0]); this.repositoryType = repositoryType; this.converters = converters; + this.entitiesMetadata = entitiesMetadata; this.entityMetadata = entitiesMetadata.get(typeClass); this.repository = SemiStructuredRepositoryProxy.SemiStructuredRepository.of(template, entityMetadata); } @@ -77,6 +80,11 @@ protected Converters converters() { return converters; } + @Override + protected EntitiesMetadata entitiesMetadata() { + return entitiesMetadata; + } + @Override protected EntityMetadata entityMetadata() { return entityMetadata; diff --git a/jnosql-solr/src/main/java/org/eclipse/jnosql/databases/solr/mapping/SolrRepositoryProxy.java b/jnosql-solr/src/main/java/org/eclipse/jnosql/databases/solr/mapping/SolrRepositoryProxy.java index 67199a805..5934467e3 100644 --- a/jnosql-solr/src/main/java/org/eclipse/jnosql/databases/solr/mapping/SolrRepositoryProxy.java +++ b/jnosql-solr/src/main/java/org/eclipse/jnosql/databases/solr/mapping/SolrRepositoryProxy.java @@ -44,6 +44,8 @@ class SolrRepositoryProxy extends AbstractSemiStructuredRepositoryProxy extends AbstractSemiStructuredRepositoryProxy extends AbstractSemiStructuredRepositoryPr private final Converters converters; + private final EntitiesMetadata entitiesMetadata; + private final EntityMetadata entityMetadata; private final Class repositoryType; @@ -55,6 +57,7 @@ class TinkerpopRepositoryProxy extends AbstractSemiStructuredRepositoryPr .getActualTypeArguments()[0]); this.converters = converters; + this.entitiesMetadata = entitiesMetadata; this.entityMetadata = entitiesMetadata.get(typeClass); this.repositoryType = repositoryType; this.repository = SemiStructuredRepositoryProxy.SemiStructuredRepository.of(template, entityMetadata); @@ -75,6 +78,11 @@ protected Class repositoryType() { return repositoryType; } + @Override + protected EntitiesMetadata entitiesMetadata() { + return entitiesMetadata; + } + @Override protected EntityMetadata entityMetadata() { return entityMetadata; diff --git a/pom.xml b/pom.xml index b08c4f899..5e283ec47 100644 --- a/pom.xml +++ b/pom.xml @@ -143,8 +143,8 @@ - oss.sonatype.org-snapshot - https://oss.sonatype.org/content/repositories/snapshots + central.sonatype.com-snapshot + https://central.sonatype.com/repository/maven-snapshots false