, 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 extends Map> 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 extends Map> 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 extends Map> 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 extends Map, ?>> userClass = (Class extends Map, ?>>) 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 extends Map> 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 extends KeyValueAdapter, ?, ?> engine) {
- this(ConcurrentHashMap.class, engine);
+ this(MapKeySpaceStore.create(), engine);
}
/**
@@ -69,7 +67,7 @@ public MapKeyValueAdapter(QueryEngine extends KeyValueAdapter, ?, ?> engine) {
*/
@SuppressWarnings("rawtypes")
public MapKeyValueAdapter(Class extends Map> mapType) {
- this(CollectionFactory.createMap(mapType, 100), mapType, null);
+ this(MapKeySpaceStore.of(mapType));
}
/**
@@ -79,14 +77,9 @@ public MapKeyValueAdapter(Class extends Map> mapType) {
* @param sortAccessor accessor granting access to sorting implementation
* @since 3.1.10
*/
+ @SuppressWarnings("rawtypes")
public MapKeyValueAdapter(Class extends Map> mapType, SortAccessor> sortAccessor) {
-
- super(sortAccessor);
-
- Assert.notNull(mapType, "Store must not be null");
-
- this.store = CollectionFactory.createMap(mapType, 100);
- this.keySpaceMapType = (Class extends Map>) ClassUtils.getUserClass(store);
+ this(MapKeySpaceStore.of(mapType), new PredicateQueryEngine(sortAccessor));
}
/**
@@ -98,7 +91,7 @@ public MapKeyValueAdapter(Class extends Map> mapType, SortAccessor mapType, QueryEngine extends KeyValueAdapter, ?, ?> engine) {
- this(CollectionFactory.createMap(mapType, 100), mapType, engine);
+ this(MapKeySpaceStore.of(mapType), engine);
}
/**
@@ -106,9 +99,8 @@ public MapKeyValueAdapter(Class extends Map> mapType, QueryEngine extends Ke
*
* @param store must not be {@literal null}.
*/
- @SuppressWarnings({ "rawtypes", "unchecked" })
public MapKeyValueAdapter(Map> store) {
- this(store, (Class extends Map>) 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 extends KeyValueAdapter, ?, ?> engine) {
- this(store, (Class extends Map>) 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
* query engine.
*
* @param store must not be {@literal null}.
- * @param keySpaceMapType must not be {@literal null}.
* @param engine the query engine.
*/
- @SuppressWarnings("rawtypes")
- private MapKeyValueAdapter(Map> store, Class extends Map> keySpaceMapType, @Nullable QueryEngine extends KeyValueAdapter, ?, ?> engine) {
+ public MapKeyValueAdapter(KeySpaceStore store, @Nullable QueryEngine extends KeyValueAdapter, ?, ?> 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 extends Map> 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 extends SortAccessor> 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 extends Map> getMapTypeToUse(RepositoryConfigurationSource source) {
+ private static Object getKeySpaceStore(RepositoryConfigurationSource source) {
+
+ Optional keySpaceStoreRef = source.getAttribute("keySpaceStoreRef", String.class);
- return (Class extends Map>) 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 extends SortAccessor>> sortAccessorType = (Class extends SortAccessor>>) getAnnotationAttributes(
- source).get("sortAccessor");
+ Class extends SortAccessor>> 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 extends QueryEngineFactory> queryEngineFactoryType = (Class extends QueryEngineFactory>) getAnnotationAttributes(
- source).get("queryEngineFactory");
+ Class extends QueryEngineFactory> queryEngineFactoryType = getClassAttribute(source, "queryEngineFactory");
- if(queryEngineFactoryType == null || queryEngineFactoryType.isInterface()) {
+ if (queryEngineFactoryType == null) {
return null;
}
@@ -122,21 +126,8 @@ private static Class extends Map> 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) {