Skip to content

Commit 9b5b455

Browse files
DATAREDIS-425 - Allow alternative id lookup and fail fast when no id found.
Allow fields named "id" to be considered as identifier property even when no explicit "@id" annotation is present. Favor fields with explicit id annotation over others and throw MappingException when ambiguous id declaration is found. Additionally fail fast when repository interfaces reference types that do not declare an id property.
1 parent 384cfb2 commit 9b5b455

File tree

8 files changed

+348
-2
lines changed

8 files changed

+348
-2
lines changed

src/main/asciidoc/reference/redis-repositories.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public class Person {
2828
We have a pretty simple domain object here. Note that it has a property named `id` annotated with `org.springframework.data.annotation.Id` and a `@RedisHash` annotation on its type.
2929
Those two are responsible for creating the actual key used to persist the hash.
3030

31+
NOTE: Properties annotated with `@Id` as well as those named `id` are considered as the identifier properties. Those with the annotation are favored over others.
32+
3133
To now actually have a component responsible for storage and retrieval we need to define a repository interface.
3234

3335
.Basic Repository Interface To Persist Person Entities

src/main/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntity.java

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
*/
1616
package org.springframework.data.redis.core.mapping;
1717

18+
import org.springframework.data.annotation.Id;
1819
import org.springframework.data.keyvalue.core.mapping.BasicKeyValuePersistentEntity;
1920
import org.springframework.data.keyvalue.core.mapping.KeySpaceResolver;
21+
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty;
22+
import org.springframework.data.mapping.model.MappingException;
2023
import org.springframework.data.redis.core.TimeToLiveAccessor;
2124
import org.springframework.data.util.TypeInformation;
2225
import org.springframework.util.Assert;
@@ -27,7 +30,8 @@
2730
* @author Christoph Strobl
2831
* @param <T>
2932
*/
30-
public class BasicRedisPersistentEntity<T> extends BasicKeyValuePersistentEntity<T> implements RedisPersistentEntity<T> {
33+
public class BasicRedisPersistentEntity<T> extends BasicKeyValuePersistentEntity<T>
34+
implements RedisPersistentEntity<T> {
3135

3236
private TimeToLiveAccessor timeToLiveAccessor;
3337

@@ -55,4 +59,47 @@ public TimeToLiveAccessor getTimeToLiveAccessor() {
5559
return this.timeToLiveAccessor;
5660
}
5761

62+
/*
63+
* (non-Javadoc)
64+
* @see org.springframework.data.mapping.model.BasicPersistentEntity#returnPropertyIfBetterIdPropertyCandidateOrNull(org.springframework.data.mapping.PersistentProperty)
65+
*/
66+
@Override
67+
protected KeyValuePersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNull(
68+
KeyValuePersistentProperty property) {
69+
70+
Assert.notNull(property);
71+
72+
if (!property.isIdProperty()) {
73+
return null;
74+
}
75+
76+
KeyValuePersistentProperty currentIdProperty = getIdProperty();
77+
boolean currentIdPropertyIsSet = currentIdProperty != null;
78+
79+
if (!currentIdPropertyIsSet) {
80+
return property;
81+
}
82+
83+
boolean currentIdPropertyIsExplicit = currentIdProperty.isAnnotationPresent(Id.class);
84+
boolean newIdPropertyIsExplicit = property.isAnnotationPresent(Id.class);
85+
86+
if (currentIdPropertyIsExplicit && newIdPropertyIsExplicit) {
87+
throw new MappingException(String.format(
88+
"Attempt to add explicit id property %s but already have an property %s registered "
89+
+ "as explicit id. Check your mapping configuration!",
90+
property.getField(), currentIdProperty.getField()));
91+
}
92+
93+
if (!currentIdPropertyIsExplicit && !newIdPropertyIsExplicit) {
94+
throw new MappingException(
95+
String.format("Attempt to add id property %s but already have an property %s registered "
96+
+ "as id. Check your mapping configuration!", property.getField(), currentIdProperty.getField()));
97+
}
98+
99+
if (newIdPropertyIsExplicit) {
100+
return property;
101+
}
102+
103+
return null;
104+
}
58105
}

src/main/java/org/springframework/data/redis/core/mapping/RedisPersistentProperty.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2015 the original author or authors.
2+
* Copyright 2015-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,8 @@
1717

1818
import java.beans.PropertyDescriptor;
1919
import java.lang.reflect.Field;
20+
import java.util.HashSet;
21+
import java.util.Set;
2022

2123
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty;
2224
import org.springframework.data.mapping.PersistentEntity;
@@ -31,6 +33,12 @@
3133
*/
3234
public class RedisPersistentProperty extends KeyValuePersistentProperty {
3335

36+
private static final Set<String> SUPPORTED_ID_PROPERTY_NAMES = new HashSet<String>();
37+
38+
static {
39+
SUPPORTED_ID_PROPERTY_NAMES.add("id");
40+
}
41+
3442
/**
3543
* Creates new {@link RedisPersistentProperty}.
3644
*
@@ -44,4 +52,17 @@ public RedisPersistentProperty(Field field, PropertyDescriptor propertyDescripto
4452
super(field, propertyDescriptor, owner, simpleTypeHolder);
4553
}
4654

55+
/*
56+
* (non-Javadoc)
57+
* @see org.springframework.data.mapping.model.AnnotationBasedPersistentProperty#isIdProperty()
58+
*/
59+
@Override
60+
public boolean isIdProperty() {
61+
62+
if (super.isIdProperty()) {
63+
return true;
64+
}
65+
66+
return SUPPORTED_ID_PROPERTY_NAMES.contains(getName());
67+
}
4768
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.redis.repository.core;
17+
18+
import java.io.Serializable;
19+
20+
import org.springframework.data.mapping.model.MappingException;
21+
import org.springframework.data.redis.core.mapping.RedisPersistentEntity;
22+
import org.springframework.data.repository.core.support.PersistentEntityInformation;
23+
24+
/**
25+
* {@link RedisEntityInformation} implementation using a {@link MongoPersistentEntity} instance to lookup the necessary
26+
* information. Can be configured with a custom collection to be returned which will trump the one returned by the
27+
* {@link MongoPersistentEntity} if given.
28+
*
29+
* @author Christoph Strobl
30+
* @param <T>
31+
* @param <ID>
32+
*/
33+
public class MappingRedisEntityInformation<T, ID extends Serializable>
34+
extends PersistentEntityInformation<T, Serializable> implements RedisEntityInformation<T, Serializable> {
35+
36+
private final RedisPersistentEntity<T> entityMetadata;
37+
38+
/**
39+
* @param entity
40+
*/
41+
public MappingRedisEntityInformation(RedisPersistentEntity<T> entity) {
42+
super(entity);
43+
44+
this.entityMetadata = entity;
45+
46+
if (!entityMetadata.hasIdProperty()) {
47+
48+
throw new MappingException(
49+
String.format("Entity %s requires to have an explicit id field. Did you forget to provide one using @Id?",
50+
entity.getName()));
51+
}
52+
}
53+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.redis.repository.core;
17+
18+
import java.io.Serializable;
19+
20+
import org.springframework.data.repository.core.EntityInformation;
21+
22+
/**
23+
* @author Christoph Strobl
24+
* @param <T>
25+
* @param <ID>
26+
*/
27+
public interface RedisEntityInformation<T, ID extends Serializable> extends EntityInformation<T, ID> {
28+
29+
}

src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactory.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,17 @@
1515
*/
1616
package org.springframework.data.redis.repository.support;
1717

18+
import java.io.Serializable;
1819
import java.lang.reflect.Method;
1920

2021
import org.springframework.data.keyvalue.core.KeyValueOperations;
2122
import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery;
2223
import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery.QueryInitialization;
2324
import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory;
2425
import org.springframework.data.projection.ProjectionFactory;
26+
import org.springframework.data.redis.core.mapping.RedisPersistentEntity;
27+
import org.springframework.data.redis.repository.core.MappingRedisEntityInformation;
28+
import org.springframework.data.repository.core.EntityInformation;
2529
import org.springframework.data.repository.core.NamedQueries;
2630
import org.springframework.data.repository.core.RepositoryMetadata;
2731
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
@@ -69,6 +73,21 @@ protected QueryLookupStrategy getQueryLookupStrategy(Key key, EvaluationContextP
6973
return new RedisQueryLookupStrategy(key, evaluationContextProvider, getKeyValueOperations(), getQueryCreator());
7074
}
7175

76+
/*
77+
* (non-Javadoc)
78+
* @see org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory#getEntityInformation(java.lang.Class)
79+
*/
80+
@Override
81+
@SuppressWarnings("unchecked")
82+
public <T, ID extends Serializable> EntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
83+
84+
RedisPersistentEntity<T> entity = (RedisPersistentEntity<T>) getMappingContext().getPersistentEntity(domainClass);
85+
EntityInformation<T, ID> entityInformation = (EntityInformation<T, ID>) new MappingRedisEntityInformation<T, ID>(
86+
entity);
87+
88+
return entityInformation;
89+
}
90+
7291
/**
7392
* @author Christoph Strobl
7493
* @since 1.7
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Copyright 2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.redis.core.mapping;
17+
18+
import static org.hamcrest.core.Is.*;
19+
import static org.hamcrest.core.IsEqual.*;
20+
import static org.junit.Assert.*;
21+
import static org.mockito.Matchers.*;
22+
import static org.mockito.Mockito.*;
23+
24+
import java.io.Serializable;
25+
26+
import org.junit.Before;
27+
import org.junit.Rule;
28+
import org.junit.Test;
29+
import org.junit.rules.ExpectedException;
30+
import org.junit.runner.RunWith;
31+
import org.mockito.Mock;
32+
import org.mockito.runners.MockitoJUnitRunner;
33+
import org.springframework.data.keyvalue.core.mapping.KeySpaceResolver;
34+
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty;
35+
import org.springframework.data.mapping.model.MappingException;
36+
import org.springframework.data.redis.core.TimeToLiveAccessor;
37+
import org.springframework.data.redis.core.convert.ConversionTestEntities;
38+
import org.springframework.data.util.TypeInformation;
39+
40+
/**
41+
* @author Christoph Strobl
42+
* @param <T>
43+
* @param <ID>
44+
*/
45+
@RunWith(MockitoJUnitRunner.class)
46+
public class BasicRedisPersistentEntityUnitTests<T, ID extends Serializable> {
47+
48+
public @Rule ExpectedException expectedException = ExpectedException.none();
49+
50+
@Mock TypeInformation<T> entityInformation;
51+
@Mock KeySpaceResolver keySpaceResolver;
52+
@Mock TimeToLiveAccessor ttlAccessor;
53+
54+
BasicRedisPersistentEntity<T> entity;
55+
56+
@Before
57+
@SuppressWarnings("unchecked")
58+
public void setUp() {
59+
60+
when(entityInformation.getType()).thenReturn((Class<T>) ConversionTestEntities.Person.class);
61+
entity = new BasicRedisPersistentEntity<T>(entityInformation, keySpaceResolver, ttlAccessor);
62+
}
63+
64+
/**
65+
* @see DATAREDIS-425
66+
*/
67+
@Test
68+
public void addingMultipleIdPropertiesWithoutAnExplicitOneThrowsException() {
69+
70+
expectedException.expect(MappingException.class);
71+
expectedException.expectMessage("Attempt to add id property");
72+
expectedException.expectMessage("but already have an property");
73+
74+
KeyValuePersistentProperty property1 = mock(RedisPersistentProperty.class);
75+
when(property1.isIdProperty()).thenReturn(true);
76+
77+
KeyValuePersistentProperty property2 = mock(RedisPersistentProperty.class);
78+
when(property2.isIdProperty()).thenReturn(true);
79+
80+
entity.addPersistentProperty(property1);
81+
entity.addPersistentProperty(property2);
82+
}
83+
84+
/**
85+
* @see DATAREDIS-425
86+
*/
87+
@Test
88+
@SuppressWarnings("unchecked")
89+
public void addingMultipleExplicitIdPropertiesThrowsException() {
90+
91+
expectedException.expect(MappingException.class);
92+
expectedException.expectMessage("Attempt to add explicit id property");
93+
expectedException.expectMessage("but already have an property");
94+
95+
KeyValuePersistentProperty property1 = mock(RedisPersistentProperty.class);
96+
when(property1.isIdProperty()).thenReturn(true);
97+
when(property1.isAnnotationPresent(any(Class.class))).thenReturn(true);
98+
99+
KeyValuePersistentProperty property2 = mock(RedisPersistentProperty.class);
100+
when(property2.isIdProperty()).thenReturn(true);
101+
when(property2.isAnnotationPresent(any(Class.class))).thenReturn(true);
102+
103+
entity.addPersistentProperty(property1);
104+
entity.addPersistentProperty(property2);
105+
}
106+
107+
/**
108+
* @see DATAREDIS-425
109+
*/
110+
@Test
111+
@SuppressWarnings("unchecked")
112+
public void explicitIdPropertiyShouldBeFavoredOverNonExplicit() {
113+
114+
KeyValuePersistentProperty property1 = mock(RedisPersistentProperty.class);
115+
when(property1.isIdProperty()).thenReturn(true);
116+
117+
KeyValuePersistentProperty property2 = mock(RedisPersistentProperty.class);
118+
when(property2.isIdProperty()).thenReturn(true);
119+
when(property2.isAnnotationPresent(any(Class.class))).thenReturn(true);
120+
121+
entity.addPersistentProperty(property1);
122+
entity.addPersistentProperty(property2);
123+
124+
assertThat(entity.getIdProperty(), is(equalTo(property2)));
125+
}
126+
}

0 commit comments

Comments
 (0)