diff --git a/README.adoc b/README.adoc index 73d602e3b..c053f8595 100644 --- a/README.adoc +++ b/README.adoc @@ -52,8 +52,6 @@ Data Aerospike 5.0.0: Important Updates] == Spring Data Aerospike Compatibility .Compatibility Table -[%collapsible] -==== [width="100%",cols="<24%,<14%,<18%,<26%,<18%",options="header",] |=== |Spring Data Aerospike |Spring Boot |Aerospike Client |Aerospike Reactor Client |Aerospike Server @@ -97,7 +95,6 @@ Data Aerospike 5.0.0: Important Updates] |1.2.1.RELEASE |1.5.x |4.1.x | | |=== -==== == Quick Start diff --git a/pom.xml b/pom.xml index 45a46f3b6..ca53acb21 100644 --- a/pom.xml +++ b/pom.xml @@ -32,6 +32,7 @@ 1.6 9.0.5 9.0.5 + 0.1.0 3.7.4 3.1.11 2.13.1 @@ -195,6 +196,11 @@ aerospike-reactor-client ${aerospike-reactor-client} + + com.aerospike + aerospike-expression-dsl + ${aerospike-expression-dsl.version} + joda-time joda-time @@ -243,6 +249,10 @@ com.aerospike aerospike-reactor-client + + com.aerospike + aerospike-expression-dsl + com.esotericsoftware kryo diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index f4845f02a..6b878e92d 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -47,6 +47,7 @@ include::reference/query-methods-pojo.adoc[] include::reference/query-methods-id.adoc[] include::reference/query-methods-combined.adoc[] include::reference/query-methods-modification.adoc[] +include::reference/query-annotation.adoc[] include::reference/aerospike-object-mapping.adoc[] include::reference/aerospike-custom-converters.adoc[] include::reference/template.adoc[] diff --git a/src/main/asciidoc/reference/query-annotation.adoc b/src/main/asciidoc/reference/query-annotation.adoc new file mode 100644 index 000000000..4649e1685 --- /dev/null +++ b/src/main/asciidoc/reference/query-annotation.adoc @@ -0,0 +1,75 @@ +[[aerospike.query-annotation]] += Query Annotation + +For details on standard Spring Data Aerospike repository queries see <>. + +Another way of defining queries is leveraging `@Query` annotation for particular method names in the repository. + +This annotation allows to define custom query expressions directly within methods. Instead of relying on Spring Data's +automatic query derivation, you can use `@Query` to specify the exact String-based expression that should be executed. This provides greater flexibility for complex or specific data retrieval needs. + +NOTE: DSL expression provided via `@Query` annotation replaces the original query method that it is added to. + +== Using Placeholders + +Element `?0` in the following example is a placeholder that gets substituted with the actual query parameter, number 0 in it is referring to the parameter's index. + +[source,java] +---- +public interface PersonRepository extends AerospikeRepository { + + @Query(expression = "$.lastName == ?0") + List findByLastName(String lastName); +} +---- + +NOTE: Currently placeholders in `@Query` annotation can substitute only String, number and boolean parameters. + +== Using Static Expression + +DSL expression can be provided via `@Query` without placeholders, thus discarding query parameters. Here is an example: + +[source,java] +---- +public interface PersonRepository extends AerospikeRepository { + + @Query(expression = "$.lastName == 'Simpson''") + List findByLastName(String lastName); +} +---- + +NOTE: Such a query will always run with the same static expression " `lastName` bin has value 'Simpson' ". + +== Examples + +Below is an example of an interface with several `@Query`-annotated methods. + +[source,java] +---- +public interface PersonRepository extends AerospikeRepository { + + @Query(expression = "$.lastName == ?0") + List findByLastName(String lastName); + + @Query(expression = "$.age > ?0") + List

findByAgeGreaterThan(int age); + + @Query(expression = "$.isActive.get(type: BOOL) == ?0") + List

findByIsActive(boolean isActive); +} +---- + +== Custom Queries + +Just like with regular and metadata queries, you can use <> with DSL expressions when such flexibility is required or when there is no need to annotate repository methods. + +[source,java] +---- + // creating an expression "firsName is equal to John" using DSL expression + Qualifier firstNameEqJohn = Qualifier.dslStringBuilder() + .setDSLString("$.firstName == 'John'") + .build(); + result = repository.findUsingQuery(new Query(firstNameEqJohn)) + assertThat(result).containsOnly(john); +---- + diff --git a/src/main/asciidoc/reference/query-methods-collection.adoc b/src/main/asciidoc/reference/query-methods-collection.adoc index 81fae6713..ed31c977f 100644 --- a/src/main/asciidoc/reference/query-methods-collection.adoc +++ b/src/main/asciidoc/reference/query-methods-collection.adoc @@ -121,7 +121,7 @@ See https://docs.aerospike.com/server/guide/data-types/cdt-ordering#list[informa ---- findByStringListBetween(Collection lowerLimit, Collection upperLimit) ---- -|...where x.stringList between ? and ? +|...where x.stringList between ? (inclusive) and ? (exclusive) |only scan |Find records where `stringList` bin value is in the range between the given arguments. See https://docs.aerospike.com/server/guide/data-types/cdt-ordering#list[information about ordering]. diff --git a/src/main/asciidoc/reference/query-methods-map.adoc b/src/main/asciidoc/reference/query-methods-map.adoc index 895382e2d..b272680fa 100644 --- a/src/main/asciidoc/reference/query-methods-map.adoc +++ b/src/main/asciidoc/reference/query-methods-map.adoc @@ -119,7 +119,7 @@ See https://docs.aerospike.com/server/guide/data-types/cdt-ordering#map[informat ---- findByStringMapBetween(Map lowerLimit, Map upperLimit) ---- -|...where x.stringMap between ? and ? +|...where x.stringMap between ? (inclusive) and ? (exclusive) |only scan |Find records where `stringMap` bin value is in the range between the given arguments. See https://docs.aerospike.com/server/guide/data-types/cdt-ordering#map[information about ordering]. diff --git a/src/main/asciidoc/reference/query-methods-modification.adoc b/src/main/asciidoc/reference/query-methods-modification.adoc index 276021f0e..a080d9991 100644 --- a/src/main/asciidoc/reference/query-methods-modification.adoc +++ b/src/main/asciidoc/reference/query-methods-modification.adoc @@ -36,13 +36,15 @@ post-processing step. This limiting can affect performance, depending on the num results and limiting parameters. [[find-using-query]] -== Find Using Query +== Custom Queries -User can perform a custom `Query` for finding matching entities in the Aerospike database. -A `Query` can be created using a `Qualifier` which represents an expression. +You can perform a custom `Query` for finding matching entities in the Aerospike database via `findUsingQuery()` method. + +A `Query` object can be created using a `Qualifier` which represents an expression. It may contain other qualifiers and combine them using either `AND` or `OR`. -`Qualifier` can be created for regular bins, metadata and ids (primary keys). +`Qualifier` can be created for regular bins, metadata and ids (primary keys). It can also be created for a DSL expression. + Below is an example of different variations: [source,java] @@ -73,4 +75,11 @@ Below is an example of different variations: // expressions are combined using AND result = repository.findUsingQuery(new Query(Qualifier.and(firstNameEqJohn, keyEqJohnsId, sinceUpdateTimeLt50Seconds))); assertThat(result).containsOnly(john); + + // creating an expression "firsName is equal to John" using DSL expression + Qualifier firstNameEqJohn = Qualifier.dslStringBuilder() + .setDSLString("$.firstName == 'John'") + .build(); + result = repository.findUsingQuery(new Query(firstNameEqJohn)) + assertThat(result).containsOnly(john); ---- \ No newline at end of file diff --git a/src/main/asciidoc/reference/query-methods-pojo.adoc b/src/main/asciidoc/reference/query-methods-pojo.adoc index 0b2143c34..a8c00e715 100644 --- a/src/main/asciidoc/reference/query-methods-pojo.adoc +++ b/src/main/asciidoc/reference/query-methods-pojo.adoc @@ -120,7 +120,7 @@ See https://docs.aerospike.com/server/guide/data-types/cdt-ordering#map[informat ---- findByAddressBetween(Address lowerLimit, Address upperLimit) ---- -|...where x.address between ? and ? +|...where x.address between ? (inclusive) and ? (exclusive) |only scan |Find records where `address` bin value (POJOs are stored in AerospikeDB as maps) is in the range between the given arguments. See https://docs.aerospike.com/server/guide/data-types/cdt-ordering#map[information about ordering]. diff --git a/src/main/asciidoc/reference/query-methods-preface.adoc b/src/main/asciidoc/reference/query-methods-preface.adoc index 3e775e763..1b0b1fd73 100644 --- a/src/main/asciidoc/reference/query-methods-preface.adoc +++ b/src/main/asciidoc/reference/query-methods-preface.adoc @@ -1,7 +1,7 @@ [[aerospike.query-methods-preface]] = Query Methods -Spring Data Aerospike supports defining queries by method name in the Repository interface so that the implementation +Spring Data Aerospike supports defining queries by method names in the Repository interface so that the implementation is generated. The format of method names is fairly flexible, comprising a verb and criteria. @@ -19,7 +19,8 @@ Repository queries in Spring Data Aerospike can be divided into 3 groups: * Queries that only use <> -* Queries that use both secondary index and scan (when looking for a nested integer or string property). +* Queries that use both secondary index and scan (e.g., for indexed queries combined with AND +or when looking for a nested integer or string property). If no corresponding secondary index is found, each query does a fallback to using scan only. diff --git a/src/main/asciidoc/reference/query-methods-simple-property.adoc b/src/main/asciidoc/reference/query-methods-simple-property.adoc index 327eec036..0e93b6451 100644 --- a/src/main/asciidoc/reference/query-methods-simple-property.adoc +++ b/src/main/asciidoc/reference/query-methods-simple-property.adoc @@ -155,9 +155,9 @@ findByAgeBetween(int lowerLimit, int upperLimit) findByFirstNameBetween(String lowerLimit, String upperLimit) ---- -|...where x.age between ? and ? +|...where x.age between ? (inclusive) and ? (exclusive) -...where x.firstName between ? and ? +...where x.firstName between ? (inclusive) and ? (exclusive) |only for integers |Strings are compared by order of each byte, assuming they have UTF-8 encoding. See https://docs.aerospike.com/server/guide/data-types/cdt-ordering#string[information about ordering]. diff --git a/src/main/java/org/springframework/data/aerospike/annotation/Query.java b/src/main/java/org/springframework/data/aerospike/annotation/Query.java new file mode 100644 index 000000000..8b9f7f1c5 --- /dev/null +++ b/src/main/java/org/springframework/data/aerospike/annotation/Query.java @@ -0,0 +1,29 @@ +package org.springframework.data.aerospike.annotation; + +import org.springframework.data.annotation.QueryAnnotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Query annotation to define expressions for repository methods. Allows using either a fixed DSL String or + * a String with placeholders like {@code ?0}, {@code ?1} etc. + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Documented +@QueryAnnotation +@Beta +public @interface Query { + + /** + * Use expression DSL to define the query taking precedence over method name + * + * @return empty {@link String} by default. + */ + String expression(); +} diff --git a/src/main/java/org/springframework/data/aerospike/config/AbstractAerospikeDataConfiguration.java b/src/main/java/org/springframework/data/aerospike/config/AbstractAerospikeDataConfiguration.java index 486246cd0..d09c48db6 100644 --- a/src/main/java/org/springframework/data/aerospike/config/AbstractAerospikeDataConfiguration.java +++ b/src/main/java/org/springframework/data/aerospike/config/AbstractAerospikeDataConfiguration.java @@ -16,6 +16,7 @@ package org.springframework.data.aerospike.config; import com.aerospike.client.IAerospikeClient; +import com.aerospike.dsl.DSLParser; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.annotation.Bean; @@ -31,6 +32,7 @@ import org.springframework.data.aerospike.query.StatementBuilder; import org.springframework.data.aerospike.query.cache.IndexInfoParser; import org.springframework.data.aerospike.query.cache.IndexRefresher; +import org.springframework.data.aerospike.query.cache.IndexesCacheHolder; import org.springframework.data.aerospike.query.cache.IndexesCacheUpdater; import org.springframework.data.aerospike.query.cache.InternalIndexOperations; import org.springframework.data.aerospike.server.version.ServerVersionSupport; @@ -45,12 +47,13 @@ public AerospikeTemplate aerospikeTemplate(IAerospikeClient aerospikeClient, AerospikeMappingContext aerospikeMappingContext, AerospikeExceptionTranslator aerospikeExceptionTranslator, QueryEngine queryEngine, IndexRefresher indexRefresher, + IndexesCacheHolder indexCacheHolder, ServerVersionSupport serverVersionSupport, - AerospikeSettings settings) + AerospikeSettings settings, DSLParser dslParser) { return new AerospikeTemplate(aerospikeClient, settings.getDataSettings().getNamespace(), mappingAerospikeConverter, aerospikeMappingContext, aerospikeExceptionTranslator, queryEngine, - indexRefresher, serverVersionSupport); + indexRefresher, indexCacheHolder, serverVersionSupport, dslParser); } @Bean(name = "aerospikeQueryEngine") diff --git a/src/main/java/org/springframework/data/aerospike/config/AbstractReactiveAerospikeDataConfiguration.java b/src/main/java/org/springframework/data/aerospike/config/AbstractReactiveAerospikeDataConfiguration.java index e4ae934c5..1afbc9c82 100644 --- a/src/main/java/org/springframework/data/aerospike/config/AbstractReactiveAerospikeDataConfiguration.java +++ b/src/main/java/org/springframework/data/aerospike/config/AbstractReactiveAerospikeDataConfiguration.java @@ -19,6 +19,7 @@ import com.aerospike.client.policy.ClientPolicy; import com.aerospike.client.reactor.AerospikeReactorClient; import com.aerospike.client.reactor.IAerospikeReactorClient; +import com.aerospike.dsl.DSLParser; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.annotation.Bean; @@ -33,6 +34,7 @@ import org.springframework.data.aerospike.query.ReactorQueryEngine; import org.springframework.data.aerospike.query.StatementBuilder; import org.springframework.data.aerospike.query.cache.IndexInfoParser; +import org.springframework.data.aerospike.query.cache.IndexesCacheHolder; import org.springframework.data.aerospike.query.cache.IndexesCacheUpdater; import org.springframework.data.aerospike.query.cache.InternalIndexOperations; import org.springframework.data.aerospike.query.cache.ReactorIndexRefresher; @@ -54,12 +56,13 @@ public ReactiveAerospikeTemplate reactiveAerospikeTemplate(MappingAerospikeConve IAerospikeReactorClient aerospikeReactorClient, ReactorQueryEngine reactorQueryEngine, ReactorIndexRefresher reactorIndexRefresher, + IndexesCacheHolder indexCacheHolder, ServerVersionSupport serverVersionSupport, - AerospikeSettings settings) + AerospikeSettings settings, DSLParser dslParser) { return new ReactiveAerospikeTemplate(aerospikeReactorClient, settings.getDataSettings().getNamespace(), mappingAerospikeConverter, aerospikeMappingContext, aerospikeExceptionTranslator, - reactorQueryEngine, reactorIndexRefresher, serverVersionSupport); + reactorQueryEngine, reactorIndexRefresher, indexCacheHolder, serverVersionSupport, dslParser); } @Bean(name = "reactiveAerospikeQueryEngine") diff --git a/src/main/java/org/springframework/data/aerospike/config/AerospikeDataConfigurationSupport.java b/src/main/java/org/springframework/data/aerospike/config/AerospikeDataConfigurationSupport.java index 54bf505ca..9f35b3f13 100644 --- a/src/main/java/org/springframework/data/aerospike/config/AerospikeDataConfigurationSupport.java +++ b/src/main/java/org/springframework/data/aerospike/config/AerospikeDataConfigurationSupport.java @@ -23,6 +23,8 @@ import com.aerospike.client.async.EventPolicy; import com.aerospike.client.async.NettyEventLoops; import com.aerospike.client.policy.ClientPolicy; +import com.aerospike.dsl.DSLParser; +import com.aerospike.dsl.DSLParserImpl; import io.netty.channel.EventLoopGroup; import io.netty.channel.epoll.Epoll; import io.netty.channel.epoll.EpollEventLoopGroup; @@ -327,4 +329,9 @@ protected AerospikeSettings aerospikeSettings(AerospikeDataSettings dataSettings return new AerospikeSettings(connectionSettings, dataSettings); } + + @Bean + public DSLParser dslParser() { + return new DSLParserImpl(); + } } diff --git a/src/main/java/org/springframework/data/aerospike/core/AerospikeOperations.java b/src/main/java/org/springframework/data/aerospike/core/AerospikeOperations.java index e779ac702..66127744c 100644 --- a/src/main/java/org/springframework/data/aerospike/core/AerospikeOperations.java +++ b/src/main/java/org/springframework/data/aerospike/core/AerospikeOperations.java @@ -185,8 +185,8 @@ public interface AerospikeOperations { * @param document The document to be inserted. Must not be {@literal null}. * @throws OptimisticLockingFailureException if the document has a version attribute with a different value from * that found on server. - * @throws DataAccessException If operation failed (see - * {@link DefaultAerospikeExceptionTranslator} for details). + * @throws DataAccessException If operation failed (see {@link DefaultAerospikeExceptionTranslator} + * for details). */ void insert(T document); @@ -200,8 +200,8 @@ public interface AerospikeOperations { * @param setName Set name to override the set associated with the document. * @throws OptimisticLockingFailureException if the document has a version attribute with a different value from * that found on server. - * @throws DataAccessException If operation failed (see - * {@link DefaultAerospikeExceptionTranslator} for details). + * @throws DataAccessException If operation failed (see {@link DefaultAerospikeExceptionTranslator} + * for details). */ void insert(T document, String setName); diff --git a/src/main/java/org/springframework/data/aerospike/core/AerospikeTemplate.java b/src/main/java/org/springframework/data/aerospike/core/AerospikeTemplate.java index 1c68c5464..477a8f79a 100644 --- a/src/main/java/org/springframework/data/aerospike/core/AerospikeTemplate.java +++ b/src/main/java/org/springframework/data/aerospike/core/AerospikeTemplate.java @@ -30,17 +30,22 @@ import com.aerospike.client.query.ResultSet; import com.aerospike.client.query.Statement; import com.aerospike.client.task.IndexTask; +import com.aerospike.dsl.DSLParser; import lombok.extern.slf4j.Slf4j; import org.springframework.data.aerospike.convert.AerospikeWriteData; import org.springframework.data.aerospike.convert.MappingAerospikeConverter; import org.springframework.data.aerospike.core.model.GroupedEntities; import org.springframework.data.aerospike.core.model.GroupedKeys; import org.springframework.data.aerospike.index.IndexesCacheRefresher; +import org.springframework.data.aerospike.index.IndexesCacheRetriever; import org.springframework.data.aerospike.mapping.AerospikeMappingContext; import org.springframework.data.aerospike.mapping.AerospikePersistentEntity; import org.springframework.data.aerospike.query.KeyRecordIterator; import org.springframework.data.aerospike.query.QueryEngine; import org.springframework.data.aerospike.query.cache.IndexRefresher; +import org.springframework.data.aerospike.query.cache.IndexesCacheHolder; +import org.springframework.data.aerospike.query.model.Index; +import org.springframework.data.aerospike.query.model.IndexKey; import org.springframework.data.aerospike.query.qualifier.Qualifier; import org.springframework.data.aerospike.repository.query.Query; import org.springframework.data.aerospike.server.version.ServerVersionSupport; @@ -52,7 +57,15 @@ import org.springframework.util.Assert; import java.time.Instant; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.regex.Matcher; @@ -83,13 +96,14 @@ */ @Slf4j public class AerospikeTemplate extends BaseAerospikeTemplate implements AerospikeOperations, - IndexesCacheRefresher { + IndexesCacheRefresher, IndexesCacheRetriever { private static final Pattern INDEX_EXISTS_REGEX_PATTERN = Pattern.compile("^FAIL:(-?\\d+).*$"); - private final IAerospikeClient client; private final QueryEngine queryEngine; private final IndexRefresher indexRefresher; + private final IndexesCacheHolder indexCacheHolder; + private final DSLParser dslParser; public AerospikeTemplate(IAerospikeClient client, String namespace, @@ -98,12 +112,16 @@ public AerospikeTemplate(IAerospikeClient client, AerospikeExceptionTranslator exceptionTranslator, QueryEngine queryEngine, IndexRefresher indexRefresher, - ServerVersionSupport serverVersionSupport) { + IndexesCacheHolder indexCacheHolder, + ServerVersionSupport serverVersionSupport, + DSLParser dslParser) { super(namespace, converter, mappingContext, exceptionTranslator, client.copyWritePolicyDefault(), serverVersionSupport); this.client = client; this.queryEngine = queryEngine; this.indexRefresher = indexRefresher; + this.indexCacheHolder = indexCacheHolder; + this.dslParser = dslParser; } @Override @@ -121,6 +139,15 @@ public Integer refreshIndexesCache() { return indexRefresher.refreshIndexes(); } + @Override + public Map getIndexesCache() { + return indexCacheHolder.getAllIndexes(); + } + + public DSLParser getDSLParser() { + return dslParser; + } + @Override public void save(T document) { Assert.notNull(document, "Document must not be null!"); @@ -1055,11 +1082,18 @@ public IntStream findByIdsUsingQueryWithoutMapping(Collection ids, String set @Override public Stream find(Query query, Class entityClass) { + Assert.notNull(entityClass, "Entity class name must not be null!"); + if (query.getCriteriaObject().hasDSLString()) { + query.getCriteriaObject().parseDSLString(query.getCriteriaObject().getDSLString(), getDSLParser(), + namespace, query.getCriteriaObject().getDSLIndexes()); + } return find(query, entityClass, getSetName(entityClass)); } @Override public Stream find(Query query, Class entityClass, Class targetClass) { + Assert.notNull(entityClass, "Entity class name must not be null!"); + return find(query, targetClass, getSetName(entityClass)); } @@ -1102,11 +1136,15 @@ public Stream findAll(Class targetClass, String setName) { @Override public Stream findAll(Sort sort, long offset, long limit, Class entityClass) { + Assert.notNull(entityClass, "Entity class name must not be null!"); + return findAll(sort, offset, limit, entityClass, getSetName(entityClass)); } @Override public Stream findAll(Sort sort, long offset, long limit, Class entityClass, Class targetClass) { + Assert.notNull(entityClass, "Entity class name must not be null!"); + return findAll(sort, offset, limit, targetClass, getSetName(entityClass)); } diff --git a/src/main/java/org/springframework/data/aerospike/core/ReactiveAerospikeTemplate.java b/src/main/java/org/springframework/data/aerospike/core/ReactiveAerospikeTemplate.java index 575cdedb8..064a788d8 100644 --- a/src/main/java/org/springframework/data/aerospike/core/ReactiveAerospikeTemplate.java +++ b/src/main/java/org/springframework/data/aerospike/core/ReactiveAerospikeTemplate.java @@ -34,17 +34,22 @@ import com.aerospike.client.query.IndexType; import com.aerospike.client.query.KeyRecord; import com.aerospike.client.reactor.IAerospikeReactorClient; +import com.aerospike.dsl.DSLParser; import lombok.extern.slf4j.Slf4j; import org.springframework.data.aerospike.convert.AerospikeWriteData; import org.springframework.data.aerospike.convert.MappingAerospikeConverter; import org.springframework.data.aerospike.core.model.GroupedEntities; import org.springframework.data.aerospike.core.model.GroupedKeys; import org.springframework.data.aerospike.index.IndexesCacheRefresher; +import org.springframework.data.aerospike.index.IndexesCacheRetriever; import org.springframework.data.aerospike.mapping.AerospikeMappingContext; import org.springframework.data.aerospike.mapping.AerospikePersistentEntity; import org.springframework.data.aerospike.mapping.AerospikePersistentProperty; import org.springframework.data.aerospike.query.ReactorQueryEngine; +import org.springframework.data.aerospike.query.cache.IndexesCacheHolder; import org.springframework.data.aerospike.query.cache.ReactorIndexRefresher; +import org.springframework.data.aerospike.query.model.Index; +import org.springframework.data.aerospike.query.model.IndexKey; import org.springframework.data.aerospike.query.qualifier.Qualifier; import org.springframework.data.aerospike.repository.query.Query; import org.springframework.data.aerospike.server.version.ServerVersionSupport; @@ -95,13 +100,15 @@ */ @Slf4j public class ReactiveAerospikeTemplate extends BaseAerospikeTemplate implements ReactiveAerospikeOperations, - IndexesCacheRefresher> { + IndexesCacheRefresher>, IndexesCacheRetriever { private static final Pattern INDEX_EXISTS_REGEX_PATTERN = Pattern.compile("^FAIL:(-?\\d+).*$"); private final IAerospikeReactorClient reactorClient; private final ReactorQueryEngine reactorQueryEngine; private final ReactorIndexRefresher reactorIndexRefresher; + private final IndexesCacheHolder indexCacheHolder; + private final DSLParser dslParser; public ReactiveAerospikeTemplate(IAerospikeReactorClient reactorClient, String namespace, @@ -109,13 +116,21 @@ public ReactiveAerospikeTemplate(IAerospikeReactorClient reactorClient, AerospikeMappingContext mappingContext, AerospikeExceptionTranslator exceptionTranslator, ReactorQueryEngine queryEngine, ReactorIndexRefresher reactorIndexRefresher, - ServerVersionSupport serverVersionSupport) { + IndexesCacheHolder indexCacheHolder, + ServerVersionSupport serverVersionSupport, + DSLParser dslParser) { super(namespace, converter, mappingContext, exceptionTranslator, reactorClient.getAerospikeClient().copyWritePolicyDefault(), serverVersionSupport); Assert.notNull(reactorClient, "Aerospike reactor client must not be null!"); this.reactorClient = reactorClient; this.reactorQueryEngine = queryEngine; this.reactorIndexRefresher = reactorIndexRefresher; + this.indexCacheHolder = indexCacheHolder; + this.dslParser = dslParser; + } + + public DSLParser getDSLParser() { + return dslParser; } @Override @@ -123,6 +138,11 @@ public Mono refreshIndexesCache() { return reactorIndexRefresher.refreshIndexes(); } + @Override + public Map getIndexesCache() { + return indexCacheHolder.getAllIndexes(); + } + @Override public Mono save(T document) { Assert.notNull(document, "Document for saving must not be null!"); diff --git a/src/main/java/org/springframework/data/aerospike/index/IndexesCacheRetriever.java b/src/main/java/org/springframework/data/aerospike/index/IndexesCacheRetriever.java new file mode 100644 index 000000000..8db4f77f9 --- /dev/null +++ b/src/main/java/org/springframework/data/aerospike/index/IndexesCacheRetriever.java @@ -0,0 +1,11 @@ +package org.springframework.data.aerospike.index; + +import org.springframework.data.aerospike.query.model.Index; +import org.springframework.data.aerospike.query.model.IndexKey; + +import java.util.Map; + +public interface IndexesCacheRetriever { + + Map getIndexesCache(); +} diff --git a/src/main/java/org/springframework/data/aerospike/query/QueryEngine.java b/src/main/java/org/springframework/data/aerospike/query/QueryEngine.java index 45fac812a..e589ef4a7 100644 --- a/src/main/java/org/springframework/data/aerospike/query/QueryEngine.java +++ b/src/main/java/org/springframework/data/aerospike/query/QueryEngine.java @@ -175,7 +175,9 @@ private Record getRecord(Policy policy, Key key, String[] binNames) { private QueryPolicy getQueryPolicy(Qualifier qualifier, boolean includeBins) { QueryPolicy queryPolicy = new QueryPolicy(client.getQueryPolicyDefault()); - queryPolicy.filterExp = filterExpressionsBuilder.build(qualifier); + queryPolicy.filterExp = qualifier != null && qualifier.hasFilterExpression() + ? qualifier.getFilterExpression() + : filterExpressionsBuilder.build(qualifier); queryPolicy.includeBinData = includeBins; return queryPolicy; } diff --git a/src/main/java/org/springframework/data/aerospike/query/ReactorQueryEngine.java b/src/main/java/org/springframework/data/aerospike/query/ReactorQueryEngine.java index af22179e8..bdac29b86 100644 --- a/src/main/java/org/springframework/data/aerospike/query/ReactorQueryEngine.java +++ b/src/main/java/org/springframework/data/aerospike/query/ReactorQueryEngine.java @@ -156,7 +156,9 @@ public Flux selectForCount(String namespace, String set, @Nullable Qu private QueryPolicy getQueryPolicy(Qualifier qualifier, boolean includeBins) { QueryPolicy queryPolicy = new QueryPolicy(client.getQueryPolicyDefault()); - queryPolicy.filterExp = filterExpressionsBuilder.build(qualifier); + queryPolicy.filterExp = qualifier != null && qualifier.hasFilterExpression() + ? qualifier.getFilterExpression() + : filterExpressionsBuilder.build(qualifier); queryPolicy.includeBinData = includeBins; return queryPolicy; } diff --git a/src/main/java/org/springframework/data/aerospike/query/StatementBuilder.java b/src/main/java/org/springframework/data/aerospike/query/StatementBuilder.java index b221f9d45..115918da8 100644 --- a/src/main/java/org/springframework/data/aerospike/query/StatementBuilder.java +++ b/src/main/java/org/springframework/data/aerospike/query/StatementBuilder.java @@ -58,11 +58,18 @@ public Statement build(String namespace, String set, @Nullable Query query, Stri stmt.setBinNames(binNames); } if (queryCriteriaIsNotNull(query)) { - // logging query - logQualifierDetails(query.getCriteriaObject(), log); - // statement's filter is set based either on cardinality (the lowest bin values ratio) - // or on order (the first processed filter) - setStatementFilterFromQualifiers(stmt, query.getCriteriaObject()); + if (!query.getCriteriaObject().hasFilterExpression()) { + // logging query + logQualifierDetails(query.getCriteriaObject(), log); + // statement's filter is set based either on cardinality (the lowest bin values ratio) + // or on order (the first processed filter) + setStatementFilterFromQualifiers(stmt, query.getCriteriaObject()); + } else { + // logging query + logQualifierDetails(query.getCriteriaObject(), log); + // statement's filter is set based on parsed DSL expression + stmt.setFilter(query.getCriteriaObject().getFilter()); + } } return stmt; } diff --git a/src/main/java/org/springframework/data/aerospike/query/cache/IndexesCache.java b/src/main/java/org/springframework/data/aerospike/query/cache/IndexesCache.java index 257506ac0..3497b645d 100644 --- a/src/main/java/org/springframework/data/aerospike/query/cache/IndexesCache.java +++ b/src/main/java/org/springframework/data/aerospike/query/cache/IndexesCache.java @@ -20,6 +20,7 @@ import org.springframework.data.aerospike.query.model.IndexedField; import java.util.List; +import java.util.Map; import java.util.Optional; public interface IndexesCache { @@ -47,4 +48,11 @@ public interface IndexesCache { * @return True if there is an index for the given indexed field. */ boolean hasIndexFor(IndexedField indexedField); + + /** + * Get all indexes. + * + * @return Map of indexes by index keys. + */ + Map getAllIndexes(); } diff --git a/src/main/java/org/springframework/data/aerospike/query/cache/IndexesCacheHolder.java b/src/main/java/org/springframework/data/aerospike/query/cache/IndexesCacheHolder.java index 244f58fb4..3e4a7f8c5 100644 --- a/src/main/java/org/springframework/data/aerospike/query/cache/IndexesCacheHolder.java +++ b/src/main/java/org/springframework/data/aerospike/query/cache/IndexesCacheHolder.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -61,4 +62,9 @@ public boolean hasIndexFor(IndexedField indexedField) { public void update(IndexesInfo cache) { this.cache = cache; } + + @Override + public Map getAllIndexes() { + return cache.indexes; + } } diff --git a/src/main/java/org/springframework/data/aerospike/query/qualifier/DSLStringQualifierBuilder.java b/src/main/java/org/springframework/data/aerospike/query/qualifier/DSLStringQualifierBuilder.java new file mode 100644 index 000000000..4e4bf823f --- /dev/null +++ b/src/main/java/org/springframework/data/aerospike/query/qualifier/DSLStringQualifierBuilder.java @@ -0,0 +1,43 @@ +package org.springframework.data.aerospike.query.qualifier; + +import com.aerospike.dsl.Index; +import org.springframework.data.aerospike.annotation.Beta; + +import java.util.Collection; + +import static org.springframework.data.aerospike.query.qualifier.QualifierKey.DSL_INDEXES; +import static org.springframework.data.aerospike.query.qualifier.QualifierKey.DSL_STRING; + +@Beta +public class DSLStringQualifierBuilder extends BaseQualifierBuilder { + + DSLStringQualifierBuilder() { + } + + /** + * Set DSL String. Mandatory parameter. + */ + public DSLStringQualifierBuilder setDSLString(String dslString) { + this.map.put(DSL_STRING, dslString); + return this; + } + + /** + * Set indexes for combined queries to choose from based on cardinality. Optional parameter. + */ + public DSLStringQualifierBuilder setIndexes(Collection indexes) { + this.map.put(DSL_INDEXES, indexes); + return this; + } + + public String getDSLString() { + return (String) map.get(DSL_STRING); + } + + @Override + protected void validate() { + if (this.getDSLString() == null) { + throw new IllegalArgumentException("Expecting DSL String to be provided"); + } + } +} diff --git a/src/main/java/org/springframework/data/aerospike/query/qualifier/FilterQualifierBuilder.java b/src/main/java/org/springframework/data/aerospike/query/qualifier/FilterQualifierBuilder.java new file mode 100644 index 000000000..27a02c38c --- /dev/null +++ b/src/main/java/org/springframework/data/aerospike/query/qualifier/FilterQualifierBuilder.java @@ -0,0 +1,39 @@ +package org.springframework.data.aerospike.query.qualifier; + +import com.aerospike.client.exp.Expression; +import com.aerospike.client.query.Filter; +import org.springframework.data.aerospike.annotation.Beta; + +import static org.springframework.data.aerospike.query.qualifier.QualifierKey.FILTER_EXPRESSION; +import static org.springframework.data.aerospike.query.qualifier.QualifierKey.SINDEX_FILTER; + +@Beta +public class FilterQualifierBuilder extends BaseQualifierBuilder { + + FilterQualifierBuilder() { + } + + /** + * Set filter expression. Mandatory parameter. + */ + public FilterQualifierBuilder setExpression(Expression filterExpression) { + this.map.put(FILTER_EXPRESSION, filterExpression); + return this; + } + + public Expression getExpression() { + return (Expression) map.get(FILTER_EXPRESSION); + } + + /** + * Set secondary index filter. Mandatory parameter. + */ + public FilterQualifierBuilder setFilter(Filter filter) { + this.map.put(SINDEX_FILTER, filter); + return this; + } + + public Filter getFilter() { + return (Filter) map.get(SINDEX_FILTER); + } +} diff --git a/src/main/java/org/springframework/data/aerospike/query/qualifier/MetadataQualifierBuilder.java b/src/main/java/org/springframework/data/aerospike/query/qualifier/MetadataQualifierBuilder.java index d40c9d33b..f6a59e940 100644 --- a/src/main/java/org/springframework/data/aerospike/query/qualifier/MetadataQualifierBuilder.java +++ b/src/main/java/org/springframework/data/aerospike/query/qualifier/MetadataQualifierBuilder.java @@ -70,5 +70,4 @@ private void validateValues() { "metadataField"); } } - } diff --git a/src/main/java/org/springframework/data/aerospike/query/qualifier/Qualifier.java b/src/main/java/org/springframework/data/aerospike/query/qualifier/Qualifier.java index 7eac569f0..5999e8ad3 100644 --- a/src/main/java/org/springframework/data/aerospike/query/qualifier/Qualifier.java +++ b/src/main/java/org/springframework/data/aerospike/query/qualifier/Qualifier.java @@ -20,7 +20,12 @@ import com.aerospike.client.cdt.CTX; import com.aerospike.client.command.ParticleType; import com.aerospike.client.exp.Exp; +import com.aerospike.client.exp.Expression; import com.aerospike.client.query.Filter; +import com.aerospike.dsl.DSLParser; +import com.aerospike.dsl.Index; +import com.aerospike.dsl.IndexFilterInput; +import com.aerospike.dsl.ParsedExpression; import org.springframework.data.aerospike.annotation.Beta; import org.springframework.data.aerospike.config.AerospikeDataSettings; import org.springframework.data.aerospike.query.FilterOperation; @@ -87,6 +92,16 @@ public static MetadataQualifierBuilder metadataBuilder() { return new MetadataQualifierBuilder(); } + @Beta + public static FilterQualifierBuilder filterBuilder() { + return new FilterQualifierBuilder(); + } + + @Beta + public static DSLStringQualifierBuilder dslStringBuilder() { + return new DSLStringQualifierBuilder(); + } + public FilterOperation getOperation() { return (FilterOperation) internalMap.get(FILTER_OPERATION); } @@ -115,6 +130,13 @@ public void setHasSecIndexFilter(Boolean queryAsFilter) { internalMap.put(HAS_SINDEX_FILTER, queryAsFilter); } + public void parseDSLString(String dslString, DSLParser dslParser, String namespace, Collection indexes) { + ParsedExpression parsedExpr = dslParser.parseExpression(dslString, IndexFilterInput.of(namespace, indexes)); + Exp exp = parsedExpr.getResultPair().getExp(); + internalMap.put(FILTER_EXPRESSION, exp == null ? null : Exp.build(exp)); + internalMap.put(SINDEX_FILTER, parsedExpr.getResultPair().getFilter()); + } + public Boolean hasSecIndexFilter() { return internalMap.containsKey(HAS_SINDEX_FILTER) && (Boolean) internalMap.get(HAS_SINDEX_FILTER); } @@ -175,10 +197,35 @@ public Filter getSecondaryIndexFilter() { return FilterOperation.valueOf(getOperation().toString()).sIndexFilter(internalMap); } + public boolean hasFilterExpression() { + return internalMap.get(FILTER_EXPRESSION) != null; + } + + public boolean hasDSLString() { + return internalMap.get(DSL_STRING) != null; + } + + public Expression getFilterExpression() { + return (Expression) internalMap.get(FILTER_EXPRESSION); + } + public Exp getFilterExp() { return FilterOperation.valueOf(getOperation().toString()).filterExp(internalMap); } + public Filter getFilter() { + return (Filter) internalMap.get(SINDEX_FILTER); + } + + public String getDSLString() { + return (String) internalMap.get(DSL_STRING); + } + + @SuppressWarnings("unchecked") + public Collection getDSLIndexes() { + return (Collection) internalMap.get(DSL_INDEXES); + } + protected String luaFieldString(String field) { return String.format("rec['%s']", field); } diff --git a/src/main/java/org/springframework/data/aerospike/query/qualifier/QualifierKey.java b/src/main/java/org/springframework/data/aerospike/query/qualifier/QualifierKey.java index c510b5c28..a1fe65c5b 100644 --- a/src/main/java/org/springframework/data/aerospike/query/qualifier/QualifierKey.java +++ b/src/main/java/org/springframework/data/aerospike/query/qualifier/QualifierKey.java @@ -15,6 +15,10 @@ public enum QualifierKey { DOT_PATH, CTX_ARRAY, IGNORE_CASE, + FILTER_EXPRESSION, + SINDEX_FILTER, + DSL_STRING, + DSL_INDEXES, KEY, NESTED_KEY, VALUE, diff --git a/src/main/java/org/springframework/data/aerospike/repository/query/AerospikePartTreeQuery.java b/src/main/java/org/springframework/data/aerospike/repository/query/AerospikePartTreeQuery.java index 116e0eb80..19d07bff6 100644 --- a/src/main/java/org/springframework/data/aerospike/repository/query/AerospikePartTreeQuery.java +++ b/src/main/java/org/springframework/data/aerospike/repository/query/AerospikePartTreeQuery.java @@ -18,6 +18,8 @@ import org.springframework.data.aerospike.core.AerospikeOperations; import org.springframework.data.aerospike.core.AerospikeTemplate; import org.springframework.data.aerospike.mapping.AerospikeMappingContext; +import org.springframework.data.aerospike.query.model.Index; +import org.springframework.data.aerospike.query.model.IndexKey; import org.springframework.data.aerospike.query.qualifier.Qualifier; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; @@ -28,6 +30,7 @@ import org.springframework.data.repository.query.parser.AbstractQueryCreator; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -39,30 +42,41 @@ * @author Peter Milne * @author Jean Mercier */ -public class AerospikePartTreeQuery extends BaseAerospikePartTreeQuery { +public class AerospikePartTreeQuery extends BaseAerospikePartTreeQuery> { private final AerospikeOperations operations; + private final AerospikeQueryMethod queryMethod; + private final String namespace; + private final Map indexCache; - public AerospikePartTreeQuery(QueryMethod queryMethod, + public AerospikePartTreeQuery(QueryMethod baseQueryMethod, QueryMethodValueEvaluationContextAccessor evalContextAccessor, AerospikeTemplate operations, Class> queryCreator) { - super(queryMethod, evalContextAccessor, queryCreator, (AerospikeMappingContext) operations.getMappingContext(), - operations.getAerospikeConverter(), operations.getServerVersionSupport()); + super(baseQueryMethod, evalContextAccessor, queryCreator, (AerospikeMappingContext) operations.getMappingContext(), + operations.getAerospikeConverter(), operations.getServerVersionSupport(), operations.getDSLParser()); this.operations = operations; + this.namespace = operations.getNamespace(); + this.indexCache = operations.getIndexesCache(); + // each queryMethod here is AerospikeQueryMethod + this.queryMethod = (AerospikeQueryMethod) baseQueryMethod; } @Override @SuppressWarnings({"NullableProblems"}) public Object execute(Object[] parameters) { ParametersParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters); - Query query = prepareQuery(parameters, accessor); Class targetClass = getTargetClass(accessor); + if (queryMethod.hasQueryAnnotation()) { + return findByQueryAnnotation(queryMethod, targetClass, namespace, indexCache, parameters); + } + Query query = prepareQuery(parameters, accessor); + // queries with id equality have their own processing flow if (parameters != null && parameters.length > 0) { Qualifier criteria = query.getCriteriaObject(); - // only for id EQ, id LIKE queries get SimpleProperty query creator + // only for id EQ, id LIKE queries have SimpleProperty query creator if (criteria.hasSingleId()) { return runQueryWithIdsEquality(targetClass, getIdValue(criteria), null); } else { @@ -150,7 +164,8 @@ private Object processPageQuery(Stream unprocessedResultsStream, Pageable pag return new PageImpl<>(resultsPage, pageable, numberOfAllResults); } - private Stream findByQuery(Query query, Class targetClass) { + @Override + protected Stream findByQuery(Query query, Class targetClass) { // Run query and map to different target class. if (targetClass != null && targetClass != entityClass) { return operations.find(query, entityClass, targetClass); diff --git a/src/main/java/org/springframework/data/aerospike/repository/query/AerospikeQueryMethod.java b/src/main/java/org/springframework/data/aerospike/repository/query/AerospikeQueryMethod.java new file mode 100644 index 000000000..2c2063f04 --- /dev/null +++ b/src/main/java/org/springframework/data/aerospike/repository/query/AerospikeQueryMethod.java @@ -0,0 +1,69 @@ +package org.springframework.data.aerospike.repository.query; + +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.data.aerospike.annotation.Query; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.lang.Nullable; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.StringUtils; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.Optional; + +public class AerospikeQueryMethod extends QueryMethod { + + private final Map, Optional> annotationCache; + private final Method method; + + /** + * Creates a new {@link QueryMethod} from the given parameters. Looks up the correct query to use for following + * invocations of the method given. + * + * @param method must not be {@literal null}. + * @param metadata must not be {@literal null}. + * @param factory must not be {@literal null}. + */ + public AerospikeQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) { + super(method, metadata, factory); + this.method = method; + this.annotationCache = new ConcurrentReferenceHashMap<>(); + } + + @SuppressWarnings("unchecked") + private Optional lookupInAnnotationCache(Class annotationType) { + return (Optional) this.annotationCache.computeIfAbsent(annotationType, + it -> Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, it))); + } + + /** + * Returns whether the method has an annotated query. + * + */ + public boolean hasQueryAnnotation() { + return findQueryAnnotation().isPresent(); + } + + /** + * Returns the DSL expression string declared in the {@link org.springframework.data.aerospike.annotation.Query} + * annotation or {@literal null}. + * + */ + @Nullable + String getQueryAnnotation() { + return findQueryAnnotation().orElse(null); + } + + private Optional findQueryAnnotation() { + return lookupQueryAnnotation() // + .map(Query::expression) // + .filter(StringUtils::hasText); + } + + Optional lookupQueryAnnotation() { + return lookupInAnnotationCache(Query.class); + } +} diff --git a/src/main/java/org/springframework/data/aerospike/repository/query/BaseAerospikePartTreeQuery.java b/src/main/java/org/springframework/data/aerospike/repository/query/BaseAerospikePartTreeQuery.java index 845e17c46..0001a0366 100644 --- a/src/main/java/org/springframework/data/aerospike/repository/query/BaseAerospikePartTreeQuery.java +++ b/src/main/java/org/springframework/data/aerospike/repository/query/BaseAerospikePartTreeQuery.java @@ -15,10 +15,18 @@ */ package org.springframework.data.aerospike.repository.query; +import com.aerospike.client.exp.Exp; +import com.aerospike.client.query.Filter; +import com.aerospike.dsl.DSLParser; +import com.aerospike.dsl.IndexFilterInput; +import com.aerospike.dsl.ParsedExpression; +import com.aerospike.dsl.exception.AerospikeDSLException; import org.springframework.beans.BeanUtils; import org.springframework.beans.support.PropertyComparator; import org.springframework.data.aerospike.convert.MappingAerospikeConverter; import org.springframework.data.aerospike.mapping.AerospikeMappingContext; +import org.springframework.data.aerospike.query.model.Index; +import org.springframework.data.aerospike.query.model.IndexKey; import org.springframework.data.aerospike.query.qualifier.Qualifier; import org.springframework.data.aerospike.server.version.ServerVersionSupport; import org.springframework.data.domain.Sort; @@ -36,8 +44,14 @@ import org.springframework.util.ClassUtils; import java.lang.reflect.Constructor; +import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; /** @@ -45,37 +59,41 @@ * @author Jean Mercier * @author Igor Ermolenko */ -public abstract class BaseAerospikePartTreeQuery implements RepositoryQuery { +public abstract class BaseAerospikePartTreeQuery implements RepositoryQuery { - protected final QueryMethod queryMethod; + protected final QueryMethod baseQueryMethod; protected final Class entityClass; private final QueryMethodValueEvaluationContextAccessor evaluationContextAccessor; private final Class> queryCreator; private final AerospikeMappingContext context; private final MappingAerospikeConverter converter; private final ServerVersionSupport versionSupport; + private final DSLParser dslParser; + private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("(?> queryCreator, AerospikeMappingContext context, - MappingAerospikeConverter converter, ServerVersionSupport versionSupport) { - this.queryMethod = queryMethod; + MappingAerospikeConverter converter, ServerVersionSupport versionSupport, + DSLParser dslParser) { + this.baseQueryMethod = queryMethod; this.evaluationContextAccessor = evalContextAccessor; this.queryCreator = queryCreator; this.entityClass = queryMethod.getEntityInformation().getJavaType(); this.context = context; this.converter = converter; this.versionSupport = versionSupport; + this.dslParser = dslParser; } @Override public QueryMethod getQueryMethod() { - return queryMethod; + return baseQueryMethod; } protected Query prepareQuery(Object[] parameters, ParametersParameterAccessor accessor) { - PartTree tree = new PartTree(queryMethod.getName(), entityClass); + PartTree tree = new PartTree(baseQueryMethod.getName(), entityClass); Query baseQuery = createQuery(accessor, tree); Qualifier criteria = baseQuery.getCriteriaObject(); @@ -103,7 +121,8 @@ protected Query prepareQuery(Object[] parameters, ParametersParameterAccessor ac if (query.getCriteria() instanceof SpelExpression spelExpression) { // Create a ValueEvaluationContextProvider using the accessor - ValueEvaluationContextProvider provider = this.evaluationContextAccessor.create(queryMethod.getParameters()); + ValueEvaluationContextProvider provider = + this.evaluationContextAccessor.create(baseQueryMethod.getParameters()); // Get the ValueEvaluationContext using the provider ValueEvaluationContext valueContext = provider.getEvaluationContext(parameters); @@ -124,11 +143,11 @@ Class getTargetClass(ParametersParameterAccessor accessor) { return accessor.findDynamicProjection(); } // DTO projection - if (!isEntityAssignableFromReturnType(queryMethod)) { - return queryMethod.getReturnedObjectType(); + if (!isEntityAssignableFromReturnType(baseQueryMethod)) { + return baseQueryMethod.getReturnedObjectType(); } // No projection - target class will be the entity class. - return queryMethod.getEntityInformation().getJavaType(); + return baseQueryMethod.getEntityInformation().getJavaType(); } public Query createQuery(ParametersParameterAccessor accessor, PartTree tree) { @@ -182,8 +201,8 @@ protected boolean isDeleteQuery(QueryMethod queryMethod) { } /** - * Find whether entity domain class is assignable from query method's returned object class. - * Not assignable when using a detached DTO (data transfer object, e.g., for projections). + * Find whether entity domain class is assignable from query method's returned object class. Not assignable when + * using a detached DTO (data transfer object, e.g., for projections). * * @param queryMethod QueryMethod in use * @return true when entity is assignable from query method's return class, otherwise false @@ -191,4 +210,81 @@ protected boolean isDeleteQuery(QueryMethod queryMethod) { protected boolean isEntityAssignableFromReturnType(QueryMethod queryMethod) { return queryMethod.getEntityInformation().getJavaType().isAssignableFrom(queryMethod.getReturnedObjectType()); } + + protected abstract T findByQuery(Query query, Class targetClass); + + protected List convertToStringsList(Object[] parameters) { + if (parameters == null) { + return new ArrayList<>(); + } + + return IntStream.range(0, parameters.length) + .mapToObj(idx -> { + Object obj = parameters[idx]; + if (obj == null) { + throw new IllegalArgumentException("Element at index " + idx + " is null"); + } else if (obj instanceof String str) { + // Replace any existing single quotes with escaped quotes + str = str.replace("'", "\\'"); + return "'" + str + "'"; + } else if (obj instanceof Number || obj instanceof Boolean) { + return obj.toString(); + } else { + throw new IllegalArgumentException(String.format( + "Element at index %s is expected to be a String, number or boolean, instead got %s", + idx, obj.getClass().getName()) + ); + } + }).collect(Collectors.toList()); + } + + protected String fillPlaceholders(String input, Object[] parameters) { + if (parameters == null || parameters.length == 0) return input; + + List parametersList = convertToStringsList(parameters); + // Use regex to find and replace placeholders + Matcher matcher = PLACEHOLDER_PATTERN.matcher(input); + StringBuffer result = new StringBuffer(); + + while (matcher.find()) { + int index = Integer.parseInt(matcher.group(1)); + if (index >= 0 && index < parametersList.size()) { + // Escape any special characters in the replacement + String replacement = Matcher.quoteReplacement(parametersList.get(index)); + matcher.appendReplacement(result, replacement); + } else { + throw new AerospikeDSLException("Parameter index out of bounds: " + index); + } + } + matcher.appendTail(result); + return result.toString(); + } + + protected T findByQueryAnnotation(AerospikeQueryMethod queryMethod, Class targetClass, String namespace, + Map indexCache, Object[] parameters) { + // Map cached indexes to DSL + List indexes = indexCache.values().stream().map(value -> + com.aerospike.dsl.Index.builder() + .namespace(value.getNamespace()) + .bin(value.getBin()) + .indexType(value.getIndexType()) + .binValuesRatio(value.getBinValuesRatio()) + .build()) + .toList(); + // Parse the given expression + ParsedExpression expr = dslParser.parseExpression( + fillPlaceholders(queryMethod.getQueryAnnotation(), parameters), + IndexFilterInput.of(namespace, indexes) + ); + // Create the query + Filter filter = expr.getResultPair().getFilter(); + Exp exp = expr.getResultPair().getExp(); + Query query = new Query( + Qualifier.filterBuilder() + .setFilter(filter) + .setExpression(exp == null ? null : Exp.build(exp)) + .build() + ); + return findByQuery(query, targetClass); + } } diff --git a/src/main/java/org/springframework/data/aerospike/repository/query/ReactiveAerospikePartTreeQuery.java b/src/main/java/org/springframework/data/aerospike/repository/query/ReactiveAerospikePartTreeQuery.java index d66466afe..514fe5f33 100644 --- a/src/main/java/org/springframework/data/aerospike/repository/query/ReactiveAerospikePartTreeQuery.java +++ b/src/main/java/org/springframework/data/aerospike/repository/query/ReactiveAerospikePartTreeQuery.java @@ -18,6 +18,8 @@ import org.springframework.data.aerospike.core.ReactiveAerospikeOperations; import org.springframework.data.aerospike.core.ReactiveAerospikeTemplate; import org.springframework.data.aerospike.mapping.AerospikeMappingContext; +import org.springframework.data.aerospike.query.model.Index; +import org.springframework.data.aerospike.query.model.IndexKey; import org.springframework.data.aerospike.query.qualifier.Qualifier; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; @@ -31,6 +33,7 @@ import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Stream; @@ -40,26 +43,37 @@ /** * @author Igor Ermolenko */ -public class ReactiveAerospikePartTreeQuery extends BaseAerospikePartTreeQuery { +public class ReactiveAerospikePartTreeQuery extends BaseAerospikePartTreeQuery> { private final ReactiveAerospikeOperations operations; + private final AerospikeQueryMethod queryMethod; + private final String namespace; + private final Map indexCache; - public ReactiveAerospikePartTreeQuery(QueryMethod queryMethod, + public ReactiveAerospikePartTreeQuery(QueryMethod baseQueryMethod, QueryMethodValueEvaluationContextAccessor evalContextAccessor, ReactiveAerospikeTemplate operations, Class> queryCreator) { - super(queryMethod, evalContextAccessor, queryCreator, (AerospikeMappingContext) operations.getMappingContext(), - operations.getAerospikeConverter(), operations.getServerVersionSupport()); + super(baseQueryMethod, evalContextAccessor, queryCreator, (AerospikeMappingContext) operations.getMappingContext(), + operations.getAerospikeConverter(), operations.getServerVersionSupport(), operations.getDSLParser()); this.operations = operations; + this.namespace = operations.getNamespace(); + this.indexCache = operations.getIndexesCache(); + // each queryMethod here is AerospikeQueryMethod + this.queryMethod = (AerospikeQueryMethod) baseQueryMethod; } @Override @SuppressWarnings({"NullableProblems"}) public Object execute(Object[] parameters) { - ParametersParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters); - Query query = prepareQuery(parameters, accessor); + ParametersParameterAccessor accessor = new ParametersParameterAccessor(baseQueryMethod.getParameters(), parameters); Class targetClass = getTargetClass(accessor); + if (queryMethod.hasQueryAnnotation()) { + return findByQueryAnnotation(queryMethod, targetClass, namespace, indexCache, parameters); + } + Query query = prepareQuery(parameters, accessor); + // queries with id equality have their own processing flow if (parameters != null && parameters.length > 0) { Qualifier criteria = query.getCriteriaObject(); @@ -75,14 +89,14 @@ public Object execute(Object[] parameters) { } } - if (isExistsQuery(queryMethod)) { - return operations.exists(query, queryMethod.getEntityInformation().getJavaType()); - } else if (isCountQuery(queryMethod)) { - return operations.count(query, queryMethod.getEntityInformation().getJavaType()); - } else if (isDeleteQuery(queryMethod)) { - operations.delete(query, queryMethod.getEntityInformation().getJavaType()); + if (isExistsQuery(baseQueryMethod)) { + return operations.exists(query, baseQueryMethod.getEntityInformation().getJavaType()); + } else if (isCountQuery(baseQueryMethod)) { + return operations.count(query, baseQueryMethod.getEntityInformation().getJavaType()); + } else if (isDeleteQuery(baseQueryMethod)) { + operations.delete(query, baseQueryMethod.getEntityInformation().getJavaType()); return Optional.empty(); - } else if (queryMethod.isPageQuery() || queryMethod.isSliceQuery()) { + } else if (baseQueryMethod.isPageQuery() || baseQueryMethod.isSliceQuery()) { Pageable pageable = accessor.getPageable(); Flux unprocessedResults = operations.findUsingQueryWithoutPostProcessing(entityClass, targetClass, query); @@ -101,27 +115,27 @@ public Object execute(Object[] parameters) { } return getPage(unprocessedResults, size, pageable, query); }); - } else if (queryMethod.isStreamQuery()) { + } else if (baseQueryMethod.isStreamQuery()) { return findByQuery(query, targetClass).toStream(); - } else if (queryMethod.isCollectionQuery()) { + } else if (baseQueryMethod.isCollectionQuery()) { // Currently there seems to be no way to distinguish return type Collection from Mono etc., // so a query method with return type Collection will compile but throw ClassCastException in runtime return findByQuery(query, targetClass).collectList(); } - else if (queryMethod.isQueryForEntity() || !isEntityAssignableFromReturnType(queryMethod)) { + else if (baseQueryMethod.isQueryForEntity() || !isEntityAssignableFromReturnType(baseQueryMethod)) { // Queries with Flux and Mono return types including projection queries return findByQuery(query, targetClass); } - throw new UnsupportedOperationException("Query method " + queryMethod.getNamedQueryName() + " is not " + + throw new UnsupportedOperationException("Query method " + baseQueryMethod.getNamedQueryName() + " is not " + "supported"); } protected Object runQueryWithIdsEquality(Class targetClass, List ids, Query query) { - if (isExistsQuery(queryMethod)) { + if (isExistsQuery(baseQueryMethod)) { return operations.existsByIdsUsingQuery(ids, entityClass, query); - } else if (isCountQuery(queryMethod)) { + } else if (isCountQuery(baseQueryMethod)) { return operations.countByIdsUsingQuery(ids, entityClass, query); - } else if (isDeleteQuery(queryMethod)) { + } else if (isDeleteQuery(baseQueryMethod)) { return operations.deleteByIdsUsingQuery(ids, entityClass, query); } else { return operations.findByIdsUsingQuery(ids, entityClass, targetClass, query); @@ -129,7 +143,7 @@ protected Object runQueryWithIdsEquality(Class targetClass, List ids, } public Object getPage(List unprocessedResults, long overallSize, Pageable pageable, Query query) { - if (queryMethod.isSliceQuery()) { + if (baseQueryMethod.isSliceQuery()) { return processSliceQuery(unprocessedResults, overallSize, pageable, query); } else { return processPageQuery(unprocessedResults, overallSize, pageable, query); @@ -137,7 +151,7 @@ public Object getPage(List unprocessedResults, long overallSize, Pageable pag } public Object getPage(Flux unprocessedResults, long overallSize, Pageable pageable, Query query) { - if (queryMethod.isSliceQuery()) { + if (baseQueryMethod.isSliceQuery()) { List resultsPaginated = applyPostProcessing(unprocessedResults, query).toList(); boolean hasNext = overallSize > pageable.getPageSize() * (pageable.getOffset() + 1); return new SliceImpl<>(resultsPaginated, pageable, hasNext); @@ -178,7 +192,8 @@ protected Stream applyPostProcessing(Flux results, Query query) { return results.toStream(); } - private Flux findByQuery(Query query, Class targetClass) { + @Override + protected Flux findByQuery(Query query, Class targetClass) { // Run query and map to different target class. if (targetClass != entityClass) { return operations.find(query, entityClass, targetClass); diff --git a/src/main/java/org/springframework/data/aerospike/repository/support/AerospikeRepositoryFactory.java b/src/main/java/org/springframework/data/aerospike/repository/support/AerospikeRepositoryFactory.java index 777bbb023..c41da1732 100644 --- a/src/main/java/org/springframework/data/aerospike/repository/support/AerospikeRepositoryFactory.java +++ b/src/main/java/org/springframework/data/aerospike/repository/support/AerospikeRepositoryFactory.java @@ -15,6 +15,7 @@ */ package org.springframework.data.aerospike.repository.support; +import org.springframework.data.aerospike.repository.query.AerospikeQueryMethod; import org.springframework.data.aerospike.core.AerospikeTemplate; import org.springframework.data.aerospike.mapping.AerospikePersistentEntity; import org.springframework.data.aerospike.mapping.AerospikePersistentProperty; @@ -31,7 +32,6 @@ import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; -import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ValueExpressionDelegate; @@ -143,7 +143,7 @@ public AerospikeQueryLookupStrategy(QueryMethodValueEvaluationContextAccessor ev public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory projectionFactory, NamedQueries namedQueries) { - QueryMethod queryMethod = new QueryMethod(method, metadata, projectionFactory); + AerospikeQueryMethod queryMethod = new AerospikeQueryMethod(method, metadata, projectionFactory); return new AerospikePartTreeQuery(queryMethod, evaluationContextAccessor, this.aerospikeTemplate, this.queryCreator); } diff --git a/src/main/java/org/springframework/data/aerospike/repository/support/ReactiveAerospikeRepositoryFactory.java b/src/main/java/org/springframework/data/aerospike/repository/support/ReactiveAerospikeRepositoryFactory.java index 92d6090c8..8e6b99008 100644 --- a/src/main/java/org/springframework/data/aerospike/repository/support/ReactiveAerospikeRepositoryFactory.java +++ b/src/main/java/org/springframework/data/aerospike/repository/support/ReactiveAerospikeRepositoryFactory.java @@ -19,6 +19,7 @@ import org.springframework.data.aerospike.mapping.AerospikePersistentEntity; import org.springframework.data.aerospike.mapping.AerospikePersistentProperty; import org.springframework.data.aerospike.repository.query.AerospikeQueryCreator; +import org.springframework.data.aerospike.repository.query.AerospikeQueryMethod; import org.springframework.data.aerospike.repository.query.ReactiveAerospikePartTreeQuery; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; @@ -31,7 +32,6 @@ import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; -import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ValueExpressionDelegate; @@ -144,7 +144,7 @@ public ReactiveAerospikeQueryLookupStrategy(@Nullable Key key, public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory projectionFactory, NamedQueries namedQueries) { - QueryMethod queryMethod = new QueryMethod(method, metadata, projectionFactory); + AerospikeQueryMethod queryMethod = new AerospikeQueryMethod(method, metadata, projectionFactory); return new ReactiveAerospikePartTreeQuery(queryMethod, evaluationContextAccessor, this.aerospikeTemplate, this.queryCreator); } diff --git a/src/main/java/org/springframework/data/aerospike/util/Utils.java b/src/main/java/org/springframework/data/aerospike/util/Utils.java index 85213bcc6..7069f0efc 100644 --- a/src/main/java/org/springframework/data/aerospike/util/Utils.java +++ b/src/main/java/org/springframework/data/aerospike/util/Utils.java @@ -28,6 +28,7 @@ import org.slf4j.Logger; import org.springframework.core.env.Environment; import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.data.aerospike.query.FilterOperation; import org.springframework.data.aerospike.query.qualifier.Qualifier; import org.springframework.data.aerospike.repository.query.CriteriaDefinition; import org.springframework.util.StringUtils; @@ -176,8 +177,8 @@ public static void logQualifierDetails(CriteriaDefinition criteria, Logger logge Arrays.stream(qualifiers).forEach(innerQualifier -> logQualifierDetails(innerQualifier, logger)); } - String operation = qualifier.getOperation().toString(); - operation = (hasLength(operation) ? operation : "N/A"); + FilterOperation operation = qualifier.getOperation(); + String strOperation = (operation != null && hasLength(operation.toString()) ? operation.toString() : "N/A"); String values = ""; String value = valueToString(qualifier.getValue()); @@ -202,7 +203,7 @@ public static void logQualifierDetails(CriteriaDefinition criteria, Logger logge ? String.format(", qualifiers = %s,", qualifiersHashesToString(qualifier.getQualifiers())) : ""; - logger.debug("Created qualifier #{}:{} operation = {}{}{}", qualifier.hashCode(), path, operation, values, + logger.debug("Created qualifier #{}:{} operation = {}{}{}", qualifier.hashCode(), path, strOperation, values, qualifiersStr); } diff --git a/src/test/java/org/springframework/data/aerospike/query/sync/QualifierTests.java b/src/test/java/org/springframework/data/aerospike/query/sync/QualifierTests.java index c948daec8..e75647369 100644 --- a/src/test/java/org/springframework/data/aerospike/query/sync/QualifierTests.java +++ b/src/test/java/org/springframework/data/aerospike/query/sync/QualifierTests.java @@ -279,12 +279,12 @@ void stringEndsWithQualifier() { @Test void betweenQualifier() { - // Ages range from 25 -> 29. Get back age between 26 and 28 inclusive + // Ages range from 25 -> 29. Get back age between 26 (inclusive) and 28 (inclusive) Qualifier qualifier = Qualifier.builder() .setPath("age") .setFilterOperation(FilterOperation.BETWEEN) .setValue(26) - .setSecondValue(29) // + 1 as upper limit is exclusive + .setSecondValue(29) // + 1 as upper limit for BETWEEN is exclusive .build(); KeyRecordIterator it = queryEngine.select(namespace, SET_NAME, null, new Query(qualifier)); diff --git a/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/CustomQueriesTests.java b/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/CustomQueriesTests.java index 1eb9d1361..908b06119 100644 --- a/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/CustomQueriesTests.java +++ b/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/CustomQueriesTests.java @@ -1,5 +1,9 @@ package org.springframework.data.aerospike.repository.query.blocking.find; +import com.aerospike.client.exp.Exp; +import com.aerospike.client.query.Filter; +import com.aerospike.client.query.IndexType; +import com.aerospike.dsl.Index; import org.junit.jupiter.api.Test; import org.springframework.data.aerospike.query.FilterOperation; import org.springframework.data.aerospike.query.qualifier.Qualifier; @@ -9,6 +13,8 @@ import org.springframework.data.domain.Sort; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -303,5 +309,103 @@ void mapValuesTest() { .build(); assertThat(repository.findUsingQuery(new Query(intMapWithExactKeyAndValueLt100))).containsOnly(carter); } + + @Test + void findByFilterExpression() { + carter.setActive(true); + repository.save(carter); + + Qualifier isActiveEqTrue = Qualifier.filterBuilder() + .setExpression( + Exp.build( + Exp.eq(Exp.boolBin("isActive"), Exp.val(true)) + ) + ).build(); + assertThat(repository.findUsingQuery(new Query(isActiveEqTrue))).contains(carter); + + carter.setActive(false); + repository.save(carter); + } + + @Test + void findByDSLString_Exp_only() { + carter.setActive(true); + repository.save(carter); + + Qualifier isActiveEqTrue = Qualifier.dslStringBuilder() + // currently there is no corresponding secondary index Filter for this DSL String, so filter Exp is used + .setDSLString("$.isActive.get(type: BOOL) == true") + .build(); + assertThat(repository.findUsingQuery(new Query(isActiveEqTrue))).contains(carter); + + carter.setActive(false); + repository.save(carter); + } + + @Test + void findByDSLString_indexed_Filter() { + template.createIndex(Person.class, "firstNameIndex", "firstName", IndexType.STRING); + + List indexes = List.of( + Index.builder().namespace("TEST").bin("firstName").indexType(IndexType.STRING).binValuesRatio(0).build() + ); + + Qualifier firstNameEqCarter = Qualifier.dslStringBuilder() + // this DSL String has a corresponding secondary index Filter, so it will be used + .setDSLString("$.firstName == 'Carter'") + .setIndexes(indexes) + .build(); + assertThat(repository.findUsingQuery(new Query(firstNameEqCarter))).contains(carter); + + Qualifier firstNameEqcarter = Qualifier.dslStringBuilder() + // this DSL String has a corresponding secondary index Filter, so it will be used + // Person's name "Carter" starts with a capital letter, so no match with lower case first letter + .setDSLString("$.firstName == 'carter'") + .build(); + assertThat(repository.findUsingQuery(new Query(firstNameEqcarter))).isEmpty(); + } + + @Test + void findByDSLString_indexed_combined_AND() { + template.createIndex(Person.class, "firstNameIndex", "firstName", IndexType.STRING); + template.createIndex(Person.class, "lastNameIndex", "lastName", IndexType.STRING); + + // Map indexes cached by Spring Data Aerospike to DSL indexes + List indexes = indexesCache.getAllIndexes().values().stream().map(value -> + com.aerospike.dsl.Index.builder() + .namespace(value.getNamespace()) + .bin(value.getBin()) + .indexType(value.getIndexType()) + .binValuesRatio(value.getBinValuesRatio()) + .build()) + .toList(); + + Qualifier firstNameAndLastName = Qualifier.dslStringBuilder() + // This DSL String has a corresponding secondary index Filter, so it will be used + // The index to build the Filter will be chosen from the collection of indexes based by binValuesRatio (cardinality) + // If cardinality is the same, the indexed bin to build Filter will be chosen alphabetically + .setDSLString("$.firstName == 'Stefan' and $.lastName == 'Lessard'") + .setIndexes(indexes) + .build(); + assertThat(firstNameAndLastName.getDSLIndexes().stream() + .map(Index::getBinValuesRatio) + .collect(Collectors.toSet())).isEqualTo(Set.of(1)); // the same cardinality between two indexes + assertThat(repository.findUsingQuery(new Query(firstNameAndLastName))).contains(stefan); + + // This way indexes for DSLParser can be built manually + List indexesBuildManually = List.of( + Index.builder().namespace("TEST").bin("firstName").indexType(IndexType.STRING).binValuesRatio(0).build(), + // Cardinality is different, and it is larger for the "lastName" bin index + Index.builder().namespace("TEST").bin("lastName").indexType(IndexType.STRING).binValuesRatio(1).build() + ); + Qualifier firstNameAndLastName2 = Qualifier.dslStringBuilder() + // this DSL String has a corresponding secondary index Filter, so it will be used + // the index to build the Filter will be chosen from the collection of indexes based by binValuesRatio (cardinality) + .setDSLString("$.firstName == 'Stefan' and $.lastName == 'Lessard'") + .setIndexes(indexes) + .build(); + // This time "lastName" bin is chosen based on cardinality (binValuesRatio of its index == 1) + assertThat(repository.findUsingQuery(new Query(firstNameAndLastName2))).contains(stefan); + } } diff --git a/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/EqualsTests.java b/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/EqualsTests.java index f95b92051..03274f4d3 100644 --- a/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/EqualsTests.java +++ b/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/EqualsTests.java @@ -1,6 +1,6 @@ package org.springframework.data.aerospike.repository.query.blocking.find; -import com.aerospike.client.Value; +import com.aerospike.client.query.IndexType; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.data.aerospike.BaseIntegrationTests; @@ -72,44 +72,8 @@ void findBySimplePropertyEquals_Enum() { assertThat(result).containsOnly(alicia); } - @Test - void findBySimplePropertyEquals_BooleanInt() { - boolean initialValue = Value.UseBoolBin; - Value.UseBoolBin = false; // save boolean as int - Person intBoolBinPerson = Person.builder() - .id(BaseIntegrationTests.nextId()) - .isActive(true) - .firstName("Test") - .build(); - repository.save(intBoolBinPerson); - - List persons = repository.findByIsActive(true); - assertThat(persons).contains(intBoolBinPerson); - - Value.UseBoolBin = initialValue; // set back to the default value - repository.delete(intBoolBinPerson); - } - - @Test - void findBySimplePropertyEquals_BooleanInt_NegativeTest() { - boolean initialValue = Value.UseBoolBin; - Value.UseBoolBin = false; // save boolean as int - - assertThatThrownBy(() -> negativeTestsRepository.findByIsActive()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Person.isActive EQ: invalid number of arguments, expecting one"); - - assertThatThrownBy(() -> negativeTestsRepository.findByIsActive(true, false)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Person.isActive EQ: invalid number of arguments, expecting one"); - - Value.UseBoolBin = initialValue; // set back to the default value - } - @Test void findBySimplePropertyEquals_Boolean() { - boolean initialValue = Value.UseBoolBin; - Value.UseBoolBin = true; // save boolean as bool, available in Server 5.6+ Person intBoolBinPerson = Person.builder().id(BaseIntegrationTests.nextId()).isActive(true).firstName("Test") .build(); repository.save(intBoolBinPerson); @@ -117,15 +81,11 @@ void findBySimplePropertyEquals_Boolean() { List persons = repository.findByIsActive(true); assertThat(persons).contains(intBoolBinPerson); - Value.UseBoolBin = initialValue; // set back to the default value repository.delete(intBoolBinPerson); } @Test void findBySimplePropertyEquals_Boolean_NegativeTest() { - boolean initialValue = Value.UseBoolBin; - Value.UseBoolBin = true; // save boolean as bool, available in Server 5.6+ - assertThatThrownBy(() -> negativeTestsRepository.findByIsActive()) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Person.isActive EQ: invalid number of arguments, expecting one"); @@ -133,8 +93,6 @@ void findBySimplePropertyEquals_Boolean_NegativeTest() { assertThatThrownBy(() -> negativeTestsRepository.findByIsActive(true, false)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Person.isActive EQ: invalid number of arguments, expecting one"); - - Value.UseBoolBin = initialValue; // set back to the default value } @Test @@ -216,11 +174,17 @@ void findBySimpleProperty_AND_id_dynamicProjection() { @Test void findBySimpleProperty_AND_simpleProperty_DynamicProjection() { + additionalAerospikeTestOperations.createIndex(Person.class, "firstNameIdx", "firstName", + IndexType.STRING); + additionalAerospikeTestOperations.createIndex(Person.class, "lastNameIdx", "lastName", + IndexType.STRING); QueryParam firstName = of(carter.getFirstName()); QueryParam lastName = of(carter.getLastName()); - List result = repository.findByFirstNameAndLastName(firstName, lastName, - PersonSomeFields.class); - assertThat(result).containsOnly(carter.toPersonSomeFields()); +// List result = repository.findByFirstNameAndLastName(firstName, lastName, +// PersonSomeFields.class); + List result = repository.findByFirstNameAndLastName(firstName, lastName); +// assertThat(result).containsOnly(carter.toPersonSomeFields()); + assertThat(result).containsOnly(carter); } @Test @@ -424,6 +388,13 @@ void findByCollectionEquals() { } } + @Test + void findByCollectionEquals_QueryAnnotation_NegativeTest() { + assertThatThrownBy(() -> negativeTestsRepository.findByStringsEquals(List.of("string1", "string2"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Element at index 0 is expected to be a String, number or boolean"); + } + @Test void findByCollectionEquals_NegativeTest() { assertThatThrownBy(() -> negativeTestsRepository.findByStrings()) diff --git a/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/FalseTests.java b/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/FalseTests.java index 7958657f4..51a4ba044 100644 --- a/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/FalseTests.java +++ b/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/FalseTests.java @@ -1,6 +1,5 @@ package org.springframework.data.aerospike.repository.query.blocking.find; -import com.aerospike.client.Value; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.data.aerospike.repository.query.blocking.PersonRepositoryQueryTests; @@ -11,31 +10,13 @@ */ public class FalseTests extends PersonRepositoryQueryTests { - @Test - void findByBooleanIntSimplePropertyIsFalse() { - boolean initialValue = Value.UseBoolBin; - Value.UseBoolBin = false; // save boolean as int - Person intBoolBinPerson = Person.builder().id(nextId()).isActive(true).firstName("Test") - .build(); - repository.save(intBoolBinPerson); - - Assertions.assertThat(repository.findByIsActiveFalse()).doesNotContain(intBoolBinPerson); - - Value.UseBoolBin = initialValue; // set back to the default value - repository.delete(intBoolBinPerson); - } - @Test void findByBooleanSimplePropertyIsFalse() { - boolean initialValue = Value.UseBoolBin; - Value.UseBoolBin = true; // save boolean as bool, available in Server 5.6+ Person intBoolBinPerson = Person.builder().id(nextId()).isActive(true).firstName("Test") .build(); repository.save(intBoolBinPerson); Assertions.assertThat(repository.findByIsActiveFalse()).doesNotContain(intBoolBinPerson); - - Value.UseBoolBin = initialValue; // set back to the default value repository.delete(intBoolBinPerson); } } diff --git a/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/TrueTests.java b/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/TrueTests.java index 2f54969a1..58869a3ed 100644 --- a/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/TrueTests.java +++ b/src/test/java/org/springframework/data/aerospike/repository/query/blocking/find/TrueTests.java @@ -1,6 +1,5 @@ package org.springframework.data.aerospike.repository.query.blocking.find; -import com.aerospike.client.Value; import org.junit.jupiter.api.Test; import org.springframework.data.aerospike.repository.query.blocking.PersonRepositoryQueryTests; import org.springframework.data.aerospike.sample.Person; @@ -14,28 +13,8 @@ */ public class TrueTests extends PersonRepositoryQueryTests { - @Test - void findByBooleanIntSimplePropertyIsTrue() { - boolean initialValue = Value.UseBoolBin; - Value.UseBoolBin = false; // save boolean as int - Person intBoolBinPerson = Person.builder().id(nextId()).isActive(true).firstName("Test") - .build(); - repository.save(intBoolBinPerson); - - List persons1 = repository.findByIsActiveTrue(); - assertThat(persons1).contains(intBoolBinPerson); - - List persons2 = repository.findByIsActiveIsTrue(); // another way to call the query method - assertThat(persons2).containsExactlyElementsOf(persons1); - - Value.UseBoolBin = initialValue; // set back to the default value - repository.delete(intBoolBinPerson); - } - @Test void findByBooleanSimplePropertyIsTrue() { - boolean initialValue = Value.UseBoolBin; - Value.UseBoolBin = true; // save boolean as bool, available in Server 5.6+ Person intBoolBinPerson = Person.builder().id(nextId()).isActive(true).firstName("Test") .build(); repository.save(intBoolBinPerson); @@ -45,8 +24,6 @@ void findByBooleanSimplePropertyIsTrue() { List persons2 = repository.findByIsActiveIsTrue(); // another way to call the query method assertThat(persons2).containsExactlyElementsOf(persons1); - - Value.UseBoolBin = initialValue; // set back to the default value repository.delete(intBoolBinPerson); } } diff --git a/src/test/java/org/springframework/data/aerospike/repository/query/reactive/find/EqualsTests.java b/src/test/java/org/springframework/data/aerospike/repository/query/reactive/find/EqualsTests.java index 307131a97..bf53b2fb3 100644 --- a/src/test/java/org/springframework/data/aerospike/repository/query/reactive/find/EqualsTests.java +++ b/src/test/java/org/springframework/data/aerospike/repository/query/reactive/find/EqualsTests.java @@ -84,6 +84,14 @@ public void findBySimpleProperty() { assertThat(results).containsOnly(homer, marge, bart, lisa, maggie); } + @Test + public void findBySimpleProperty_usingQueryAnnotation() { + List results = reactiveRepository.findByFirstName("Homer") + .collectList().block(); + + assertThat(results).containsOnly(homer); + } + @Test public void findBySimpleProperty_Projection() { List results = reactiveRepository.findCustomerSomeFieldsByLastName("Simpson") diff --git a/src/test/java/org/springframework/data/aerospike/sample/PersonNegativeTestsRepository.java b/src/test/java/org/springframework/data/aerospike/sample/PersonNegativeTestsRepository.java index 9e1046ab1..dc2436aa8 100644 --- a/src/test/java/org/springframework/data/aerospike/sample/PersonNegativeTestsRepository.java +++ b/src/test/java/org/springframework/data/aerospike/sample/PersonNegativeTestsRepository.java @@ -15,6 +15,7 @@ */ package org.springframework.data.aerospike.sample; +import org.springframework.data.aerospike.annotation.Query; import org.springframework.data.aerospike.query.QueryParam; import org.springframework.data.aerospike.repository.AerospikeRepository; import org.springframework.data.aerospike.repository.query.CriteriaDefinition; @@ -199,6 +200,11 @@ List

