diff --git a/README.md b/README.md index f584512ae..0c6e21512 100644 --- a/README.md +++ b/README.md @@ -22,14 +22,16 @@ The Spring Data Aerospike project aims to provide a familiar and consistent Spri 3. [Basic error handling in spring-data-aerospike](https://medium.com/aerospike-developer-blog/basic-error-handling-in-spring-data-aerospike-5edd580d77d9?source=friends_link&sk=cff71ea1539b36e5a89b2c3411b58a06) 4. [How to create secondary index in Spring Data Aerospike](https://medium.com/aerospike-developer-blog/how-to-create-secondary-index-in-spring-data-aerospike-e19d7e343d7c?source=friends_link&sk=413619a568f9aac51ed2f2611ee70aba) -## Spring Boot compatibility +## Spring Data Aerospike compatibility -|`spring-data-aerospike` Version | Spring Boot Version -| :----------- | :----: | -|2.4.2.RELEASE | 2.3.x -|2.3.5.RELEASE | 2.2.x -|2.1.1.RELEASE | 2.1.x, 2.0.x -|1.2.1.RELEASE | 1.5.x +|`spring-data-aerospike` Version | Spring Boot Version | Aerospike Client | Aerospike Reactor Client +| :----------- | :----: | :----------- | :----------- +|3.0.0 | 2.5.X | 5.1.x | 5.0.x +|2.5.0 | 2.5.X | 4.4.x | 4.4.x +|2.4.2.RELEASE | 2.3.x | 4.4.x | 4.4.x +|2.3.5.RELEASE | 2.2.x | 4.4.x | 4.4.x +|2.1.1.RELEASE | 2.1.x, 2.0.x | 4.4.x | 3.2.x +|1.2.1.RELEASE | 1.5.x | 4.1.x | ## Quick Start @@ -41,7 +43,7 @@ Add the Maven dependency: com.aerospike spring-data-aerospike - 2.4.2.RELEASE + 3.0.0 ``` diff --git a/pom.xml b/pom.xml index 8714e0add..6ba5a468a 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.aerospike spring-data-aerospike - 2.4.2.RELEASE + 2.5.0 Spring Data Aerospike Aerospike Inc. @@ -16,7 +16,7 @@ org.springframework.data.build spring-data-parent - 2.4.6 + 2.5.1 @@ -24,17 +24,17 @@ 4.4.18 4.4.10 - 2.4.6 - 2.4.6 + 2.5.1 + 2.5.1 DATAAERO UTF-8 - 2.4.4 + 2.4.5 3.0.2 - 2.0.3 - 4.0.3 - 1.0.4.RELEASE - 1.18.18 + 2.0.8 + 4.1.0 + 1.0.6.RELEASE + 1.18.20 @@ -311,7 +311,7 @@ org.apache.maven.plugins maven-gpg-plugin - 1.6 + 3.0.1 sign-artifacts diff --git a/src/main/java/org/springframework/data/aerospike/config/ReadPolicyFactoryBean.java b/src/main/java/org/springframework/data/aerospike/config/ReadPolicyFactoryBean.java index ef587de10..3836ed1d9 100644 --- a/src/main/java/org/springframework/data/aerospike/config/ReadPolicyFactoryBean.java +++ b/src/main/java/org/springframework/data/aerospike/config/ReadPolicyFactoryBean.java @@ -18,7 +18,6 @@ import org.springframework.beans.factory.FactoryBean; import com.aerospike.client.policy.Policy; -import com.aerospike.client.policy.Priority; /** * A {@link FactoryBean} implementation that exposes the setters necessary to configure a read policy via XML. @@ -73,15 +72,6 @@ public void setSleepBetweenRetries(int sleepBetweenRetries){ this.policy.sleepBetweenRetries = sleepBetweenRetries; } - /** - * Configures the priority of request relative to other transactions. - * Currently, only used for scans. - * @param priority The priority configuration value. - */ - public void setPriority(Priority priority){ - this.policy.priority = priority; - } - /* * (non-Javadoc) * @see org.springframework.beans.factory.FactoryBean#getObject() diff --git a/src/main/java/org/springframework/data/aerospike/config/ScanPolicyFactoryBean.java b/src/main/java/org/springframework/data/aerospike/config/ScanPolicyFactoryBean.java index 033645701..97a8621c7 100644 --- a/src/main/java/org/springframework/data/aerospike/config/ScanPolicyFactoryBean.java +++ b/src/main/java/org/springframework/data/aerospike/config/ScanPolicyFactoryBean.java @@ -43,14 +43,6 @@ public void setConcurrentNodes(boolean concurrentNodes){ this.policy.concurrentNodes = concurrentNodes; } - /** - * Configures termination of scan if cluster in fluctuating state. - * @param failOnClusterChange The failOnClusterChange configuration value. - */ - public void setFailOnClusterChange(boolean failOnClusterChange){ - this.policy.failOnClusterChange = failOnClusterChange; - } - /** * Indicates if bin data is retrieved. If false, only record digests are retrieved. * @param includeBinData The includeBinData configuration value. @@ -72,15 +64,6 @@ public void setIncludeBinData(boolean includeBinData){ public void setMaxConcurrentNodes(int maxConcurrentNodes){ this.policy.maxConcurrentNodes = maxConcurrentNodes; } - - /** - * Configure the percent of data to scan. Valid integer range is 1 to 100. - * Default is 100. - * @param scanPercent The scanPercent configuration value. - */ - public void setScanPercent(int scanPercent){ - this.policy.scanPercent = scanPercent; - } /* * (non-Javadoc) diff --git a/src/main/java/org/springframework/data/aerospike/convert/CustomConversions.java b/src/main/java/org/springframework/data/aerospike/convert/CustomConversions.java deleted file mode 100644 index 3d1a5c594..000000000 --- a/src/main/java/org/springframework/data/aerospike/convert/CustomConversions.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2012-2020 the original author or authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.data.aerospike.convert; - -import org.springframework.data.mapping.model.SimpleTypeHolder; - -import java.util.List; - -/** - * Value object to capture custom conversion. - *

- *

Types that can be mapped directly onto JSON are considered simple ones, because they neither need deeper - * inspection nor nested conversion.

- * - * @author Michael Nitschinger - * @author Oliver Gierke - * @author Mark Paluch - * @deprecated instead use {@link org.springframework.data.aerospike.convert.AerospikeCustomConversions} - */ -@Deprecated -public class CustomConversions extends AerospikeCustomConversions { - - /** - * Instead use the other constructor. - */ - @Deprecated - public CustomConversions(final List converters, SimpleTypeHolder simpleTypeHolder) { - super(converters); - } - - -} 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 9ed2ab60f..fd40422e4 100644 --- a/src/main/java/org/springframework/data/aerospike/core/AerospikeOperations.java +++ b/src/main/java/org/springframework/data/aerospike/core/AerospikeOperations.java @@ -22,6 +22,8 @@ import com.aerospike.client.query.IndexCollectionType; import com.aerospike.client.query.IndexType; import org.springframework.data.aerospike.IndexAlreadyExistsException; +import org.springframework.data.aerospike.core.model.GroupedEntities; +import org.springframework.data.aerospike.core.model.GroupedKeys; import org.springframework.data.aerospike.repository.query.Query; import org.springframework.data.domain.Sort; import org.springframework.data.mapping.context.MappingContext; @@ -109,6 +111,8 @@ public interface AerospikeOperations { List findByIds(Iterable ids, Class entityClass); + GroupedEntities findByIds(GroupedKeys groupedKeys); + T add(T objectToAddTo, Map values); T add(T objectToAddTo, String binName, long value); 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 96156b46b..2a68e9af8 100644 --- a/src/main/java/org/springframework/data/aerospike/core/AerospikeTemplate.java +++ b/src/main/java/org/springframework/data/aerospike/core/AerospikeTemplate.java @@ -15,17 +15,31 @@ */ package org.springframework.data.aerospike.core; +import com.aerospike.client.AerospikeException; +import com.aerospike.client.Bin; +import com.aerospike.client.IAerospikeClient; +import com.aerospike.client.Info; +import com.aerospike.client.Key; +import com.aerospike.client.Operation; import com.aerospike.client.Record; -import com.aerospike.client.*; +import com.aerospike.client.ResultCode; +import com.aerospike.client.Value; import com.aerospike.client.cluster.Node; import com.aerospike.client.policy.RecordExistsAction; import com.aerospike.client.policy.WritePolicy; -import com.aerospike.client.query.*; +import com.aerospike.client.query.Filter; +import com.aerospike.client.query.IndexCollectionType; +import com.aerospike.client.query.IndexType; +import com.aerospike.client.query.KeyRecord; +import com.aerospike.client.query.ResultSet; +import com.aerospike.client.query.Statement; import com.aerospike.client.task.IndexTask; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.InvalidDataAccessResourceUsageException; 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.mapping.AerospikeMappingContext; import org.springframework.data.aerospike.mapping.AerospikePersistentEntity; import org.springframework.data.aerospike.query.KeyRecordIterator; @@ -39,7 +53,14 @@ import org.springframework.data.util.StreamUtils; import org.springframework.util.Assert; -import java.util.*; +import java.util.Arrays; +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.Random; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -334,6 +355,35 @@ private List findByIdsInternal(Collection ids, Class entityClass) { } } + + /** + * Executes a single batch request to get results for several entities. + * + * Aerospike provides functionality to get records from different sets in 1 batch + * request. The methods allows to put grouped keys by entity type as parameter and + * get result as spring data aerospike entities grouped by entity type. + * + * @param groupedKeys will never be {@literal null}. + * @return GroupedEntities grouped entities + */ + @Override + public GroupedEntities findByIds(GroupedKeys groupedKeys) { + Assert.notNull(groupedKeys, "Grouped keys must not be null!"); + + if (groupedKeys.getEntitiesKeys().isEmpty()) { + return GroupedEntities.builder().build(); + } + + return findEntitiesByIdsInternal(groupedKeys); + } + + private GroupedEntities findEntitiesByIdsInternal(GroupedKeys groupedKeys) { + EntitiesKeys entitiesKeys = EntitiesKeys.of(toEntitiesKeyMap(groupedKeys)); + Record[] records = client.get(null, entitiesKeys.getKeys()); + + return toGroupedEntities(entitiesKeys, records); + } + @SuppressWarnings("unchecked") @Override public Iterable aggregate(Filter filter, Class entityClass, diff --git a/src/main/java/org/springframework/data/aerospike/core/BaseAerospikeTemplate.java b/src/main/java/org/springframework/data/aerospike/core/BaseAerospikeTemplate.java index 40c6473ec..e2b092499 100644 --- a/src/main/java/org/springframework/data/aerospike/core/BaseAerospikeTemplate.java +++ b/src/main/java/org/springframework/data/aerospike/core/BaseAerospikeTemplate.java @@ -34,6 +34,8 @@ import org.springframework.data.aerospike.convert.AerospikeTypeAliasAccessor; 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.mapping.AerospikeMappingContext; import org.springframework.data.aerospike.mapping.AerospikePersistentEntity; import org.springframework.data.aerospike.mapping.AerospikePersistentProperty; @@ -41,13 +43,20 @@ import org.springframework.data.aerospike.repository.query.Query; import org.springframework.data.convert.CustomConversions; import org.springframework.data.domain.Sort; +import org.springframework.data.keyvalue.core.IterableConverter; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.util.Assert; +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.stream.Collectors; +import java.util.stream.IntStream; /** * Base class for creation Aerospike templates @@ -232,6 +241,35 @@ Key getKey(Object id, AerospikePersistentEntity entity) { return new Key(this.namespace, entity.getSetName(), userKey); } + GroupedEntities toGroupedEntities(EntitiesKeys entitiesKeys, Record[] records) { + GroupedEntities.GroupedEntitiesBuilder builder = GroupedEntities.builder(); + + IntStream.range(0, entitiesKeys.getKeys().length) + .filter(index -> records[index] != null) + .mapToObj(index -> mapToEntity(entitiesKeys.getKeys()[index], entitiesKeys.getEntityClasses()[index], records[index])) + .filter(Objects::nonNull) + .forEach(entity -> builder.entity(getEntityClass(entity), entity)); + + return builder.build(); + } + + Map, List> toEntitiesKeyMap(GroupedKeys groupedKeys) { + return groupedKeys.getEntitiesKeys().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> toKeysList(entry.getKey(), entry.getValue()))); + } + + private List toKeysList(Class entityClass, Collection ids) { + Assert.notNull(entityClass, "Entity class must not be null!"); + Assert.notNull(ids, "List of ids must not be null!"); + + AerospikePersistentEntity entity = mappingContext.getRequiredPersistentEntity(entityClass); + List idsList = IterableConverter.toList(ids); + + return idsList.stream() + .map(id -> getKey(id, entity)) + .collect(Collectors.toList()); + } + @SuppressWarnings("unchecked") private S convertIfNecessary(Object source, Class type) { return type.isAssignableFrom(source.getClass()) ? (S) source : converter.getConversionService().convert(source, type); diff --git a/src/main/java/org/springframework/data/aerospike/core/EntitiesKeys.java b/src/main/java/org/springframework/data/aerospike/core/EntitiesKeys.java new file mode 100644 index 000000000..cf2206c7b --- /dev/null +++ b/src/main/java/org/springframework/data/aerospike/core/EntitiesKeys.java @@ -0,0 +1,48 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.aerospike.core; + +import com.aerospike.client.Key; +import lombok.Builder; +import lombok.Getter; +import lombok.Value; + +import java.util.List; +import java.util.Map; + +@Value +@Builder +@Getter +class EntitiesKeys { + + Class[] entityClasses; + Key[] keys; + + public static EntitiesKeys of(Map, List> entitiesKeys) { + Class[] entityClasses = entitiesKeys.entrySet().stream() + .flatMap(entry -> entry.getValue().stream().map(item -> entry.getKey())) + .toArray(Class[]::new); + + Key[] keys = entitiesKeys.entrySet().stream() + .flatMap(entry -> entry.getValue().stream()) + .toArray(Key[]::new); + + return EntitiesKeys.builder() + .entityClasses(entityClasses) + .keys(keys) + .build(); + } +} diff --git a/src/main/java/org/springframework/data/aerospike/core/ReactiveAerospikeOperations.java b/src/main/java/org/springframework/data/aerospike/core/ReactiveAerospikeOperations.java index c2cb64f2b..50a57da73 100644 --- a/src/main/java/org/springframework/data/aerospike/core/ReactiveAerospikeOperations.java +++ b/src/main/java/org/springframework/data/aerospike/core/ReactiveAerospikeOperations.java @@ -18,6 +18,8 @@ import com.aerospike.client.query.IndexCollectionType; import com.aerospike.client.query.IndexType; import com.aerospike.client.reactor.IAerospikeReactorClient; +import org.springframework.data.aerospike.core.model.GroupedEntities; +import org.springframework.data.aerospike.core.model.GroupedKeys; import org.springframework.data.aerospike.repository.query.Query; import org.springframework.data.domain.Sort; import org.springframework.data.mapping.context.MappingContext; @@ -61,6 +63,8 @@ public interface ReactiveAerospikeOperations { Flux findByIds(Iterable ids, Class entityClass); + Mono findByIds(GroupedKeys groupedKeys); + Flux find(Query query, Class entityClass); Flux findInRange(long offset, long limit, Sort sort, Class 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 c85f8ce1c..4f5f23e2e 100644 --- a/src/main/java/org/springframework/data/aerospike/core/ReactiveAerospikeTemplate.java +++ b/src/main/java/org/springframework/data/aerospike/core/ReactiveAerospikeTemplate.java @@ -15,8 +15,12 @@ */ package org.springframework.data.aerospike.core; +import com.aerospike.client.AerospikeException; +import com.aerospike.client.Bin; +import com.aerospike.client.Key; +import com.aerospike.client.Operation; import com.aerospike.client.Record; -import com.aerospike.client.*; +import com.aerospike.client.Value; import com.aerospike.client.policy.RecordExistsAction; import com.aerospike.client.policy.WritePolicy; import com.aerospike.client.query.Filter; @@ -28,6 +32,8 @@ 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.mapping.AerospikeMappingContext; import org.springframework.data.aerospike.mapping.AerospikePersistentEntity; import org.springframework.data.aerospike.query.Qualifier; @@ -257,6 +263,35 @@ public Flux findByIds(Iterable ids, Class entityClass) { .map(keyRecord -> mapToEntity(keyRecord.key, entityClass, keyRecord.record)); } + /** + * Executes a single batch request to get results for several entities. + * + * Aerospike provides functionality to get records from different sets in 1 batch + * request. The methods allows to put grouped keys by entity type as parameter and + * get result as spring data aerospike entities grouped by entity type. + * + * @param groupedKeys + * @return Mono + */ + @Override + public Mono findByIds(GroupedKeys groupedKeys) { + Assert.notNull(groupedKeys, "Grouped keys must not be null!"); + + if (groupedKeys.getEntitiesKeys().isEmpty()) { + return Mono.just(GroupedEntities.builder().build()); + } + + return findEntitiesByIdsInternal(groupedKeys); + } + + private Mono findEntitiesByIdsInternal(GroupedKeys groupedKeys) { + EntitiesKeys entitiesKeys = EntitiesKeys.of(toEntitiesKeyMap(groupedKeys)); + + return reactorClient.get(null, entitiesKeys.getKeys()) + .map(item -> toGroupedEntities(entitiesKeys, item.records)) + .onErrorMap(this::translateError); + } + @Override public Flux find(Query query, Class entityClass) { Assert.notNull(query, "Query must not be null!"); diff --git a/src/main/java/org/springframework/data/aerospike/core/model/GroupedEntities.java b/src/main/java/org/springframework/data/aerospike/core/model/GroupedEntities.java new file mode 100644 index 000000000..49de590e0 --- /dev/null +++ b/src/main/java/org/springframework/data/aerospike/core/model/GroupedEntities.java @@ -0,0 +1,66 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.aerospike.core.model; + + +import lombok.Builder; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; + +@Builder +public class GroupedEntities { + + private final Map, List> entitiesResults; + + @SuppressWarnings("unchecked") + public List getEntitiesByClass(Class entityClass) { + return (List) entitiesResults.getOrDefault(entityClass, emptyList()); + } + + public boolean containsEntities() { + return entitiesResults.entrySet().stream().anyMatch(entry -> !entry.getValue().isEmpty()); + } + + public static class GroupedEntitiesBuilder { + + private Map, List> entitiesResults = new HashMap<>(); + + @SuppressWarnings("unchecked") + public GroupedEntities.GroupedEntitiesBuilder entity(Class key, T entity) { + entitiesResults.compute(key, (k,v) -> { + if (v == null) { + return new ArrayList<>(singletonList(entity)); + } + + ((List)v).add(entity); + return v; + }); + + return this; + } + + private GroupedEntities.GroupedEntitiesBuilder entitiesResults(Map, List> keysMap) { + this.entitiesResults = keysMap; + return this; + } + } +} diff --git a/src/main/java/org/springframework/data/aerospike/core/model/GroupedKeys.java b/src/main/java/org/springframework/data/aerospike/core/model/GroupedKeys.java new file mode 100644 index 000000000..b67e3575d --- /dev/null +++ b/src/main/java/org/springframework/data/aerospike/core/model/GroupedKeys.java @@ -0,0 +1,46 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.aerospike.core.model; + +import lombok.Builder; +import lombok.Getter; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +@Getter +@Builder +public class GroupedKeys { + + private final Map, Collection> entitiesKeys; + + public static class GroupedKeysBuilder { + + private Map, Collection> entitiesKeys = new HashMap<>(); + + public GroupedKeys.GroupedKeysBuilder entityKeys(Class key, Collection value) { + entitiesKeys.put(key, value); + + return this; + } + + private GroupedKeys.GroupedKeysBuilder entitiesKeys(Map, Collection> keys) { + this.entitiesKeys = keys; + return this; + } + } +} diff --git a/src/main/java/org/springframework/data/aerospike/repository/support/SimpleAerospikeRepository.java b/src/main/java/org/springframework/data/aerospike/repository/support/SimpleAerospikeRepository.java index 41043df13..296c38ab1 100644 --- a/src/main/java/org/springframework/data/aerospike/repository/support/SimpleAerospikeRepository.java +++ b/src/main/java/org/springframework/data/aerospike/repository/support/SimpleAerospikeRepository.java @@ -71,6 +71,12 @@ public void delete(T entity) { operations.delete(entity); } + @Override + public void deleteAllById(Iterable iterable) { + Assert.notNull(iterable, "The given Iterable must not be null!"); + iterable.forEach(this::deleteById); + } + @Override public Iterable findAll(Sort sort) { return operations.findAll(sort, entityInformation.getJavaType()); diff --git a/src/main/java/org/springframework/data/aerospike/repository/support/SimpleReactiveAerospikeRepository.java b/src/main/java/org/springframework/data/aerospike/repository/support/SimpleReactiveAerospikeRepository.java index 3c67b8104..01659559a 100644 --- a/src/main/java/org/springframework/data/aerospike/repository/support/SimpleReactiveAerospikeRepository.java +++ b/src/main/java/org/springframework/data/aerospike/repository/support/SimpleReactiveAerospikeRepository.java @@ -118,6 +118,14 @@ public Mono delete(T entity) { return operations.delete(entity).then(); } + @Override + public Mono deleteAllById(Iterable iterable) { + Assert.notNull(iterable, "The given Iterable must not be null!"); + iterable.forEach(id -> + Assert.notNull(id, "The given Iterable of entities must not contain null!")); + return Flux.fromIterable(iterable).flatMap(this::deleteById).then(); + } + @Override public Mono deleteAll(Iterable entities) { Assert.notNull(entities, "The given Iterable of entities must not be null!"); diff --git a/src/test/java/org/springframework/data/aerospike/core/AbstractFindByEntitiesTest.java b/src/test/java/org/springframework/data/aerospike/core/AbstractFindByEntitiesTest.java new file mode 100644 index 000000000..8587afd91 --- /dev/null +++ b/src/test/java/org/springframework/data/aerospike/core/AbstractFindByEntitiesTest.java @@ -0,0 +1,206 @@ +package org.springframework.data.aerospike.core; + +import org.junit.jupiter.api.Test; +import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.data.aerospike.core.model.GroupedEntities; +import org.springframework.data.aerospike.core.model.GroupedKeys; +import org.springframework.data.aerospike.sample.Customer; +import org.springframework.data.aerospike.sample.Person; +import org.springframework.data.mapping.MappingException; + +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.springframework.data.aerospike.utility.AerospikeUniqueId.nextId; + +public interface AbstractFindByEntitiesTest { + + @Test + default void shouldFindAllRequestedEntities() { + List persons = generatePersons(5); + List customers = generateCustomers(3); + + GroupedKeys groupedKeys = getGroupedKeys(persons, customers); + GroupedEntities byIds = findByIds(groupedKeys); + + assertThat(byIds.getEntitiesByClass(Person.class)).containsExactlyInAnyOrderElementsOf(persons); + assertThat(byIds.getEntitiesByClass(Customer.class)).containsExactlyInAnyOrderElementsOf(customers); + } + + @Test + default void shouldReturnAnEmptyResultIfKeysWhereSetToWrongEntities() { + List persons = generatePersons(5); + List customers = generateCustomers(3); + + Set requestedPersonsIds = persons.stream() + .map(Person::getId) + .collect(Collectors.toSet()); + Set requestedCustomerIds = customers.stream().map(Customer::getId) + .collect(Collectors.toSet()); + + GroupedKeys groupedKeys = GroupedKeys.builder() + .entityKeys(Person.class, requestedCustomerIds) + .entityKeys(Customer.class, requestedPersonsIds) + .build(); + + GroupedEntities byIds = findByIds(groupedKeys); + + assertThat(byIds.containsEntities()).isFalse(); + } + + @Test + default void shouldFindSomeOfIdsOfRequestedEntities() { + List persons = generatePersons(2); + List customers = generateCustomers(3); + + GroupedKeys requestMapWithRandomExtraIds = getGroupedEntitiesKeysWithRandomExtraIds(persons, customers); + GroupedEntities results = findByIds(requestMapWithRandomExtraIds); + + assertThat(results.getEntitiesByClass(Person.class)).containsExactlyInAnyOrderElementsOf(persons); + assertThat(results.getEntitiesByClass(Customer.class)).containsExactlyInAnyOrderElementsOf(customers); + } + + @Test + default void shouldFindResultsOfOneOfRequestedEntity() { + List persons = generatePersons(3); + + GroupedKeys groupedKeysWithRandomExtraIds = getGroupedEntitiesKeysWithRandomExtraIds(persons, emptyList()); + GroupedEntities results = findByIds(groupedKeysWithRandomExtraIds); + + assertThat(results.getEntitiesByClass(Person.class)).containsExactlyInAnyOrderElementsOf(persons); + assertThat(results.getEntitiesByClass(Customer.class)).containsExactlyInAnyOrderElementsOf(emptyList()); + } + + @Test + default void shouldFindForOneEntityIfAnotherContainsEmptyRequestList() { + List persons = generatePersons(3); + + GroupedKeys groupedKeys = getGroupedKeys(persons, emptyList()); + GroupedEntities batchGroupedEntities = findByIds(groupedKeys); + + assertThat(batchGroupedEntities.getEntitiesByClass(Person.class)).containsExactlyInAnyOrderElementsOf(persons); + assertThat(batchGroupedEntities.getEntitiesByClass(Customer.class)).containsExactlyInAnyOrderElementsOf(emptyList()); + } + + @Test + default void shouldReturnMapWithEmptyResultsOnEmptyRequest() { + GroupedKeys groupedKeys = GroupedKeys.builder() + .entityKeys(Person.class, emptyList()) + .entityKeys(Customer.class, emptyList()) + .build(); + + + GroupedEntities batchGroupedEntities = findByIds(groupedKeys); + + assertThat(batchGroupedEntities.getEntitiesByClass(Person.class)) + .containsExactlyInAnyOrderElementsOf(emptyList()); + assertThat(batchGroupedEntities.getEntitiesByClass(Customer.class)) + .containsExactlyInAnyOrderElementsOf(emptyList()); + } + + @Test + default void shouldReturnMapWithEmptyResultsIfNoEntitiesWhereFound() { + GroupedKeys groupedKeys = GroupedKeys.builder() + .entityKeys(Person.class, singletonList(nextId())) + .entityKeys(Customer.class, singletonList(nextId())) + .build(); + + GroupedEntities batchGroupedEntities = findByIds(groupedKeys); + + assertThat(batchGroupedEntities.getEntitiesByClass(Person.class)) + .containsExactlyInAnyOrderElementsOf(emptyList()); + assertThat(batchGroupedEntities.getEntitiesByClass(Customer.class)) + .containsExactlyInAnyOrderElementsOf(emptyList()); + } + + @Test + default void shouldThrowMappingExceptionOnNonAerospikeEntityClass() { + List persons = generatePersons(2); + Set personIds = persons.stream() + .map(Person::getId) + .collect(Collectors.toSet()); + + GroupedKeys groupedKeys = GroupedKeys.builder() + .entityKeys(Person.class, personIds) + .entityKeys(String.class, singletonList(1L)) + .build(); + + assertThatThrownBy(() -> findByIds(groupedKeys)) + .isInstanceOf(MappingException.class) + .hasMessage("Couldn't find PersistentEntity for type class java.lang.String!"); + } + + @Test + default void shouldReturnAnEmptyResultOnEmptyRequestMap() { + GroupedKeys groupedKeys = GroupedKeys.builder().build(); + GroupedEntities byIds = findByIds(groupedKeys); + assertThat(byIds.getEntitiesByClass(Person.class)).isEmpty(); + } + + @Test + default void shouldThrowConverterNotFoundExceptionOnClassWithoutConverter() { + GroupedKeys groupedKeys = GroupedKeys.builder() + .entityKeys(Person.class, singletonList(Person.builder().id("id").build())) + .build(); + + assertThatThrownBy(() -> findByIds(groupedKeys)) + .isInstanceOf(ConverterNotFoundException.class) + .hasMessageContaining("No converter found capable of converting from type"); + } + + default GroupedKeys getGroupedKeys(Collection persons, Collection customers) { + Set requestedPersonsIds = persons.stream() + .map(Person::getId) + .collect(Collectors.toSet()); + Set requestedCustomerIds = customers.stream().map(Customer::getId) + .collect(Collectors.toSet()); + + return GroupedKeys.builder() + .entityKeys(Person.class, requestedPersonsIds) + .entityKeys(Customer.class, requestedCustomerIds) + .build(); + } + + default GroupedKeys getGroupedEntitiesKeysWithRandomExtraIds(Collection persons, Collection customers) { + Set requestedPersonsIds = Stream.concat(persons.stream().map(Person::getId), Stream.of(nextId(), nextId())) + .collect(Collectors.toSet()); + Set requestedCustomerIds = Stream.concat(customers.stream().map(Customer::getId), Stream.of(nextId(), nextId())) + .collect(Collectors.toSet()); + + return GroupedKeys.builder() + .entityKeys(Person.class, requestedPersonsIds) + .entityKeys(Customer.class, requestedCustomerIds) + .build(); + } + + default List generateCustomers(int count) { + return IntStream.range(0, count) + .mapToObj(i -> Customer.builder().id(nextId()) + .firstname("firstName" + i) + .lastname("Smith") + .build()) + .peek(this::save) + .collect(Collectors.toList()); + } + + default List generatePersons(int count) { + return IntStream.range(0, count) + .mapToObj(i -> Person.builder().id(nextId()) + .firstName("firstName" + i) + .emailAddress("gmail.com") + .build()) + .peek(this::save) + .collect(Collectors.toList()); + } + + void save(T obj); + GroupedEntities findByIds(GroupedKeys groupedKeys); +} diff --git a/src/test/java/org/springframework/data/aerospike/core/AerospikeTemplateFindByEntitiesTests.java b/src/test/java/org/springframework/data/aerospike/core/AerospikeTemplateFindByEntitiesTests.java new file mode 100644 index 000000000..04e79f681 --- /dev/null +++ b/src/test/java/org/springframework/data/aerospike/core/AerospikeTemplateFindByEntitiesTests.java @@ -0,0 +1,18 @@ +package org.springframework.data.aerospike.core; + +import org.springframework.data.aerospike.BaseBlockingIntegrationTests; +import org.springframework.data.aerospike.core.model.GroupedEntities; +import org.springframework.data.aerospike.core.model.GroupedKeys; + +public class AerospikeTemplateFindByEntitiesTests extends BaseBlockingIntegrationTests implements AbstractFindByEntitiesTest { + + @Override + public void save(T obj) { + template.save(obj); + } + + @Override + public GroupedEntities findByIds(GroupedKeys groupedKeys) { + return template.findByIds(groupedKeys); + } +} diff --git a/src/test/java/org/springframework/data/aerospike/core/model/GroupedEntitiesTest.java b/src/test/java/org/springframework/data/aerospike/core/model/GroupedEntitiesTest.java new file mode 100644 index 000000000..986363d0e --- /dev/null +++ b/src/test/java/org/springframework/data/aerospike/core/model/GroupedEntitiesTest.java @@ -0,0 +1,38 @@ +package org.springframework.data.aerospike.core.model; + + +import org.junit.Test; +import org.springframework.data.aerospike.sample.Customer; +import org.springframework.data.aerospike.sample.Person; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GroupedEntitiesTest { + + private static final GroupedEntities TEST_GROUPED_ENTITIES = GroupedEntities.builder() + .entity(Person.class, Person.builder().id("22").build()) + .entity(Customer.class, Customer.builder().id("33").build()) + .build(); + @Test + public void shouldGetEntitiesByClass() { + Person expectedResult = Person.builder().id("22").build(); + assertThat(TEST_GROUPED_ENTITIES.getEntitiesByClass(Person.class)) + .containsExactlyInAnyOrder(expectedResult); + } + + @Test + public void shouldReturnAnEmptyResultIfGroupedEntitiesDoesNotContainResult() { + assertThat(TEST_GROUPED_ENTITIES.getEntitiesByClass(String.class)).isEmpty(); + } + + @Test + public void shouldContainEntities() { + assertThat(TEST_GROUPED_ENTITIES.containsEntities()).isTrue(); + } + + @Test + public void shouldNotContainEntities() { + GroupedEntities groupedEntities = GroupedEntities.builder().build(); + assertThat(groupedEntities.containsEntities()).isFalse(); + } +} diff --git a/src/test/java/org/springframework/data/aerospike/core/model/GroupedKeysTest.java b/src/test/java/org/springframework/data/aerospike/core/model/GroupedKeysTest.java new file mode 100644 index 000000000..4ca51f9a6 --- /dev/null +++ b/src/test/java/org/springframework/data/aerospike/core/model/GroupedKeysTest.java @@ -0,0 +1,32 @@ +package org.springframework.data.aerospike.core.model; + + +import org.junit.Test; +import org.springframework.data.aerospike.sample.Person; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GroupedKeysTest { + + @Test + public void shouldGetEntitiesKeys() { + Set keys = new HashSet<>(); + keys.add("p22"); + + GroupedKeys groupedKeys = GroupedKeys.builder() + .entityKeys(Person.class, keys) + .build(); + + Map, Collection> expectedResult = + new HashMap<>(); + expectedResult.put(Person.class, keys); + + assertThat(groupedKeys.getEntitiesKeys()).containsAllEntriesOf(expectedResult); + } +} diff --git a/src/test/java/org/springframework/data/aerospike/core/reactive/ReactiveAerospikeTemplateFindByEntitiesTest.java b/src/test/java/org/springframework/data/aerospike/core/reactive/ReactiveAerospikeTemplateFindByEntitiesTest.java new file mode 100644 index 000000000..b0bd48fb1 --- /dev/null +++ b/src/test/java/org/springframework/data/aerospike/core/reactive/ReactiveAerospikeTemplateFindByEntitiesTest.java @@ -0,0 +1,24 @@ +package org.springframework.data.aerospike.core.reactive; + +import org.springframework.data.aerospike.BaseReactiveIntegrationTests; +import org.springframework.data.aerospike.core.AbstractFindByEntitiesTest; +import org.springframework.data.aerospike.core.model.GroupedEntities; +import org.springframework.data.aerospike.core.model.GroupedKeys; +import reactor.core.scheduler.Schedulers; + +public class ReactiveAerospikeTemplateFindByEntitiesTest extends BaseReactiveIntegrationTests implements AbstractFindByEntitiesTest { + + @Override + public void save(T obj) { + reactiveTemplate.save(obj) + .subscribeOn(Schedulers.parallel()) + .block(); + } + + @Override + public GroupedEntities findByIds(GroupedKeys groupedKeys) { + return reactiveTemplate.findByIds(groupedKeys) + .subscribeOn(Schedulers.parallel()) + .block(); + } +} diff --git a/src/test/java/org/springframework/data/aerospike/repository/reactive/ReactiveAerospikeRepositoryDeleteRelatedTests.java b/src/test/java/org/springframework/data/aerospike/repository/reactive/ReactiveAerospikeRepositoryDeleteRelatedTests.java index fa9d3aeeb..706035268 100644 --- a/src/test/java/org/springframework/data/aerospike/repository/reactive/ReactiveAerospikeRepositoryDeleteRelatedTests.java +++ b/src/test/java/org/springframework/data/aerospike/repository/reactive/ReactiveAerospikeRepositoryDeleteRelatedTests.java @@ -145,4 +145,12 @@ public void deleteAllPublisher_ShouldSkipNonexistent() { StepVerifier.create(customerRepo.findById(customer1.getId())).expectNextCount(0).verifyComplete(); StepVerifier.create(customerRepo.findById(customer2.getId())).expectNextCount(0).verifyComplete(); } + + @Test + public void deleteAllById_ShouldDelete() { + customerRepo.deleteAllById(asList(customer1.getId(), customer2.getId())).subscribeOn(Schedulers.parallel()).block(); + + StepVerifier.create(customerRepo.findById(customer1.getId())).expectNextCount(0).verifyComplete(); + StepVerifier.create(customerRepo.findById(customer2.getId())).expectNextCount(0).verifyComplete(); + } } \ No newline at end of file diff --git a/src/test/java/org/springframework/data/aerospike/repository/support/SimpleAerospikeRepositoryTest.java b/src/test/java/org/springframework/data/aerospike/repository/support/SimpleAerospikeRepositoryTest.java index b28034bd0..f9ffbc477 100644 --- a/src/test/java/org/springframework/data/aerospike/repository/support/SimpleAerospikeRepositoryTest.java +++ b/src/test/java/org/springframework/data/aerospike/repository/support/SimpleAerospikeRepositoryTest.java @@ -164,6 +164,18 @@ public void deleteID() { verify(operations).delete("one", Person.class); } + @Test + public void deleteAllById() { + List personIds = testPersons.stream() + .map(Person::getId) + .collect(toList()); + aerospikeRepository.deleteAllById(personIds); + + verify(operations).delete("one", Person.class); + verify(operations).delete("two", Person.class); + verify(operations).delete("three", Person.class); + } + @Test public void deleteIterableOfQExtendsT() { aerospikeRepository.deleteAll(testPersons);