diff --git a/pom.xml b/pom.xml index 4cc6241b..b15e17be 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-keyvalue - 4.0.0-SNAPSHOT + 4.0.0-GH-71-SNAPSHOT Spring Data KeyValue @@ -18,6 +18,7 @@ 4.0.0-SNAPSHOT + 3.1.0 spring.data.keyvalue @@ -45,6 +46,14 @@ ${querydsl} true + + + org.mapdb + mapdb + ${mapdb} + test + + diff --git a/src/main/java/org/springframework/data/keyvalue/core/PredicateQueryEngine.java b/src/main/java/org/springframework/data/keyvalue/core/PredicateQueryEngine.java index 1577a7a2..8b954e4b 100644 --- a/src/main/java/org/springframework/data/keyvalue/core/PredicateQueryEngine.java +++ b/src/main/java/org/springframework/data/keyvalue/core/PredicateQueryEngine.java @@ -35,6 +35,8 @@ */ public class PredicateQueryEngine extends QueryEngine, Comparator> { + public static final PredicateQueryEngine INSTANCE = new PredicateQueryEngine(); + /** * Creates a new {@link PredicateQueryEngine}. */ diff --git a/src/main/java/org/springframework/data/keyvalue/repository/query/KeyValuePartTreeQuery.java b/src/main/java/org/springframework/data/keyvalue/repository/query/KeyValuePartTreeQuery.java index 76325790..ee46210f 100644 --- a/src/main/java/org/springframework/data/keyvalue/repository/query/KeyValuePartTreeQuery.java +++ b/src/main/java/org/springframework/data/keyvalue/repository/query/KeyValuePartTreeQuery.java @@ -134,10 +134,11 @@ public KeyValuePartTreeQuery(QueryMethod queryMethod, ValueExpressionDelegate va return new PageImpl(IterableConverter.toList(result), page, count); } else if (queryMethod.isCollectionQuery()) { - return this.keyValueOperations.find(query, queryMethod.getEntityInformation().getJavaType()); } else if (partTree.get().isExistsProjection()) { return keyValueOperations.exists(query, queryMethod.getEntityInformation().getJavaType()); + } else if (partTree.get().isCountProjection()) { + return keyValueOperations.count(query, queryMethod.getEntityInformation().getJavaType()); } else { Iterable result = this.keyValueOperations.find(query, queryMethod.getEntityInformation().getJavaType()); diff --git a/src/main/java/org/springframework/data/map/KeySpaceStore.java b/src/main/java/org/springframework/data/map/KeySpaceStore.java new file mode 100644 index 00000000..d38214e0 --- /dev/null +++ b/src/main/java/org/springframework/data/map/KeySpaceStore.java @@ -0,0 +1,95 @@ +/* + * Copyright 2025 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.map; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Strategy interface to obtain a map for a given key space. Implementations should be thread-safe when intended for use + * with multiple threads (both, the store itself and the used keystore maps). + *

+ * Can be used to plug in keystore creation or implementation strategies (for example, Map-based implementations such as + * MapDB or Infinispan) through a consolidated interface. A keyspace store represents a map of maps or a database with + * multiple collections and can use any kind of map per keyspace. + *

+ * For example, a {@link ConcurrentHashMap} can be used as keystore map type to allow concurrent access to keyspaces + * using: + * + *

+ * KeyspaceStore store = KeyspaceStore.create();
+ * 
+ * + * Custom map types (or instances of these) can be used as well using the provided factory methods: + * + *
+ * KeyspaceStore store = KeyspaceStore.of(LinkedHashMap.class);
+ *
+ * Map> backingMap = …;
+ * KeyspaceStore store = KeyspaceStore.of(backingMap);
+ * 
+ * + * @since 4.0 + */ +public interface KeySpaceStore { + + /** + * Return the map associated with given keyspace. Implementations can return an empty map if the keyspace does not + * exist yet or a reference to the map that represents an existing keyspace holding keys and values for the requested + * keyspace. + * + * @param keyspace name of the keyspace to obtain the map for, must not be {@literal null}. + * @return the map associated with the given keyspace, never {@literal null}. + */ + Map getKeySpace(String keyspace); + + /** + * Clear all keyspaces. Access to {@link #getKeySpace(String)} will return an empty map for each keyspace after this + * method call. It is not required to clear each keyspace individually but it makes sense to do so to free up memory. + */ + void clear(); + + /** + * Create a new {@link KeySpaceStore} using {@link ConcurrentHashMap} as backing map type for each keyspace map. + * + * @return a new and empty {@link KeySpaceStore}. + */ + static KeySpaceStore create() { + return MapKeySpaceStore.create(); + } + + /** + * Create new {@link KeySpaceStore} using given map type for each keyspace map. + * + * @param mapType map type to use. + * @return the new {@link KeySpaceStore} object. + */ + @SuppressWarnings("rawtypes") + static KeySpaceStore of(Class mapType) { + return MapKeySpaceStore.of(mapType); + } + + /** + * Create new {@link KeySpaceStore} using given map as backing store. Determines the map type from the given map. + * + * @param store map of maps. + * @return the new {@link KeySpaceStore} object for the given {@code store}. + */ + static KeySpaceStore of(Map> store) { + return MapKeySpaceStore.of(store); + } + +} diff --git a/src/main/java/org/springframework/data/map/MapKeySpaceStore.java b/src/main/java/org/springframework/data/map/MapKeySpaceStore.java new file mode 100644 index 00000000..bb9b227b --- /dev/null +++ b/src/main/java/org/springframework/data/map/MapKeySpaceStore.java @@ -0,0 +1,89 @@ +/* + * Copyright 2025 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.map; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.core.CollectionFactory; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Keyspace store that uses a map of maps to store keyspace data. The outer map holds the keyspaces and the inner maps + * hold the actual keys and values. + * + * @param store reference to the map of maps holding the keyspace data. + * @param keySpaceMapType map type to be used for each keyspace map. + * @param initialCapacity initial keyspace map capacity to optimize allocations. + * @since 4.0 + */ +@SuppressWarnings("rawtypes") +record MapKeySpaceStore(Map> store, Class keySpaceMapType, + int initialCapacity) implements KeySpaceStore { + + public static final int DEFAULT_INITIAL_CAPACITY = 1000; + + /** + * Create a new {@link KeySpaceStore} using {@link ConcurrentHashMap} as backing map type for each keyspace map. + * + * @return a new and empty {@link KeySpaceStore}. + */ + public static KeySpaceStore create() { + return new MapKeySpaceStore(new ConcurrentHashMap<>(100), ConcurrentHashMap.class, DEFAULT_INITIAL_CAPACITY); + } + + /** + * Create new {@link KeySpaceStore} using given map type for each keyspace map. + * + * @param mapType map type to use. + * @return the new {@link KeySpaceStore} object. + */ + public static KeySpaceStore of(Class mapType) { + + Assert.notNull(mapType, "Store map type must not be null"); + + return of(CollectionFactory.createMap(mapType, 100)); + } + + /** + * Create new {@link KeySpaceStore} using given map as backing store. Determines the map type from the given map. + * + * @param store map of maps. + * @return the new {@link KeySpaceStore} object for the given {@code store}. + */ + @SuppressWarnings("unchecked") + public static KeySpaceStore of(Map> store) { + + Assert.notNull(store, "Store map must not be null"); + + Class> userClass = (Class>) ClassUtils.getUserClass(store); + return new MapKeySpaceStore(store, userClass, DEFAULT_INITIAL_CAPACITY); + } + + @Override + public Map getKeySpace(String keyspace) { + return store.computeIfAbsent(keyspace, k -> CollectionFactory.createMap(keySpaceMapType, initialCapacity)); + } + + @Override + public void clear() { + + store.values().forEach(Map::clear); + store.clear(); + } + +} diff --git a/src/main/java/org/springframework/data/map/MapKeyValueAdapter.java b/src/main/java/org/springframework/data/map/MapKeyValueAdapter.java index 3018c104..d66a4c7a 100644 --- a/src/main/java/org/springframework/data/map/MapKeyValueAdapter.java +++ b/src/main/java/org/springframework/data/map/MapKeyValueAdapter.java @@ -22,15 +22,15 @@ import java.util.concurrent.ConcurrentHashMap; import org.jspecify.annotations.Nullable; -import org.springframework.core.CollectionFactory; + import org.springframework.data.keyvalue.core.AbstractKeyValueAdapter; import org.springframework.data.keyvalue.core.ForwardingCloseableIterator; import org.springframework.data.keyvalue.core.KeyValueAdapter; +import org.springframework.data.keyvalue.core.PredicateQueryEngine; import org.springframework.data.keyvalue.core.QueryEngine; import org.springframework.data.keyvalue.core.SortAccessor; import org.springframework.data.util.CloseableIterator; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; /** * {@link KeyValueAdapter} implementation for {@link Map}. @@ -41,15 +41,13 @@ */ public class MapKeyValueAdapter extends AbstractKeyValueAdapter { - @SuppressWarnings("rawtypes") // - private final Class keySpaceMapType; - private final Map> store; + private final KeySpaceStore store; /** * Create new {@link MapKeyValueAdapter} using {@link ConcurrentHashMap} as backing store type. */ public MapKeyValueAdapter() { - this(ConcurrentHashMap.class); + this(MapKeySpaceStore.create()); } /** @@ -59,7 +57,7 @@ public MapKeyValueAdapter() { * @since 2.4 */ public MapKeyValueAdapter(QueryEngine engine) { - this(ConcurrentHashMap.class, engine); + this(MapKeySpaceStore.create(), engine); } /** @@ -69,7 +67,7 @@ public MapKeyValueAdapter(QueryEngine engine) { */ @SuppressWarnings("rawtypes") public MapKeyValueAdapter(Class mapType) { - this(CollectionFactory.createMap(mapType, 100), mapType, null); + this(MapKeySpaceStore.of(mapType)); } /** @@ -79,14 +77,9 @@ public MapKeyValueAdapter(Class mapType) { * @param sortAccessor accessor granting access to sorting implementation * @since 3.1.10 */ + @SuppressWarnings("rawtypes") public MapKeyValueAdapter(Class mapType, SortAccessor> sortAccessor) { - - super(sortAccessor); - - Assert.notNull(mapType, "Store must not be null"); - - this.store = CollectionFactory.createMap(mapType, 100); - this.keySpaceMapType = (Class) ClassUtils.getUserClass(store); + this(MapKeySpaceStore.of(mapType), new PredicateQueryEngine(sortAccessor)); } /** @@ -98,7 +91,7 @@ public MapKeyValueAdapter(Class mapType, SortAccessor mapType, QueryEngine engine) { - this(CollectionFactory.createMap(mapType, 100), mapType, engine); + this(MapKeySpaceStore.of(mapType), engine); } /** @@ -106,9 +99,8 @@ public MapKeyValueAdapter(Class mapType, QueryEngine> store) { - this(store, (Class) ClassUtils.getUserClass(store), null); + this(MapKeySpaceStore.of(store)); } /** @@ -118,9 +110,17 @@ public MapKeyValueAdapter(Map> store) { * @param engine the query engine. * @since 2.4 */ - @SuppressWarnings({ "rawtypes", "unchecked" }) public MapKeyValueAdapter(Map> store, QueryEngine engine) { - this(store, (Class) ClassUtils.getUserClass(store), engine); + this(MapKeySpaceStore.of(store), engine); + } + + /** + * Create new instance of {@link MapKeyValueAdapter} using given dataStore for persistence. + * + * @param store must not be {@literal null}. + */ + public MapKeyValueAdapter(KeySpaceStore store) { + this(store, new PredicateQueryEngine()); } /** @@ -128,19 +128,14 @@ public MapKeyValueAdapter(Map> store, QueryEngine> store, Class keySpaceMapType, @Nullable QueryEngine engine) { + public MapKeyValueAdapter(KeySpaceStore store, @Nullable QueryEngine engine) { super(engine); - Assert.notNull(store, "Store must not be null"); - Assert.notNull(keySpaceMapType, "Map type to be used for key spaces must not be null"); - + Assert.notNull(store, "KeyspaceStore must not be null"); this.store = store; - this.keySpaceMapType = keySpaceMapType; } @Override @@ -210,7 +205,7 @@ public void destroy() throws Exception { protected Map getKeySpaceMap(String keyspace) { Assert.notNull(keyspace, "Collection must not be null for lookup"); - return store.computeIfAbsent(keyspace, k -> CollectionFactory.createMap(keySpaceMapType, 1000)); + return store.getKeySpace(keyspace); } } diff --git a/src/main/java/org/springframework/data/map/repository/config/EnableMapRepositories.java b/src/main/java/org/springframework/data/map/repository/config/EnableMapRepositories.java index 482e180d..25e2d62d 100644 --- a/src/main/java/org/springframework/data/map/repository/config/EnableMapRepositories.java +++ b/src/main/java/org/springframework/data/map/repository/config/EnableMapRepositories.java @@ -149,13 +149,23 @@ /** * Configures the {@link Map} structure used for data storage. Defaults to {@link ConcurrentHashMap}. Will be ignored - * in case an explicit bean for the {@link KeyValueTemplate} is available in the {@link ApplicationContext}. + * in case an explicit bean for the {@link KeyValueTemplate} is available in the {@link ApplicationContext} or + * {@link #keySpaceStoreRef()} is configured. * * @see #keyValueTemplateRef() */ @SuppressWarnings("rawtypes") Class mapType() default ConcurrentHashMap.class; + /** + * Configures the name to a {@link org.springframework.data.map.KeySpaceStore} bean to be used as database for all + * keyspaces. + * + * @since 4.0 + * @see org.springframework.data.map.KeySpaceStore + */ + String keySpaceStoreRef() default ""; + /** * Configures the {@link QueryEngineFactory} to create the QueryEngine. When both, the query engine and sort accessors * are configured, the query engine is instantiated using the configured sort accessor. @@ -182,4 +192,5 @@ * @since 3.1.10 */ Class sortAccessor() default SortAccessor.class; + } diff --git a/src/main/java/org/springframework/data/map/repository/config/MapRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/map/repository/config/MapRepositoryConfigurationExtension.java index 411f22e5..0e075647 100644 --- a/src/main/java/org/springframework/data/map/repository/config/MapRepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/map/repository/config/MapRepositoryConfigurationExtension.java @@ -16,20 +16,23 @@ package org.springframework.data.map.repository.config; import java.lang.reflect.Constructor; -import java.util.Map; +import java.util.Optional; +import java.util.function.Predicate; import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.core.type.AnnotationMetadata; import org.springframework.data.config.ParsingUtils; import org.springframework.data.keyvalue.core.KeyValueTemplate; import org.springframework.data.keyvalue.core.QueryEngine; import org.springframework.data.keyvalue.core.QueryEngineFactory; import org.springframework.data.keyvalue.core.SortAccessor; import org.springframework.data.keyvalue.repository.config.KeyValueRepositoryConfigurationExtension; +import org.springframework.data.map.KeySpaceStore; import org.springframework.data.map.MapKeyValueAdapter; import org.springframework.data.repository.config.RepositoryConfigurationExtension; import org.springframework.data.repository.config.RepositoryConfigurationSource; @@ -64,7 +67,7 @@ protected AbstractBeanDefinition getDefaultKeyValueTemplateBeanDefinition( RepositoryConfigurationSource configurationSource) { BeanDefinitionBuilder adapterBuilder = BeanDefinitionBuilder.rootBeanDefinition(MapKeyValueAdapter.class); - adapterBuilder.addConstructorArgValue(getMapTypeToUse(configurationSource)); + adapterBuilder.addConstructorArgValue(getKeySpaceStore(configurationSource)); SortAccessor sortAccessor = getSortAccessor(configurationSource); QueryEngine queryEngine = getQueryEngine(sortAccessor, configurationSource); @@ -83,31 +86,32 @@ protected AbstractBeanDefinition getDefaultKeyValueTemplateBeanDefinition( return ParsingUtils.getSourceBeanDefinition(builder, configurationSource.getSource()); } - @SuppressWarnings({ "unchecked", "rawtypes", "NullAway" }) - private static Class getMapTypeToUse(RepositoryConfigurationSource source) { + private static Object getKeySpaceStore(RepositoryConfigurationSource source) { + + Optional keySpaceStoreRef = source.getAttribute("keySpaceStoreRef", String.class); - return (Class) getAnnotationAttributes(source).get("mapType"); + return keySpaceStoreRef.map(beanName -> new RuntimeBeanReference(beanName, KeySpaceStore.class)) // + .map(Object.class::cast) // + .orElseGet(() -> source.getRequiredAttribute("mapType", Class.class)); } private static @Nullable SortAccessor getSortAccessor(RepositoryConfigurationSource source) { - Class> sortAccessorType = (Class>) getAnnotationAttributes( - source).get("sortAccessor"); + Class> sortAccessorType = getClassAttribute(source, "sortAccessor"); - if (sortAccessorType != null && !sortAccessorType.isInterface()) { - return BeanUtils.instantiateClass(sortAccessorType); + if (sortAccessorType == null) { + return null; } - return null; + return BeanUtils.instantiateClass(sortAccessorType); } private static @Nullable QueryEngine getQueryEngine(@Nullable SortAccessor sortAccessor, RepositoryConfigurationSource source) { - Class queryEngineFactoryType = (Class) getAnnotationAttributes( - source).get("queryEngineFactory"); + Class queryEngineFactoryType = getClassAttribute(source, "queryEngineFactory"); - if(queryEngineFactoryType == null || queryEngineFactoryType.isInterface()) { + if (queryEngineFactoryType == null) { return null; } @@ -122,21 +126,8 @@ private static Class getMapTypeToUse(RepositoryConfigurationSourc return BeanUtils.instantiateClass(queryEngineFactoryType).create(); } - private static Map getAnnotationAttributes(RepositoryConfigurationSource source) { - - AnnotationMetadata annotationSource = (AnnotationMetadata) source.getSource(); - - if (annotationSource == null) { - throw new IllegalArgumentException("AnnotationSource not available"); - } - - Map annotationAttributes = annotationSource - .getAnnotationAttributes(EnableMapRepositories.class.getName()); - - if (annotationAttributes == null) { - throw new IllegalStateException("No annotation attributes for @EnableMapRepositories"); - } - - return annotationAttributes; + private static @Nullable Class getClassAttribute(RepositoryConfigurationSource source, String attributeName) { + return source.getAttribute(attributeName, Class.class).filter(Predicate.not(Class::isInterface)).orElse(null); } + } diff --git a/src/test/java/org/springframework/data/keyvalue/repository/query/KeyValuePartTreeQueryUnitTests.java b/src/test/java/org/springframework/data/keyvalue/repository/query/KeyValuePartTreeQueryUnitTests.java index d6ed4e81..f30f06a5 100644 --- a/src/test/java/org/springframework/data/keyvalue/repository/query/KeyValuePartTreeQueryUnitTests.java +++ b/src/test/java/org/springframework/data/keyvalue/repository/query/KeyValuePartTreeQueryUnitTests.java @@ -39,6 +39,8 @@ import org.springframework.data.util.TypeInformation; /** + * Unit tests for {@link KeyValuePartTreeQuery}. + * * @author Christoph Strobl * @author Mark Paluch */ @@ -175,12 +177,34 @@ void shouldUseCountForExists() throws NoSuchMethodException { verify(kvOpsMock).exists(eq(query), eq(Person.class)); } + @Test // GH-71 + void shouldUseCountForCount() throws NoSuchMethodException { + + when(metadataMock.getDomainType()).thenReturn((Class) Person.class); + when(metadataMock.getDomainTypeInformation()).thenReturn((TypeInformation) TypeInformation.of(Person.class)); + when(metadataMock.getReturnType(any(Method.class))).thenReturn((TypeInformation) TypeInformation.of(Boolean.class)); + when(metadataMock.getReturnedDomainClass(any(Method.class))).thenReturn((Class) Boolean.class); + + QueryMethod qm = new QueryMethod(Repo.class.getMethod("countByFirstname", String.class), metadataMock, + projectionFactoryMock); + + KeyValuePartTreeQuery partTreeQuery = new KeyValuePartTreeQuery(qm, ValueExpressionDelegate.create(), kvOpsMock, + SpelQueryCreator.class); + + KeyValueQuery query = partTreeQuery.prepareQuery(new Object[] { "firstname" }); + partTreeQuery.doExecute(new Object[] { "firstname" }, query); + + verify(kvOpsMock).count(eq(query), eq(Person.class)); + } + interface Repo { List findByFirstname(String firstname); boolean existsByFirstname(String firstname); + int countByFirstname(String firstname); + List findBy(Pageable page); List findTop3By(); diff --git a/src/test/java/org/springframework/data/map/MapDbIntegrationTests.java b/src/test/java/org/springframework/data/map/MapDbIntegrationTests.java new file mode 100644 index 00000000..0dc58ff6 --- /dev/null +++ b/src/test/java/org/springframework/data/map/MapDbIntegrationTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2025 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.map; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.mapdb.DB; +import org.mapdb.DBMaker; +import org.mapdb.HTreeMap; +import org.mapdb.Serializer; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.annotation.Id; +import org.springframework.data.keyvalue.repository.KeyValueRepository; +import org.springframework.data.map.repository.config.EnableMapRepositories; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +/** + * Example for MapDB integration testing the repository support through {@link KeySpaceStore}. + * + * @author Mark Paluch + */ +@SpringJUnitConfig +class MapDbIntegrationTests { + + @Configuration + @EnableMapRepositories(considerNestedRepositories = true, keySpaceStoreRef = "store") + static class TestConfiguration { + + @Bean + DB db() { + return DBMaker.heapDB().make(); + } + + @Bean + KeySpaceStore store(DB db) { + + return new KeySpaceStore() { + + @Override + public Map getKeySpace(String keyspace) { + return db.hashMap(keyspace, Serializer.JAVA, Serializer.JAVA).createOrOpen(); + } + + @Override + public void clear() { + db.getStore().getAllRecids().forEachRemaining(it -> db.getStore().delete(it, Serializer.JAVA)); + } + + }; + } + + } + + @Autowired PersonRepository personRepository; + @Autowired DB db; + + @Test + void shouldStoreEntriesInMapDb() { + + Person walter = personRepository.save(new Person("Walter", "White")); + personRepository.save(new Person("Skyler", "White")); + personRepository.save(new Person("Flynn", "White")); + + assertThat(personRepository.countByLastname("White")).isEqualTo(3); + + HTreeMap backingMap = db.hashMap(Person.class.getName(), Serializer.JAVA, Serializer.JAVA) + .createOrOpen(); + + assertThat(backingMap.size()).isEqualTo(3); + assertThat(backingMap).containsEntry(walter.id, walter); + } + + interface PersonRepository extends KeyValueRepository { + + long countByLastname(String lastname); + } + + static class Person { + + @Id String id; + String firstname; + String lastname; + + Person(String firstname, String lastname) { + this.firstname = firstname; + this.lastname = lastname; + } + + } + +} diff --git a/src/test/java/org/springframework/data/map/repository/config/MapRepositoriesConfigurationExtensionIntegrationTests.java b/src/test/java/org/springframework/data/map/repository/config/MapRepositoriesConfigurationExtensionIntegrationTests.java index d4b5fabd..a784695b 100644 --- a/src/test/java/org/springframework/data/map/repository/config/MapRepositoriesConfigurationExtensionIntegrationTests.java +++ b/src/test/java/org/springframework/data/map/repository/config/MapRepositoriesConfigurationExtensionIntegrationTests.java @@ -48,6 +48,7 @@ import org.springframework.data.keyvalue.repository.KeyValueRepository; import org.springframework.data.keyvalue.repository.query.PredicateQueryCreator; import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactoryBean; +import org.springframework.data.map.KeySpaceStore; import org.springframework.data.map.MapKeyValueAdapter; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.parser.AbstractQueryCreator; @@ -158,7 +159,9 @@ private static void assertKeyValueTemplateWithAdapterFor(Class mapType, Appli Object adapter = ReflectionTestUtils.getField(template, "adapter"); assertThat(adapter).isInstanceOf(MapKeyValueAdapter.class); - assertThat(ReflectionTestUtils.getField(adapter, "store")).isInstanceOf(mapType); + + KeySpaceStore store = (KeySpaceStore) ReflectionTestUtils.getField(adapter, "store"); + assertThat(ReflectionTestUtils.getField(store, "store")).isInstanceOf(mapType); } private static void assertKeyValueTemplateWithSortAccessorFor(Class sortAccessorType, ApplicationContext context) {