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:
Christoph Strobl
2016-07-14 12:14:32 +02:00
committed by Mark Paluch
parent 5d09272876
commit 07d0b82100
21 changed files with 930 additions and 43 deletions

View File

@@ -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

View File

@@ -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);

View File

@@ -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()));
}

View File

@@ -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;
}
}
}
}

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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));
}
}
}

View File

@@ -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());
}
}

View File

@@ -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;

View File

@@ -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));
}
}
}

View File

@@ -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 {
}

View File

@@ -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

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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

View File

@@ -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);
}
}