findByIntMapContaining(CriteriaDefinition.AerospikeQueryCriterion criter */ List

findByStringsEquals(String string1, String string2); + /** + * Invalid number of arguments: expecting one + */ + @Query(expression = "$.strings == ?0") + List

findByStringsEquals(Collection strings); /** * Invalid number of arguments: expecting one */ diff --git a/src/test/java/org/springframework/data/aerospike/sample/PersonRepository.java b/src/test/java/org/springframework/data/aerospike/sample/PersonRepository.java index 45e786716..d3f9eb8ed 100644 --- a/src/test/java/org/springframework/data/aerospike/sample/PersonRepository.java +++ b/src/test/java/org/springframework/data/aerospike/sample/PersonRepository.java @@ -16,6 +16,7 @@ package org.springframework.data.aerospike.sample; import jakarta.validation.constraints.NotNull; +import org.springframework.data.aerospike.annotation.Query; import org.springframework.data.aerospike.query.QueryParam; import org.springframework.data.aerospike.repository.AerospikeRepository; import org.springframework.data.aerospike.repository.query.CriteriaDefinition.AerospikeNullQueryCriterion; @@ -67,7 +68,8 @@ public interface PersonRepository

extends AerospikeRepository< List findByLastNameAndId(QueryParam lastName, QueryParam id, Class type); // Dynamic Projection - List findByFirstNameAndLastName(QueryParam firstName, QueryParam lastName, Class type); +// List findByFirstNameAndLastName(QueryParam firstName, QueryParam lastName, Class type); + List findByFirstNameAndLastName(QueryParam firstName, QueryParam lastName); /** * Find all entities that satisfy the condition "have primary key in the given list and first name equal to the @@ -377,6 +379,7 @@ List

findByAgeOrLastNameLikeAndFirstNameLike(QueryParam age, QueryParam lastN List

findByAgeIn(ArrayList ages); + @Query(expression = "$.isActive.get(type: BOOL) == ?0") List

findByIsActive(boolean isActive); List

findByIsActiveTrue(); @@ -415,6 +418,7 @@ List

findByAgeOrLastNameLikeAndFirstNameLike(QueryParam age, QueryParam lastN * * @param age integer to compare with */ + @Query(expression = "$.age > ?0") List

findByAgeGreaterThan(int age); /** @@ -1594,6 +1598,7 @@ List

findByFriendStringMapNotContaining(AerospikeQueryCriterion criterion, Page

findTop3ByLastNameStartingWith(String lastName, Pageable pageRequest); + @Query(expression = "$.firstName == ?0") List

findByFirstName(String name); boolean existsByFirstName(String name); diff --git a/src/test/java/org/springframework/data/aerospike/sample/ReactiveCustomerRepository.java b/src/test/java/org/springframework/data/aerospike/sample/ReactiveCustomerRepository.java index 1f28ab1e9..4b3ebcd96 100644 --- a/src/test/java/org/springframework/data/aerospike/sample/ReactiveCustomerRepository.java +++ b/src/test/java/org/springframework/data/aerospike/sample/ReactiveCustomerRepository.java @@ -15,6 +15,7 @@ */ package org.springframework.data.aerospike.sample; +import org.springframework.data.aerospike.annotation.Query; import org.springframework.data.aerospike.query.QueryParam; import org.springframework.data.aerospike.repository.ReactiveAerospikeRepository; import org.springframework.data.domain.Sort; @@ -34,6 +35,9 @@ public interface ReactiveCustomerRepository extends ReactiveAerospikeRepository< Flux findByLastName(String lastName); + @Query(expression = "$.firstName == ?0") + Flux findByFirstName(String firstName); + // DTO Projection Flux findCustomerSomeFieldsByLastName(String lastName); diff --git a/src/test/java/org/springframework/data/aerospike/sample/ReactiveIndexedPersonRepository.java b/src/test/java/org/springframework/data/aerospike/sample/ReactiveIndexedPersonRepository.java index 9ff84485a..25cca6e4a 100644 --- a/src/test/java/org/springframework/data/aerospike/sample/ReactiveIndexedPersonRepository.java +++ b/src/test/java/org/springframework/data/aerospike/sample/ReactiveIndexedPersonRepository.java @@ -86,7 +86,7 @@ public interface ReactiveIndexedPersonRepository extends ReactiveAerospikeReposi * field) * * @param from lower limit, inclusive - * @param to upper limit, inclusive + * @param to upper limit, exclusive */ Flux findByFriendAgeBetween(int from, int to);