Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-keyvalue</artifactId>
<version>4.0.0-SNAPSHOT</version>
<version>4.0.0-GH-71-SNAPSHOT</version>

<name>Spring Data KeyValue</name>

Expand All @@ -18,6 +18,7 @@

<properties>
<springdata.commons>4.0.0-SNAPSHOT</springdata.commons>
<mapdb>3.1.0</mapdb>
<java-module-name>spring.data.keyvalue</java-module-name>
</properties>

Expand Down Expand Up @@ -45,6 +46,14 @@
<version>${querydsl}</version>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.mapdb</groupId>
<artifactId>mapdb</artifactId>
<version>${mapdb}</version>
<scope>test</scope>
</dependency>

</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
*/
public class PredicateQueryEngine extends QueryEngine<KeyValueAdapter, Predicate<?>, Comparator<?>> {

public static final PredicateQueryEngine INSTANCE = new PredicateQueryEngine();

/**
* Creates a new {@link PredicateQueryEngine}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
95 changes: 95 additions & 0 deletions src/main/java/org/springframework/data/map/KeySpaceStore.java
Original file line number Diff line number Diff line change
@@ -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).
* <p>
* 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.
* <p>
* For example, a {@link ConcurrentHashMap} can be used as keystore map type to allow concurrent access to keyspaces
* using:
*
* <pre class="code">
* KeyspaceStore store = KeyspaceStore.create();
* </pre>
*
* Custom map types (or instances of these) can be used as well using the provided factory methods:
*
* <pre class="code">
* KeyspaceStore store = KeyspaceStore.of(LinkedHashMap.class);
*
* Map<String, Map<Object, Object>> backingMap = …;
* KeyspaceStore store = KeyspaceStore.of(backingMap);
* </pre>
*
* @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<Object, Object> 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<String, Map<Object, Object>> store) {
return MapKeySpaceStore.of(store);
}

}
89 changes: 89 additions & 0 deletions src/main/java/org/springframework/data/map/MapKeySpaceStore.java
Original file line number Diff line number Diff line change
@@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Author?

*/
@SuppressWarnings("rawtypes")
record MapKeySpaceStore(Map<String, Map<Object, Object>> store, Class<? extends Map> keySpaceMapType,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting use of record as the impl for the store. Any limitations of using record for this? Will the equals auto-implemented by record be sufficient? It may go through the entire store contents to check equality.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using records as interface implementations is a neat pattern as our API surface is limited by the interface. Clearly, Object methods are inherited and their functionality can be surfaced in various ways. We don't define a contract regarding equality and hash code but we might fill in these blanks as we see fit.

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<String, Map<Object, Object>> 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<Object, Object> getKeySpace(String keyspace) {
return store.computeIfAbsent(keyspace, k -> CollectionFactory.createMap(keySpaceMapType, initialCapacity));
}

@Override
public void clear() {

store.values().forEach(Map::clear);
store.clear();
}

}
51 changes: 23 additions & 28 deletions src/main/java/org/springframework/data/map/MapKeyValueAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand All @@ -41,15 +41,13 @@
*/
public class MapKeyValueAdapter extends AbstractKeyValueAdapter {

@SuppressWarnings("rawtypes") //
private final Class<? extends Map> keySpaceMapType;
private final Map<String, Map<Object, Object>> store;
private final KeySpaceStore store;

/**
* Create new {@link MapKeyValueAdapter} using {@link ConcurrentHashMap} as backing store type.
*/
public MapKeyValueAdapter() {
this(ConcurrentHashMap.class);
this(MapKeySpaceStore.create());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to not use the KeySpaceStore static factory methods?

Copy link
Member Author

@mp911de mp911de Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially, a left-over from evolving the interface while moving things around. It lead to some more thoughts on the design and I refined create vs of method naming removing the default ConcurrentHashMap convenience method at KeySpaceStore. We can add it later if necessary and for the time being, we can be less opinionated with the public API while retaining some convenience in our internal code. It's an assumption in the same sense as MapKeySpaceStore is good enough for our own perspective on map storage making it sufficient to pass all tests. Yet, I would not want to expose MapKeySpaceStore as public API because it is an implementation detail. If we see need to evolve an API around the storage itself, then we can take action, but for now, less exposure gives us more flexibility and less API surface.

}

/**
Expand All @@ -59,7 +57,7 @@ public MapKeyValueAdapter() {
* @since 2.4
*/
public MapKeyValueAdapter(QueryEngine<? extends KeyValueAdapter, ?, ?> engine) {
this(ConcurrentHashMap.class, engine);
this(MapKeySpaceStore.create(), engine);
}

/**
Expand All @@ -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));
}

/**
Expand All @@ -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<Comparator<?>> 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));
}

/**
Expand All @@ -98,17 +91,16 @@ public MapKeyValueAdapter(Class<? extends Map> mapType, SortAccessor<Comparator<
*/
@SuppressWarnings("rawtypes")
public MapKeyValueAdapter(Class<? extends Map> mapType, QueryEngine<? extends KeyValueAdapter, ?, ?> engine) {
this(CollectionFactory.createMap(mapType, 100), mapType, engine);
this(MapKeySpaceStore.of(mapType), engine);
}

/**
* Create new instance of {@link MapKeyValueAdapter} using given dataStore for persistence.
*
* @param store must not be {@literal null}.
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public MapKeyValueAdapter(Map<String, Map<Object, Object>> store) {
this(store, (Class<? extends Map>) ClassUtils.getUserClass(store), null);
this(MapKeySpaceStore.of(store));
}

/**
Expand All @@ -118,29 +110,32 @@ public MapKeyValueAdapter(Map<String, Map<Object, Object>> store) {
* @param engine the query engine.
* @since 2.4
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public MapKeyValueAdapter(Map<String, Map<Object, Object>> 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());
}

/**
* Creates a new {@link MapKeyValueAdapter} with the given store and type to be used when creating key spaces and
* 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<String, Map<Object, Object>> 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
Expand Down Expand Up @@ -210,7 +205,7 @@ public void destroy() throws Exception {
protected Map<Object, Object> 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);
}

}
Loading