DATAREDIS-533 - Add support for geo indexes.
We now allow usage of @GeoIndexed to mark GeoLocation or Point properties as candidates for secondary index creation. Non null values will be included in GEOADD command as follows:
GEOADD keyspace:property-path point.x point.y entity-id
@GeoIndexed can be used on top level as well as on nested properties.
class Person {
@Id String id;
String firstname, lastname;
Address hometown;
}
class Address {
String city, street, housenumber;
@GeoIndexed Point location;
}
The above allows us to derive geospatial queries from a given method using NEAR and WITHIN keywords like:
interface PersonRepository extends CrudRepository<Person, String> {
List<Person> findByAddressLocationNear(Point point, Distance distance);
List<Person> findByAddressLocationWithin(Circle circle);
}
Partial updates on the Point itself also trigger an index refresh operation. So it is possible to alter existing entities via:
template.save(new PartialUpdate<Person>("1", Person.class).set("address.location", new Point(17, 18));
Original pull request: #215.
This commit is contained in:
committed by
Mark Paluch
parent
5d09272876
commit
07d0b82100
@@ -310,6 +310,8 @@ public class ApplicationConfig {
|
||||
== Secondary Indexes
|
||||
http://redis.io/topics/indexes[Secondary indexes] are used to enable lookup operations based on native Redis structures. Values are written to the according indexes on every save and are removed when objects are deleted or <<redis.repositories.expirations,expire>>.
|
||||
|
||||
=== Simple Property Index
|
||||
|
||||
Given the sample `Person` entity we can create an index for _firstname_ by annotating the property with `@Indexed`.
|
||||
|
||||
.Annotation driven indexing
|
||||
@@ -419,6 +421,51 @@ public class ApplicationConfig {
|
||||
----
|
||||
====
|
||||
|
||||
=== Geospatial Index
|
||||
|
||||
Assume the `Address` type contains a property `location` of type `Point` that holds the geo coordinates of the particular address. By annotating the property with `@GeoIndexed` those values will be added using Redis `GEO` commands.
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
@RedisHash("persons")
|
||||
public class Person {
|
||||
|
||||
Address address;
|
||||
|
||||
// ... other properties omitted
|
||||
}
|
||||
|
||||
public class Address {
|
||||
|
||||
@GeoIndexed Point location;
|
||||
|
||||
// ... other properties omitted
|
||||
}
|
||||
|
||||
public interface PersonRepository extends CrudRepository<Person, String> {
|
||||
|
||||
List<Person> findByAddressLocationNear(Point point, Distance distance); <1>
|
||||
List<Person> findByAddressLocationWithin(Circle circle); <2>
|
||||
}
|
||||
|
||||
Person rand = new Person("rand", "al'thor");
|
||||
rand.setAddress(new Address(new Point(13.361389D, 38.115556D)));
|
||||
|
||||
repository.save(rand); <3>
|
||||
|
||||
repository.findByAddressLocationNear(new Point(15D, 37D), new Distance(200)); <4>
|
||||
----
|
||||
<1> finder declaration on nested property using Point and Distance.
|
||||
<2> finder declaration on nested property using Circle to search within.
|
||||
<3> `GEOADD persons:address:location 13.361389 38.115556 e2c7dcee-b8cd-4424-883e-736ce564363e`
|
||||
<4> `GEORADIUS persons:address:location 15.0 37.0 200.0 km`
|
||||
====
|
||||
|
||||
In the above example the lon/lat values are stored using `GEOADD` using the objects `id` as the members name. The finder methods allow usage of `Circle` or `Point, Distance` combinations for querying those values.
|
||||
|
||||
NOTE: It is **not** possible to combine `near`/`within` with other criteria.
|
||||
|
||||
|
||||
[[redis.repositories.expirations]]
|
||||
== Time To Live
|
||||
|
||||
@@ -2431,7 +2431,7 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
|
||||
|
||||
Map<byte[], Point> byteMap = new HashMap<byte[], Point>();
|
||||
for (Entry<String, Point> entry : memberCoordinateMap.entrySet()) {
|
||||
byteMap.put(serialize(entry.getKey()), memberCoordinateMap.get(entry.getValue()));
|
||||
byteMap.put(serialize(entry.getKey()), entry.getValue());
|
||||
}
|
||||
|
||||
return geoAdd(serialize(key), byteMap);
|
||||
|
||||
@@ -18,7 +18,9 @@ package org.springframework.data.redis.core;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.data.redis.connection.DataType;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.core.convert.GeoIndexedPropertyValue;
|
||||
import org.springframework.data.redis.core.convert.IndexedData;
|
||||
import org.springframework.data.redis.core.convert.RedisConverter;
|
||||
import org.springframework.data.redis.core.convert.RemoveIndexedData;
|
||||
@@ -126,7 +128,13 @@ class IndexWriter {
|
||||
byte[] indexHelperKey = ByteUtils.concatAll(toBytes(keyspace + ":"), binKey, toBytes(":idx"));
|
||||
|
||||
for (byte[] indexKey : connection.sMembers(indexHelperKey)) {
|
||||
connection.sRem(indexKey, binKey);
|
||||
|
||||
DataType type = connection.type(indexKey);
|
||||
if (DataType.ZSET.equals(type)) {
|
||||
connection.zRem(indexKey, binKey);
|
||||
} else {
|
||||
connection.sRem(indexKey, binKey);
|
||||
}
|
||||
}
|
||||
|
||||
connection.del(indexHelperKey);
|
||||
@@ -166,7 +174,12 @@ class IndexWriter {
|
||||
|
||||
if (!CollectionUtils.isEmpty(existingKeys)) {
|
||||
for (byte[] existingKey : existingKeys) {
|
||||
connection.sRem(existingKey, key);
|
||||
|
||||
if (indexedData instanceof GeoIndexedPropertyValue) {
|
||||
connection.geoRemove(existingKey, key);
|
||||
} else {
|
||||
connection.sRem(existingKey, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,7 +220,23 @@ class IndexWriter {
|
||||
|
||||
// keep track of indexes used for the object
|
||||
connection.sAdd(ByteUtils.concatAll(toBytes(indexedData.getKeyspace() + ":"), key, toBytes(":idx")), indexKey);
|
||||
} else {
|
||||
} else if (indexedData instanceof GeoIndexedPropertyValue) {
|
||||
|
||||
GeoIndexedPropertyValue geoIndexedData = ((GeoIndexedPropertyValue) indexedData);
|
||||
|
||||
Object value = geoIndexedData.getValue();
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] indexKey = toBytes(indexedData.getKeyspace() + ":" + indexedData.getIndexName());
|
||||
connection.geoAdd(indexKey, geoIndexedData.getPoint(), key);
|
||||
|
||||
// keep track of indexes used for the object
|
||||
connection.sAdd(ByteUtils.concatAll(toBytes(indexedData.getKeyspace() + ":"), key, toBytes(":idx")), indexKey);
|
||||
}
|
||||
|
||||
else {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Cannot write index data for unknown index type %s", indexedData.getClass()));
|
||||
}
|
||||
|
||||
@@ -42,12 +42,14 @@ import org.springframework.data.keyvalue.core.AbstractKeyValueAdapter;
|
||||
import org.springframework.data.keyvalue.core.KeyValueAdapter;
|
||||
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.redis.connection.DataType;
|
||||
import org.springframework.data.redis.connection.Message;
|
||||
import org.springframework.data.redis.connection.MessageListener;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.core.PartialUpdate.PropertyUpdate;
|
||||
import org.springframework.data.redis.core.PartialUpdate.UpdateCommand;
|
||||
import org.springframework.data.redis.core.convert.CustomConversions;
|
||||
import org.springframework.data.redis.core.convert.GeoIndexedPropertyValue;
|
||||
import org.springframework.data.redis.core.convert.KeyspaceConfiguration;
|
||||
import org.springframework.data.redis.core.convert.MappingRedisConverter;
|
||||
import org.springframework.data.redis.core.convert.PathIndexResolver;
|
||||
@@ -461,8 +463,13 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
|
||||
redisUpdateObject.fieldsToRemove.toArray(new byte[redisUpdateObject.fieldsToRemove.size()][]));
|
||||
}
|
||||
|
||||
for (byte[] index : redisUpdateObject.indexesToUpdate) {
|
||||
connection.sRem(index, toBytes(redisUpdateObject.targetId));
|
||||
for (RedisUpdateObject.Index index : redisUpdateObject.indexesToUpdate) {
|
||||
|
||||
if (ObjectUtils.nullSafeEquals(DataType.ZSET, index.type)) {
|
||||
connection.zRem(index.key, toBytes(redisUpdateObject.targetId));
|
||||
} else {
|
||||
connection.sRem(index.key, toBytes(redisUpdateObject.targetId));
|
||||
}
|
||||
}
|
||||
|
||||
if (!rdo.getBucket().isEmpty()) {
|
||||
@@ -509,7 +516,8 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
|
||||
? ByteUtils.concatAll(toBytes(redisUpdateObject.keyspace), toBytes((":" + path)), toBytes(":"), value) : null;
|
||||
|
||||
if (connection.exists(existingValueIndexKey)) {
|
||||
redisUpdateObject.addIndexToUpdate(existingValueIndexKey);
|
||||
|
||||
redisUpdateObject.addIndexToUpdate(new RedisUpdateObject.Index(existingValueIndexKey, DataType.SET));
|
||||
}
|
||||
return redisUpdateObject;
|
||||
}
|
||||
@@ -530,12 +538,22 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
|
||||
: null;
|
||||
|
||||
if (connection.exists(existingValueIndexKey)) {
|
||||
redisUpdateObject.addIndexToUpdate(existingValueIndexKey);
|
||||
redisUpdateObject.addIndexToUpdate(new RedisUpdateObject.Index(existingValueIndexKey, DataType.SET));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String pathToUse = GeoIndexedPropertyValue.geoIndexName(path);
|
||||
if (connection.zRank(ByteUtils.concatAll(toBytes(redisUpdateObject.keyspace), toBytes(":"), toBytes(pathToUse)),
|
||||
toBytes(redisUpdateObject.targetId)) != null) {
|
||||
|
||||
redisUpdateObject
|
||||
.addIndexToUpdate(new org.springframework.data.redis.core.RedisKeyValueAdapter.RedisUpdateObject.Index(
|
||||
ByteUtils.concatAll(toBytes(redisUpdateObject.keyspace), toBytes(":"), toBytes(pathToUse)),
|
||||
DataType.ZSET));
|
||||
}
|
||||
|
||||
return redisUpdateObject;
|
||||
}
|
||||
|
||||
@@ -855,7 +873,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
|
||||
private final byte[] targetKey;
|
||||
|
||||
private Set<byte[]> fieldsToRemove = new LinkedHashSet<byte[]>();
|
||||
private Set<byte[]> indexesToUpdate = new LinkedHashSet<byte[]>();
|
||||
private Set<Index> indexesToUpdate = new LinkedHashSet<Index>();
|
||||
|
||||
RedisUpdateObject(byte[] targetKey, String keyspace, Object targetId) {
|
||||
|
||||
@@ -868,8 +886,19 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
|
||||
fieldsToRemove.add(field);
|
||||
}
|
||||
|
||||
void addIndexToUpdate(byte[] indexName) {
|
||||
indexesToUpdate.add(indexName);
|
||||
void addIndexToUpdate(Index index) {
|
||||
indexesToUpdate.add(index);
|
||||
}
|
||||
|
||||
static class Index {
|
||||
final DataType type;
|
||||
final byte[] key;
|
||||
|
||||
public Index(byte[] key, DataType type) {
|
||||
this.key = key;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,13 +25,19 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.data.geo.Circle;
|
||||
import org.springframework.data.geo.GeoResult;
|
||||
import org.springframework.data.geo.GeoResults;
|
||||
import org.springframework.data.keyvalue.core.CriteriaAccessor;
|
||||
import org.springframework.data.keyvalue.core.QueryEngine;
|
||||
import org.springframework.data.keyvalue.core.SortAccessor;
|
||||
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation;
|
||||
import org.springframework.data.redis.core.convert.GeoIndexedPropertyValue;
|
||||
import org.springframework.data.redis.core.convert.RedisData;
|
||||
import org.springframework.data.redis.repository.query.RedisOperationChain;
|
||||
import org.springframework.data.redis.repository.query.RedisOperationChain.NearPath;
|
||||
import org.springframework.data.redis.repository.query.RedisOperationChain.PathAndValue;
|
||||
import org.springframework.data.redis.util.ByteUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -74,7 +80,8 @@ class RedisQueryEngine extends QueryEngine<RedisKeyValueAdapter, RedisOperationC
|
||||
final int rows, final Serializable keyspace, Class<T> type) {
|
||||
|
||||
if (criteria == null
|
||||
|| (CollectionUtils.isEmpty(criteria.getOrSismember()) && CollectionUtils.isEmpty(criteria.getSismember()))) {
|
||||
|| (CollectionUtils.isEmpty(criteria.getOrSismember()) && CollectionUtils.isEmpty(criteria.getSismember()))
|
||||
&& criteria.getNear() == null) {
|
||||
return (Collection<T>) getAdapter().getAllOf(keyspace, offset, rows);
|
||||
}
|
||||
|
||||
@@ -99,6 +106,15 @@ class RedisQueryEngine extends QueryEngine<RedisKeyValueAdapter, RedisOperationC
|
||||
allKeys.addAll(connection.sUnion(keys(keyspace + ":", criteria.getOrSismember())));
|
||||
}
|
||||
|
||||
if (criteria.getNear() != null) {
|
||||
|
||||
GeoResults<GeoLocation<byte[]>> x = connection.geoRadius(geoKey(keyspace + ":", criteria.getNear()),
|
||||
new Circle(criteria.getNear().getPoint(), criteria.getNear().getDistance()));
|
||||
for (GeoResult<GeoLocation<byte[]>> y : x) {
|
||||
allKeys.add(y.getContent().getName());
|
||||
}
|
||||
}
|
||||
|
||||
byte[] keyspaceBin = getAdapter().getConverter().getConversionService().convert(keyspace + ":", byte[].class);
|
||||
|
||||
final Map<byte[], Map<byte[], byte[]>> rawData = new LinkedHashMap<byte[], Map<byte[], byte[]>>();
|
||||
@@ -195,6 +211,13 @@ class RedisQueryEngine extends QueryEngine<RedisKeyValueAdapter, RedisOperationC
|
||||
return keys;
|
||||
}
|
||||
|
||||
private byte[] geoKey(String prefix, NearPath source) {
|
||||
|
||||
String path = GeoIndexedPropertyValue.geoIndexName(source.getPath());
|
||||
return getAdapter().getConverter().getConversionService().convert(prefix + path, byte[].class);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2016 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
|
||||
*
|
||||
* http://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.redis.core.convert;
|
||||
|
||||
import org.springframework.data.geo.Point;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* {@link IndexedData} implementation indicating storage of data within a Redis GEO structure.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.8
|
||||
*/
|
||||
@Data
|
||||
public class GeoIndexedPropertyValue implements IndexedData {
|
||||
|
||||
private final String keyspace;
|
||||
private final String indexName;
|
||||
private final Point value;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.core.convert.IndexedData#getIndexName()
|
||||
*/
|
||||
@Override
|
||||
public String getIndexName() {
|
||||
return GeoIndexedPropertyValue.geoIndexName(indexName);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.core.convert.IndexedData#getKeyspace()
|
||||
*/
|
||||
@Override
|
||||
public String getKeyspace() {
|
||||
return keyspace;
|
||||
}
|
||||
|
||||
public Point getPoint() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public static String geoIndexName(String path) {
|
||||
|
||||
int index = path.lastIndexOf('.');
|
||||
if (index == -1) {
|
||||
return path;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder(path);
|
||||
sb.setCharAt(index, ':');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2016 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
|
||||
*
|
||||
* http://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.redis.core.convert;
|
||||
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.redis.core.index.GeoIndexDefinition;
|
||||
import org.springframework.data.redis.core.index.IndexDefinition;
|
||||
import org.springframework.data.redis.core.index.SimpleIndexDefinition;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.8
|
||||
*/
|
||||
class IndexedDataFactoryProvider {
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.8
|
||||
*/
|
||||
IndexedDataFactory getIndexedDataFactory(IndexDefinition definition) {
|
||||
|
||||
if (definition instanceof SimpleIndexDefinition) {
|
||||
return new SimpleIndexedPropertyValueFactory((SimpleIndexDefinition) definition);
|
||||
} else if (definition instanceof GeoIndexDefinition) {
|
||||
return new GeoIndexedPropertyValueFactory(((GeoIndexDefinition) definition));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static interface IndexedDataFactory {
|
||||
IndexedData createIndexedDataFor(Object value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.8
|
||||
*/
|
||||
static class SimpleIndexedPropertyValueFactory implements IndexedDataFactory {
|
||||
|
||||
final SimpleIndexDefinition indexDefinition;
|
||||
|
||||
public SimpleIndexedPropertyValueFactory(SimpleIndexDefinition indexDefinition) {
|
||||
this.indexDefinition = indexDefinition;
|
||||
}
|
||||
|
||||
public SimpleIndexedPropertyValue createIndexedDataFor(Object value) {
|
||||
|
||||
return new SimpleIndexedPropertyValue(indexDefinition.getKeyspace(), indexDefinition.getIndexName(),
|
||||
indexDefinition.valueTransformer().convert(value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.8
|
||||
*/
|
||||
static class GeoIndexedPropertyValueFactory implements IndexedDataFactory {
|
||||
|
||||
final GeoIndexDefinition indexDefinition;
|
||||
|
||||
public GeoIndexedPropertyValueFactory(GeoIndexDefinition indexDefinition) {
|
||||
this.indexDefinition = indexDefinition;
|
||||
}
|
||||
|
||||
public GeoIndexedPropertyValue createIndexedDataFor(Object value) {
|
||||
|
||||
return new GeoIndexedPropertyValue(indexDefinition.getKeyspace(), indexDefinition.getPath(),
|
||||
(Point) indexDefinition.valueTransformer().convert(value));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -210,7 +210,7 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean {
|
||||
|
||||
Object instance = instantiator.createInstance((RedisPersistentEntity<?>) entity,
|
||||
new PersistentEntityParameterValueProvider<KeyValuePersistentProperty>(entity,
|
||||
new ConverterAwareParameterValueProvider(source, conversionService), null));
|
||||
new ConverterAwareParameterValueProvider(path, source, conversionService), this.conversionService));
|
||||
|
||||
final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance);
|
||||
|
||||
@@ -259,14 +259,14 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean {
|
||||
|
||||
Bucket bucket = source.getBucket().extract(currentPath + ".");
|
||||
|
||||
RedisData source = new RedisData(bucket);
|
||||
RedisData newBucket = new RedisData(bucket);
|
||||
|
||||
byte[] type = bucket.get(currentPath + "." + TYPE_HINT_ALIAS);
|
||||
if (type != null && type.length > 0) {
|
||||
source.getBucket().put(TYPE_HINT_ALIAS, type);
|
||||
newBucket.getBucket().put(TYPE_HINT_ALIAS, type);
|
||||
}
|
||||
|
||||
accessor.setProperty(persistentProperty, readInternal(currentPath, targetType, source));
|
||||
accessor.setProperty(persistentProperty, readInternal(currentPath, targetType, newBucket));
|
||||
} else {
|
||||
|
||||
if (persistentProperty.isIdProperty() && StringUtils.isEmpty(path.isEmpty())) {
|
||||
@@ -602,8 +602,8 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean {
|
||||
writeAssociation(path, entity, value, sink);
|
||||
}
|
||||
|
||||
private void writeAssociation(final String path, final KeyValuePersistentEntity<?> entity,
|
||||
final Object value, final RedisData sink) {
|
||||
private void writeAssociation(final String path, final KeyValuePersistentEntity<?> entity, final Object value,
|
||||
final RedisData sink) {
|
||||
|
||||
if (value == null) {
|
||||
return;
|
||||
@@ -992,10 +992,13 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean {
|
||||
private static class ConverterAwareParameterValueProvider
|
||||
implements PropertyValueProvider<KeyValuePersistentProperty> {
|
||||
|
||||
private final String path;
|
||||
private final RedisData source;
|
||||
private final ConversionService conversionService;
|
||||
|
||||
public ConverterAwareParameterValueProvider(RedisData source, ConversionService conversionService) {
|
||||
public ConverterAwareParameterValueProvider(String path, RedisData source, ConversionService conversionService) {
|
||||
|
||||
this.path = path;
|
||||
this.source = source;
|
||||
this.conversionService = conversionService;
|
||||
}
|
||||
@@ -1003,7 +1006,9 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean {
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T getPropertyValue(KeyValuePersistentProperty property) {
|
||||
return (T) conversionService.convert(source.getBucket().get(property.getName()), property.getActualType());
|
||||
|
||||
String name = StringUtils.hasText(path) ? path + "." + property.getName() : property.getName();
|
||||
return (T) conversionService.convert(source.getBucket().get(name), property.getActualType());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,16 +15,22 @@
|
||||
*/
|
||||
package org.springframework.data.redis.core.convert;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.PropertyHandler;
|
||||
import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation;
|
||||
import org.springframework.data.redis.core.index.ConfigurableIndexDefinitionProvider;
|
||||
import org.springframework.data.redis.core.index.GeoIndexDefinition;
|
||||
import org.springframework.data.redis.core.index.GeoIndexed;
|
||||
import org.springframework.data.redis.core.index.IndexConfiguration;
|
||||
import org.springframework.data.redis.core.index.IndexDefinition;
|
||||
import org.springframework.data.redis.core.index.IndexDefinition.Condition;
|
||||
@@ -49,8 +55,12 @@ import org.springframework.util.CollectionUtils;
|
||||
*/
|
||||
public class PathIndexResolver implements IndexResolver {
|
||||
|
||||
private final Set<Class<?>> VALUE_TYPES = new HashSet<Class<?>>(
|
||||
Arrays.<Class<?>> asList(Point.class, GeoLocation.class));
|
||||
|
||||
private ConfigurableIndexDefinitionProvider indexConfiguration;
|
||||
private RedisMappingContext mappingContext;
|
||||
private IndexedDataFactoryProvider indexedDataFactoryProvider;
|
||||
|
||||
/**
|
||||
* Creates new {@link PathIndexResolver} with empty {@link IndexConfiguration}.
|
||||
@@ -69,6 +79,7 @@ public class PathIndexResolver implements IndexResolver {
|
||||
Assert.notNull(mappingContext, "MappingContext must not be null!");
|
||||
this.mappingContext = mappingContext;
|
||||
this.indexConfiguration = mappingContext.getMappingConfiguration().getIndexConfiguration();
|
||||
this.indexedDataFactoryProvider = new IndexedDataFactoryProvider();
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -94,7 +105,7 @@ public class PathIndexResolver implements IndexResolver {
|
||||
|
||||
RedisPersistentEntity<?> entity = mappingContext.getPersistentEntity(typeInformation);
|
||||
|
||||
if (entity == null) {
|
||||
if (entity == null || (value != null && VALUE_TYPES.contains(value.getClass()))) {
|
||||
return resolveIndex(keyspace, path, fallback, value);
|
||||
}
|
||||
|
||||
@@ -206,10 +217,11 @@ public class PathIndexResolver implements IndexResolver {
|
||||
|
||||
Object transformedValue = indexDefinition.valueTransformer().convert(value);
|
||||
|
||||
IndexedData indexedData = new SimpleIndexedPropertyValue(keyspace, indexDefinition.getIndexName(),
|
||||
transformedValue);
|
||||
IndexedData indexedData = null;
|
||||
if (transformedValue == null) {
|
||||
indexedData = new RemoveIndexedData(indexedData);
|
||||
} else {
|
||||
indexedData = indexedDataFactoryProvider.getIndexedDataFactory(indexDefinition).createIndexedDataFor(value);
|
||||
}
|
||||
data.add(indexedData);
|
||||
}
|
||||
@@ -220,7 +232,13 @@ public class PathIndexResolver implements IndexResolver {
|
||||
SimpleIndexDefinition indexDefinition = new SimpleIndexDefinition(keyspace, path);
|
||||
indexConfiguration.addIndexDefinition(indexDefinition);
|
||||
|
||||
data.add(new SimpleIndexedPropertyValue(keyspace, path, indexDefinition.valueTransformer().convert(value)));
|
||||
data.add(indexedDataFactoryProvider.getIndexedDataFactory(indexDefinition).createIndexedDataFor(value));
|
||||
} else if (property != null && property.isAnnotationPresent(GeoIndexed.class)) {
|
||||
|
||||
GeoIndexDefinition indexDefinition = new GeoIndexDefinition(keyspace, path);
|
||||
indexConfiguration.addIndexDefinition(indexDefinition);
|
||||
|
||||
data.add(indexedDataFactoryProvider.getIndexedDataFactory(indexDefinition).createIndexedDataFor(value));
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2016 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
|
||||
*
|
||||
* http://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.redis.core.index;
|
||||
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.8
|
||||
*/
|
||||
public class GeoIndexDefinition extends RedisIndexDefinition implements PathBasedRedisIndexDefinition {
|
||||
|
||||
/**
|
||||
* Creates new {@link GeoIndexDefinition}.
|
||||
*
|
||||
* @param keyspace must not be {@literal null}.
|
||||
* @param path
|
||||
*/
|
||||
public GeoIndexDefinition(String keyspace, String path) {
|
||||
this(keyspace, path, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new {@link GeoIndexDefinition}.
|
||||
*
|
||||
* @param keyspace must not be {@literal null}.
|
||||
* @param path
|
||||
* @param name must not be {@literal null}.
|
||||
*/
|
||||
public GeoIndexDefinition(String keyspace, String path, String name) {
|
||||
super(keyspace, path, name);
|
||||
addCondition(new PathCondition(path));
|
||||
setValueTransformer(new PointValueTransformer());
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.8
|
||||
*/
|
||||
static class PointValueTransformer implements IndexValueTransformer {
|
||||
|
||||
@Override
|
||||
public Point convert(Object source) {
|
||||
|
||||
if (source == null || source instanceof Point) {
|
||||
return (Point) source;
|
||||
}
|
||||
|
||||
if (source instanceof GeoLocation<?>) {
|
||||
return ((GeoLocation<?>) source).getPoint();
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Cannot convert %s to %s. GeoIndexed property needs to be of type Point or GeoLocation!",
|
||||
source.getClass(), Point.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2016 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
|
||||
*
|
||||
* http://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.redis.core.index;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Mark properties value to be included in a secondary index. <br />
|
||||
* Uses Redis {@literal GEO} structures for storage. <br />
|
||||
* The value will be part of the key built for the index.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.8
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
|
||||
public @interface GeoIndexed {
|
||||
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Mark properties value to be included in a secondary index. <br />
|
||||
* Uses Redos {@literal SET} for storage. <br />
|
||||
* Uses Redis {@literal SET} for storage. <br />
|
||||
* The value will be part of the key built for the index.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
|
||||
@@ -15,11 +15,15 @@
|
||||
*/
|
||||
package org.springframework.data.redis.repository.query;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
@@ -33,6 +37,8 @@ public class RedisOperationChain {
|
||||
private Set<PathAndValue> sismember = new LinkedHashSet<PathAndValue>();
|
||||
private Set<PathAndValue> orSismember = new LinkedHashSet<PathAndValue>();
|
||||
|
||||
private NearPath near;
|
||||
|
||||
public void sismember(String path, Object value) {
|
||||
sismember(new PathAndValue(path, value));
|
||||
}
|
||||
@@ -61,6 +67,14 @@ public class RedisOperationChain {
|
||||
return orSismember;
|
||||
}
|
||||
|
||||
public void near(NearPath near) {
|
||||
this.near = near;
|
||||
}
|
||||
|
||||
public NearPath getNear() {
|
||||
return near;
|
||||
}
|
||||
|
||||
public static class PathAndValue {
|
||||
|
||||
private final String path;
|
||||
@@ -128,4 +142,26 @@ public class RedisOperationChain {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.8
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public static class NearPath extends PathAndValue {
|
||||
|
||||
public NearPath(String path, Point point, Distance distance) {
|
||||
super(path, Arrays.<Object> asList(point, distance));
|
||||
}
|
||||
|
||||
public Point getPoint() {
|
||||
return (Point) getFirstValue();
|
||||
}
|
||||
|
||||
public Distance getDistance() {
|
||||
|
||||
Iterator<Object> it = values().iterator();
|
||||
it.next();
|
||||
return (Distance) it.next();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,8 +17,14 @@ package org.springframework.data.redis.repository.query;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.geo.Circle;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.Metrics;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
|
||||
import org.springframework.data.redis.repository.query.RedisOperationChain.NearPath;
|
||||
import org.springframework.data.repository.query.ParameterAccessor;
|
||||
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
|
||||
import org.springframework.data.repository.query.parser.Part;
|
||||
@@ -35,6 +41,7 @@ public class RedisQueryCreator extends AbstractQueryCreator<KeyValueQuery<RedisO
|
||||
|
||||
public RedisQueryCreator(PartTree tree, ParameterAccessor parameters) {
|
||||
super(tree, parameters);
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -52,6 +59,10 @@ public class RedisQueryCreator extends AbstractQueryCreator<KeyValueQuery<RedisO
|
||||
case SIMPLE_PROPERTY:
|
||||
sink.sismember(part.getProperty().toDotPath(), iterator.next());
|
||||
break;
|
||||
case WITHIN:
|
||||
case NEAR:
|
||||
sink.near(getNearPath(part, iterator));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(part.getType() + "is not supported for redis query derivation");
|
||||
}
|
||||
@@ -103,4 +114,41 @@ public class RedisQueryCreator extends AbstractQueryCreator<KeyValueQuery<RedisO
|
||||
return query;
|
||||
}
|
||||
|
||||
private NearPath getNearPath(Part part, Iterator<Object> iterator) {
|
||||
|
||||
Object o = iterator.next();
|
||||
|
||||
Point point = null;
|
||||
Distance distance = null;
|
||||
|
||||
if (o instanceof Circle) {
|
||||
|
||||
point = ((Circle) o).getCenter();
|
||||
distance = ((Circle) o).getRadius();
|
||||
} else if (o instanceof Point) {
|
||||
|
||||
point = (Point) o;
|
||||
|
||||
if (!iterator.hasNext()) {
|
||||
throw new InvalidDataAccessApiUsageException(
|
||||
"Expected to find distance value for geo query. Are you missing a parameter?");
|
||||
}
|
||||
|
||||
Object distObject = iterator.next();
|
||||
if (distObject instanceof Distance) {
|
||||
distance = (Distance) distObject;
|
||||
} else if (distObject instanceof Number) {
|
||||
distance = new Distance(((Number) distObject).doubleValue(), Metrics.KILOMETERS);
|
||||
} else {
|
||||
throw new InvalidDataAccessApiUsageException(String
|
||||
.format("Expected to find Distance or Numeric value for geo query but was %s.", distObject.getClass()));
|
||||
}
|
||||
} else {
|
||||
throw new InvalidDataAccessApiUsageException(
|
||||
String.format("Expected to find a Circle or Point/Distance for geo query but was %s.", o.getClass()));
|
||||
}
|
||||
|
||||
return new NearPath(part.getProperty().toDotPath(), point, distance);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.core.convert.support.GenericConversionService;
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.core.convert.GeoIndexedPropertyValue;
|
||||
import org.springframework.data.redis.core.convert.IndexedData;
|
||||
import org.springframework.data.redis.core.convert.MappingRedisConverter;
|
||||
import org.springframework.data.redis.core.convert.PathIndexResolver;
|
||||
@@ -215,6 +216,21 @@ public class IndexWriterUnitTests {
|
||||
verify(connectionMock, times(1)).sRem(any(byte[].class), eq(KEY_BIN));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-533
|
||||
*/
|
||||
@Test
|
||||
public void removeGeoIndexShouldCallGeoRemove() {
|
||||
|
||||
byte[] indexKey1 = "persons:location".getBytes(CHARSET);
|
||||
|
||||
when(connectionMock.keys(any(byte[].class))).thenReturn(new LinkedHashSet<byte[]>(Arrays.asList(indexKey1)));
|
||||
|
||||
writer.removeKeyFromExistingIndexes(KEY_BIN, new GeoIndexedPropertyValue(KEYSPACE, "address.city", null));
|
||||
|
||||
verify(connectionMock).geoRemove(indexKey1, KEY_BIN);
|
||||
}
|
||||
|
||||
static class StubIndxedData implements IndexedData {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.annotation.Reference;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.keyvalue.annotation.KeySpace;
|
||||
import org.springframework.data.redis.ConnectionFactoryTracker;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
@@ -45,6 +46,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactor
|
||||
import org.springframework.data.redis.core.convert.Bucket;
|
||||
import org.springframework.data.redis.core.convert.KeyspaceConfiguration;
|
||||
import org.springframework.data.redis.core.convert.MappingConfiguration;
|
||||
import org.springframework.data.redis.core.index.GeoIndexed;
|
||||
import org.springframework.data.redis.core.index.IndexConfiguration;
|
||||
import org.springframework.data.redis.core.index.Indexed;
|
||||
import org.springframework.data.redis.core.mapping.RedisMappingContext;
|
||||
@@ -510,6 +512,90 @@ public class RedisKeyValueAdapterTests {
|
||||
assertThat(template.opsForHash().hasKey("persons:1", "relatives.[stepfather].firstname"), is(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-533
|
||||
*/
|
||||
@Test
|
||||
public void putShouldCreateGeoIndexCorrectly() {
|
||||
|
||||
Person tam = new Person();
|
||||
tam.id = "1";
|
||||
tam.firstname = "tam";
|
||||
tam.address = new Address();
|
||||
tam.address.location = new Point(10, 20);
|
||||
|
||||
adapter.put("1", tam, "persons");
|
||||
|
||||
assertThat(template.opsForZSet().score("persons:address:location", "1"), is(notNullValue()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-533
|
||||
*/
|
||||
@Test
|
||||
public void deleteShouldRemoveGeoIndexCorrectly() {
|
||||
|
||||
Person tam = new Person();
|
||||
tam.id = "1";
|
||||
tam.firstname = "tam";
|
||||
tam.address = new Address();
|
||||
tam.address.location = new Point(10, 20);
|
||||
|
||||
adapter.put("1", tam, "persons");
|
||||
|
||||
adapter.delete("1", "persons", Person.class);
|
||||
|
||||
assertThat(template.opsForZSet().score("persons:address:location", "1"), is(nullValue()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-533
|
||||
*/
|
||||
@Test
|
||||
public void updateShouldAlterGeoIndexCorrectlyOnDelete() {
|
||||
|
||||
Person tam = new Person();
|
||||
tam.id = "1";
|
||||
tam.firstname = "tam";
|
||||
tam.address = new Address();
|
||||
tam.address.location = new Point(10, 20);
|
||||
|
||||
adapter.put("1", tam, "persons");
|
||||
|
||||
PartialUpdate<Person> update = new PartialUpdate<Person>("1", Person.class) //
|
||||
.del("address.location");
|
||||
|
||||
adapter.update(update);
|
||||
|
||||
assertThat(template.opsForZSet().score("persons:address:location", "1"), is(nullValue()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-533
|
||||
*/
|
||||
@Test
|
||||
public void updateShouldAlterGeoIndexCorrectlyOnUpdate() {
|
||||
|
||||
Person tam = new Person();
|
||||
tam.id = "1";
|
||||
tam.firstname = "tam";
|
||||
tam.address = new Address();
|
||||
tam.address.location = new Point(10, 20);
|
||||
|
||||
adapter.put("1", tam, "persons");
|
||||
|
||||
PartialUpdate<Person> update = new PartialUpdate<Person>("1", Person.class) //
|
||||
.set("address.location", new Point(17, 18));
|
||||
|
||||
adapter.update(update);
|
||||
|
||||
assertThat(template.opsForZSet().score("persons:address:location", "1"), is(notNullValue()));
|
||||
Point updatedLocation = template.opsForGeo().geoPos("persons:address:location", "1").iterator().next();
|
||||
|
||||
assertThat(updatedLocation.getX(), is(closeTo(17D, 0.005)));
|
||||
assertThat(updatedLocation.getY(), is(closeTo(18D, 0.005)));
|
||||
}
|
||||
|
||||
@KeySpace("persons")
|
||||
static class Person {
|
||||
|
||||
@@ -536,6 +622,8 @@ public class RedisKeyValueAdapterTests {
|
||||
|
||||
String city;
|
||||
@Indexed String country;
|
||||
@GeoIndexed Point location;
|
||||
|
||||
}
|
||||
|
||||
static class AddressWithId extends Address {
|
||||
|
||||
@@ -34,10 +34,13 @@ import java.util.Set;
|
||||
import org.hamcrest.core.IsCollectionContaining;
|
||||
import org.hamcrest.core.IsInstanceOf;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.redis.core.convert.ConversionTestEntities.Address;
|
||||
import org.springframework.data.redis.core.convert.ConversionTestEntities.AddressWithId;
|
||||
@@ -48,6 +51,7 @@ import org.springframework.data.redis.core.convert.ConversionTestEntities.Person
|
||||
import org.springframework.data.redis.core.convert.ConversionTestEntities.Size;
|
||||
import org.springframework.data.redis.core.convert.ConversionTestEntities.TaVeren;
|
||||
import org.springframework.data.redis.core.convert.ConversionTestEntities.TheWheelOfTime;
|
||||
import org.springframework.data.redis.core.index.GeoIndexed;
|
||||
import org.springframework.data.redis.core.index.IndexConfiguration;
|
||||
import org.springframework.data.redis.core.index.Indexed;
|
||||
import org.springframework.data.redis.core.index.SimpleIndexDefinition;
|
||||
@@ -61,6 +65,8 @@ import org.springframework.data.util.ClassTypeInformation;
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class PathIndexResolverUnitTests {
|
||||
|
||||
public @Rule ExpectedException exception = ExpectedException.none();
|
||||
|
||||
IndexConfiguration indexConfig;
|
||||
PathIndexResolver indexResolver;
|
||||
|
||||
@@ -514,6 +520,68 @@ public class PathIndexResolverUnitTests {
|
||||
new SimpleIndexedPropertyValue(IndexedOnPrimitiveArrayField.class.getName(), "values", 3)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-533
|
||||
*/
|
||||
@Test
|
||||
public void resolveGeoIndexShouldMapNameCorrectly() {
|
||||
|
||||
when(propertyMock.isMap()).thenReturn(true);
|
||||
when(propertyMock.isAnnotationPresent(eq(GeoIndexed.class))).thenReturn(true);
|
||||
when(propertyMock.findAnnotation(eq(GeoIndexed.class))).thenReturn(createGeoIndexedInstance());
|
||||
|
||||
IndexedData index = resolve("location", new Point(1D, 2D));
|
||||
|
||||
assertThat(index.getIndexName(), is("location"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-533
|
||||
*/
|
||||
@Test
|
||||
public void resolveGeoIndexShouldMapNameForNestedPropertyCorrectly() {
|
||||
|
||||
when(propertyMock.isMap()).thenReturn(true);
|
||||
when(propertyMock.isAnnotationPresent(eq(GeoIndexed.class))).thenReturn(true);
|
||||
when(propertyMock.findAnnotation(eq(GeoIndexed.class))).thenReturn(createGeoIndexedInstance());
|
||||
|
||||
IndexedData index = resolve("property.location", new Point(1D, 2D));
|
||||
|
||||
assertThat(index.getIndexName(), is("property:location"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-533
|
||||
*/
|
||||
@Test
|
||||
public void resolveGeoIndexOnPointField() {
|
||||
|
||||
GeoIndexedOnPoint source = new GeoIndexedOnPoint();
|
||||
source.location = new Point(1D, 2D);
|
||||
|
||||
Set<IndexedData> indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(GeoIndexedOnPoint.class),
|
||||
source);
|
||||
|
||||
assertThat(indexes.size(), is(1));
|
||||
assertThat(indexes, IsCollectionContaining.<IndexedData> hasItems(
|
||||
new GeoIndexedPropertyValue(GeoIndexedOnPoint.class.getName(), "location", source.location)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-533
|
||||
*/
|
||||
@Test
|
||||
public void resolveGeoIndexOnArrayFieldThrowsError() {
|
||||
|
||||
exception.expect(IllegalArgumentException.class);
|
||||
exception.expectMessage("GeoIndexed property needs to be of type Point or GeoLocation");
|
||||
|
||||
GeoIndexedOnArray source = new GeoIndexedOnArray();
|
||||
source.location = new double[] { 10D, 20D };
|
||||
|
||||
indexResolver.resolveIndexesFor(ClassTypeInformation.from(GeoIndexedOnArray.class), source);
|
||||
}
|
||||
|
||||
private IndexedData resolve(String path, Object value) {
|
||||
|
||||
Set<IndexedData> data = indexResolver.resolveIndex(KEYSPACE_PERSON, path, propertyMock, value);
|
||||
@@ -534,7 +602,16 @@ public class PathIndexResolverUnitTests {
|
||||
public Class<? extends Annotation> annotationType() {
|
||||
return Indexed.class;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private GeoIndexed createGeoIndexedInstance() {
|
||||
|
||||
return new GeoIndexed() {
|
||||
@Override
|
||||
public Class<? extends Annotation> annotationType() {
|
||||
return GeoIndexed.class;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -553,4 +630,12 @@ public class PathIndexResolverUnitTests {
|
||||
@Indexed Map<String, String> values;
|
||||
}
|
||||
|
||||
static class GeoIndexedOnPoint {
|
||||
@GeoIndexed Point location;
|
||||
}
|
||||
|
||||
static class GeoIndexedOnArray {
|
||||
@GeoIndexed double[] location;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -51,8 +51,8 @@ public class RedisRepositoryClusterIntegrationTests extends RedisRepositoryInteg
|
||||
|
||||
@Configuration
|
||||
@EnableRedisRepositories(considerNestedRepositories = true, indexConfiguration = MyIndexConfiguration.class,
|
||||
keyspaceConfiguration = MyKeyspaceConfiguration.class,
|
||||
includeFilters = { @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*PersonRepository") })
|
||||
keyspaceConfiguration = MyKeyspaceConfiguration.class, includeFilters = {
|
||||
@ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*PersonRepository|.*CityRepository") })
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -15,9 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.redis.repository;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.hamcrest.collection.IsCollectionWithSize.*;
|
||||
import static org.hamcrest.collection.IsIterableContainingInAnyOrder.*;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.Serializable;
|
||||
@@ -34,13 +32,18 @@ import org.springframework.data.annotation.Reference;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.Metrics;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.keyvalue.core.KeyValueTemplate;
|
||||
import org.springframework.data.redis.core.RedisHash;
|
||||
import org.springframework.data.redis.core.convert.KeyspaceConfiguration;
|
||||
import org.springframework.data.redis.core.index.GeoIndexed;
|
||||
import org.springframework.data.redis.core.index.IndexConfiguration;
|
||||
import org.springframework.data.redis.core.index.IndexDefinition;
|
||||
import org.springframework.data.redis.core.index.Indexed;
|
||||
import org.springframework.data.redis.core.index.SimpleIndexDefinition;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||
|
||||
/**
|
||||
@@ -52,6 +55,7 @@ import org.springframework.data.repository.PagingAndSortingRepository;
|
||||
public abstract class RedisRepositoryIntegrationTestBase {
|
||||
|
||||
@Autowired PersonRepository repo;
|
||||
@Autowired CityRepository cityRepo;
|
||||
@Autowired KeyValueTemplate kvTemplate;
|
||||
|
||||
@Before
|
||||
@@ -318,6 +322,74 @@ public abstract class RedisRepositoryIntegrationTestBase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-533
|
||||
*/
|
||||
@Test
|
||||
public void nearQueryShouldReturnResultsCorrectly() {
|
||||
|
||||
City palermo = new City();
|
||||
palermo.location = new Point(13.361389D, 38.115556D);
|
||||
|
||||
City catania = new City();
|
||||
catania.location = new Point(15.087269D, 37.502669D);
|
||||
|
||||
cityRepo.save(Arrays.asList(palermo, catania));
|
||||
|
||||
List<City> result = cityRepo.findByLocationNear(new Point(15D, 37D), new Distance(200, Metrics.KILOMETERS));
|
||||
assertThat(result, hasItems(palermo, catania));
|
||||
|
||||
result = cityRepo.findByLocationNear(new Point(15D, 37D), new Distance(100, Metrics.KILOMETERS));
|
||||
assertThat(result, hasItems(catania));
|
||||
assertThat(result, not(hasItems(palermo)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-533
|
||||
*/
|
||||
@Test
|
||||
public void nearQueryShouldFindNothingIfOutOfRange() {
|
||||
|
||||
City palermo = new City();
|
||||
palermo.location = new Point(13.361389D, 38.115556D);
|
||||
|
||||
City catania = new City();
|
||||
catania.location = new Point(15.087269D, 37.502669D);
|
||||
|
||||
cityRepo.save(Arrays.asList(palermo, catania));
|
||||
|
||||
List<City> result = cityRepo.findByLocationNear(new Point(15D, 37D), new Distance(10, Metrics.KILOMETERS));
|
||||
assertThat(result, is(empty()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-533
|
||||
*/
|
||||
@Test
|
||||
public void nearQueryShouldReturnResultsCorrectlyOnNestedProperty() {
|
||||
|
||||
City palermo = new City();
|
||||
palermo.location = new Point(13.361389D, 38.115556D);
|
||||
|
||||
City catania = new City();
|
||||
catania.location = new Point(15.087269D, 37.502669D);
|
||||
|
||||
Person p1 = new Person("foo", "bar");
|
||||
p1.hometown = palermo;
|
||||
|
||||
Person p2 = new Person("two", "two");
|
||||
p2.hometown = catania;
|
||||
|
||||
repo.save(Arrays.asList(p1, p2));
|
||||
|
||||
List<Person> result = repo.findByHometownLocationNear(new Point(15D, 37D), new Distance(200, Metrics.KILOMETERS));
|
||||
assertThat(result, hasItems(p1, p2));
|
||||
|
||||
result = repo.findByHometownLocationNear(new Point(15D, 37D), new Distance(100, Metrics.KILOMETERS));
|
||||
assertThat(result, hasItems(p2));
|
||||
assertThat(result, not(hasItems(p1)));
|
||||
}
|
||||
|
||||
public static interface PersonRepository extends PagingAndSortingRepository<Person, String> {
|
||||
|
||||
List<Person> findByFirstname(String firstname);
|
||||
@@ -337,6 +409,13 @@ public abstract class RedisRepositoryIntegrationTestBase {
|
||||
List<Person> findTop2ByLastname(String lastname);
|
||||
|
||||
Page<Person> findBy(Pageable page);
|
||||
|
||||
List<Person> findByHometownLocationNear(Point point, Distance distance);
|
||||
}
|
||||
|
||||
public static interface CityRepository extends CrudRepository<City, String> {
|
||||
|
||||
List<City> findByLocationNear(Point point, Distance distance);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -373,6 +452,7 @@ public abstract class RedisRepositoryIntegrationTestBase {
|
||||
@Indexed String firstname;
|
||||
String lastname;
|
||||
@Reference City city;
|
||||
City hometown;
|
||||
|
||||
public Person() {}
|
||||
|
||||
@@ -460,9 +540,12 @@ public abstract class RedisRepositoryIntegrationTestBase {
|
||||
}
|
||||
|
||||
public static class City {
|
||||
|
||||
@Id String id;
|
||||
String name;
|
||||
|
||||
@GeoIndexed Point location;
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
|
||||
@@ -35,8 +35,8 @@ public class RedisRepositoryIntegrationTests extends RedisRepositoryIntegrationT
|
||||
|
||||
@Configuration
|
||||
@EnableRedisRepositories(considerNestedRepositories = true, indexConfiguration = MyIndexConfiguration.class,
|
||||
keyspaceConfiguration = MyKeyspaceConfiguration.class,
|
||||
includeFilters = { @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*PersonRepository") })
|
||||
keyspaceConfiguration = MyKeyspaceConfiguration.class, includeFilters = {
|
||||
@ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*PersonRepository|.*CityRepository") })
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -16,15 +16,26 @@
|
||||
package org.springframework.data.redis.repository.query;
|
||||
|
||||
import static org.hamcrest.collection.IsCollectionWithSize.*;
|
||||
import static org.hamcrest.core.Is.*;
|
||||
import static org.hamcrest.core.IsCollectionContaining.*;
|
||||
import static org.hamcrest.core.IsNull.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.mockito.Mock;
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.data.geo.Box;
|
||||
import org.springframework.data.geo.Circle;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.geo.Metrics;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.geo.Shape;
|
||||
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
|
||||
import org.springframework.data.redis.core.convert.ConversionTestEntities;
|
||||
import org.springframework.data.redis.core.convert.ConversionTestEntities.Person;
|
||||
import org.springframework.data.redis.repository.query.RedisOperationChain.PathAndValue;
|
||||
import org.springframework.data.repository.Repository;
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
@@ -37,6 +48,8 @@ import org.springframework.data.repository.query.parser.PartTree;
|
||||
*/
|
||||
public class RedisQueryCreatorUnitTests {
|
||||
|
||||
public @Rule ExpectedException exception = ExpectedException.none();
|
||||
|
||||
private @Mock RepositoryMetadata metadataMock;
|
||||
|
||||
/**
|
||||
@@ -61,8 +74,8 @@ public class RedisQueryCreatorUnitTests {
|
||||
public void findByMultipleSimpleProperties() throws SecurityException, NoSuchMethodException {
|
||||
|
||||
RedisQueryCreator creator = createQueryCreatorForMethodWithArgs(
|
||||
SampleRepository.class.getMethod("findByFirstnameAndAge", String.class, Integer.class), new Object[] {
|
||||
"eddard", 43 });
|
||||
SampleRepository.class.getMethod("findByFirstnameAndAge", String.class, Integer.class),
|
||||
new Object[] { "eddard", 43 });
|
||||
|
||||
KeyValueQuery<RedisOperationChain> query = creator.createQuery();
|
||||
|
||||
@@ -78,8 +91,8 @@ public class RedisQueryCreatorUnitTests {
|
||||
public void findByMultipleSimplePropertiesUsingOr() throws SecurityException, NoSuchMethodException {
|
||||
|
||||
RedisQueryCreator creator = createQueryCreatorForMethodWithArgs(
|
||||
SampleRepository.class.getMethod("findByAgeOrFirstname", Integer.class, String.class), new Object[] { 43,
|
||||
"eddard" });
|
||||
SampleRepository.class.getMethod("findByAgeOrFirstname", Integer.class, String.class),
|
||||
new Object[] { 43, "eddard" });
|
||||
|
||||
KeyValueQuery<RedisOperationChain> query = creator.createQuery();
|
||||
|
||||
@@ -88,21 +101,127 @@ public class RedisQueryCreatorUnitTests {
|
||||
assertThat(query.getCritieria().getOrSismember(), hasItem(new PathAndValue("firstname", "eddard")));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-533
|
||||
*/
|
||||
@Test
|
||||
public void findWithinCircle() throws SecurityException, NoSuchMethodException {
|
||||
|
||||
RedisQueryCreator creator = createQueryCreatorForMethodWithArgs(
|
||||
SampleRepository.class.getMethod("findByLocationWithin", Circle.class),
|
||||
new Object[] { new Circle(new Point(1, 2), new Distance(200, Metrics.KILOMETERS)) });
|
||||
|
||||
KeyValueQuery<RedisOperationChain> query = creator.createQuery();
|
||||
|
||||
assertThat(query.getCritieria().getNear(), is(notNullValue()));
|
||||
assertThat(query.getCritieria().getNear().getPoint(), is(new Point(1, 2)));
|
||||
assertThat(query.getCritieria().getNear().getDistance(), is(new Distance(200, Metrics.KILOMETERS)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-533
|
||||
*/
|
||||
@Test
|
||||
public void findNearWithPointAndDistance() throws SecurityException, NoSuchMethodException {
|
||||
|
||||
RedisQueryCreator creator = createQueryCreatorForMethodWithArgs(
|
||||
SampleRepository.class.getMethod("findByLocationNear", Point.class, Distance.class),
|
||||
new Object[] { new Point(1, 2), new Distance(200, Metrics.KILOMETERS) });
|
||||
|
||||
KeyValueQuery<RedisOperationChain> query = creator.createQuery();
|
||||
|
||||
assertThat(query.getCritieria().getNear(), is(notNullValue()));
|
||||
assertThat(query.getCritieria().getNear().getPoint(), is(new Point(1, 2)));
|
||||
assertThat(query.getCritieria().getNear().getDistance(), is(new Distance(200, Metrics.KILOMETERS)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-533
|
||||
*/
|
||||
@Test
|
||||
public void findNearWithPointAndNumericValueDefaultsToKilometers() throws SecurityException, NoSuchMethodException {
|
||||
|
||||
RedisQueryCreator creator = createQueryCreatorForMethodWithArgs(
|
||||
SampleRepository.class.getMethod("findByLocationNear", Shape.class, Object.class),
|
||||
new Object[] { new Point(1, 2), 200F });
|
||||
|
||||
KeyValueQuery<RedisOperationChain> query = creator.createQuery();
|
||||
|
||||
assertThat(query.getCritieria().getNear(), is(notNullValue()));
|
||||
assertThat(query.getCritieria().getNear().getPoint(), is(new Point(1, 2)));
|
||||
assertThat(query.getCritieria().getNear().getDistance(), is(new Distance(200, Metrics.KILOMETERS)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-533
|
||||
*/
|
||||
@Test
|
||||
public void findNearWithInvalidShapeParameter() throws SecurityException, NoSuchMethodException {
|
||||
|
||||
exception.expect(InvalidDataAccessApiUsageException.class);
|
||||
exception.expectMessage("Expected to find a Circle or Point/Distance");
|
||||
|
||||
RedisQueryCreator creator = createQueryCreatorForMethodWithArgs(
|
||||
SampleRepository.class.getMethod("findByLocationNear", Shape.class, Object.class),
|
||||
new Object[] { new Box(new Point(0, 0), new Point(1, 1)), 200F });
|
||||
|
||||
creator.createQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-533
|
||||
*/
|
||||
@Test
|
||||
public void findNearWithInvalidDistanceParameter() throws SecurityException, NoSuchMethodException {
|
||||
|
||||
exception.expect(InvalidDataAccessApiUsageException.class);
|
||||
exception.expectMessage("Expected to find Distance or Numeric value");
|
||||
|
||||
RedisQueryCreator creator = createQueryCreatorForMethodWithArgs(
|
||||
SampleRepository.class.getMethod("findByLocationNear", Shape.class, Object.class),
|
||||
new Object[] { new Point(0, 0), "200" });
|
||||
|
||||
creator.createQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATAREDIS-533
|
||||
*/
|
||||
@Test
|
||||
public void findNearWithMissingDistanceParameter() throws SecurityException, NoSuchMethodException {
|
||||
|
||||
exception.expect(InvalidDataAccessApiUsageException.class);
|
||||
exception.expectMessage("Are you missing a parameter?");
|
||||
|
||||
RedisQueryCreator creator = createQueryCreatorForMethodWithArgs(
|
||||
SampleRepository.class.getMethod("findByLocationNear", Shape.class), new Object[] { new Point(0, 0) });
|
||||
|
||||
creator.createQuery();
|
||||
}
|
||||
|
||||
private RedisQueryCreator createQueryCreatorForMethodWithArgs(Method method, Object[] args) {
|
||||
|
||||
PartTree partTree = new PartTree(method.getName(), method.getReturnType());
|
||||
RedisQueryCreator creator = new RedisQueryCreator(partTree, new ParametersParameterAccessor(new DefaultParameters(
|
||||
method), args));
|
||||
RedisQueryCreator creator = new RedisQueryCreator(partTree,
|
||||
new ParametersParameterAccessor(new DefaultParameters(method), args));
|
||||
|
||||
return creator;
|
||||
}
|
||||
|
||||
private interface SampleRepository extends Repository<ConversionTestEntities.Person, String> {
|
||||
private interface SampleRepository extends Repository<Person, String> {
|
||||
|
||||
ConversionTestEntities.Person findByFirstname(String firstname);
|
||||
Person findByFirstname(String firstname);
|
||||
|
||||
ConversionTestEntities.Person findByFirstnameAndAge(String firstname, Integer age);
|
||||
Person findByFirstnameAndAge(String firstname, Integer age);
|
||||
|
||||
ConversionTestEntities.Person findByAgeOrFirstname(Integer age, String firstname);
|
||||
Person findByAgeOrFirstname(Integer age, String firstname);
|
||||
|
||||
Person findByLocationWithin(Circle circle);
|
||||
|
||||
Person findByLocationNear(Point point, Distance distance);
|
||||
|
||||
Person findByLocationNear(Shape point, Object distance);
|
||||
|
||||
Person findByLocationNear(Shape point);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user