Skip to content

Commit d387376

Browse files
committed
Support derived queries on repositories defined with an abstract entity class.
Motivation: Currently an abstract entity class specified in the repository definition can be used for the predicate typeKey = typeAlias(of abstract entity class) in queries. Since documents are stored with typeKey = typeAlias(of concrete class) those queries will never match any documents. To allow this to work, all of the abstract entity class an all concrete subclasses must use the same typeAlias. Once those documents are found, regardless of their concrete class, they will all have the same typeKey = typeAlias, instead of having the typeAlias specific to the concrete class. Additional information in the stored document is needed to identify the concrete class (subtype in the example test case), as well as a TypeMapper to interpret that information. Changes: This allows a common TypeAlias to be used for the purpose of the predicate typeKey = typeAlias, and the determination of the concrete type by implementing an AbstractingMappingCouchbaseConverter that inspects the 'subtype' property. Closes #1365.
1 parent d9327ce commit d387376

12 files changed

+317
-106
lines changed

src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java

+27-3
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,18 @@ public CouchbaseDocument encodeEntity(final Object entityToEncode) {
8787
public <T> T decodeEntity(String id, String source, long cas, Class<T> entityClass, String scope, String collection) {
8888
final CouchbaseDocument converted = new CouchbaseDocument(id);
8989
converted.setId(id);
90+
91+
// this is the entity class defined for the repository. It may not be the class of the document that was read
92+
// we will reset it after reading the document
93+
//
94+
// This will fail for the case where:
95+
// 1) The version is defined in the concrete class, but not in the abstract class; and
96+
// 2) The constructor takes a "long version" argument resulting in an exception would be thrown if version in
97+
// the source is null.
98+
// We could expose from the MappingCouchbaseConverter determining the persistent entity from the source,
99+
// but that is a lot of work to do every time just for this very rare and avoidable case.
100+
// TypeInformation<? extends R> typeToUse = typeMapper.readType(source, type);
101+
90102
CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(entityClass);
91103

92104
if (persistentEntity == null) { // method could return a Long, Boolean, String etc.
@@ -99,14 +111,26 @@ public <T> T decodeEntity(String id, String source, long cas, Class<T> entityCla
99111
return (T) set.iterator().next().getValue();
100112
}
101113

114+
CouchbaseDocument doc = (CouchbaseDocument) translationService.decode(source, converted);
115+
// if possible, set the version property in the source so that if the constructor has a long version argument,
116+
// it will have a value an not fail (as null is not a valid argument for a long argument). This possible failure
117+
// can be avoid by defining the argument as Long instead of long.
118+
// persistentEntity is still the (possibly abstract) class specified in the repository definition
119+
// it's possible that the abstract class does not have a version property, and this won't be able to set the version
102120
if (cas != 0 && persistentEntity.getVersionProperty() != null) {
103-
converted.put(persistentEntity.getVersionProperty().getName(), cas);
121+
doc.put(persistentEntity.getVersionProperty().getName(), cas);
104122
}
105123

106-
T readEntity = converter.read(entityClass, (CouchbaseDocument) translationService.decode(source, converted));
124+
// if the constructor has an argument that is long version, then it will fail if it is not available as 'null'
125+
// is not a legal value for a long.
126+
// Version doesn't come from
127+
// 'source', it comes from the cas argument to decodeEntity which is set later.
128+
T readEntity = converter.read(entityClass, doc);
107129
final ConvertingPropertyAccessor<T> accessor = getPropertyAccessor(readEntity);
108130

109-
if (persistentEntity.getVersionProperty() != null) {
131+
persistentEntity = couldBePersistentEntity(readEntity.getClass());
132+
133+
if (cas != 0 && persistentEntity.getVersionProperty() != null) {
110134
accessor.setProperty(persistentEntity.getVersionProperty(), cas);
111135
}
112136
N1qlJoinResolver.handleProperties(persistentEntity, accessor, template.reactive(), id, scope, collection);

src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentProperty.java

+4
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ public String getFieldName() {
7777
if (fieldName != null) {
7878
return fieldName;
7979
}
80+
if (getField() == null) { // use the name of the property - instead of getting an NPE trying to use field
81+
return fieldName = getName();
82+
}
83+
8084
Field annotationField = getField().getAnnotation(Field.class);
8185

8286
if (annotationField != null) {

src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.springframework.data.couchbase.core.support.TemplateUtils.SELECT_CAS;
2121
import static org.springframework.data.couchbase.core.support.TemplateUtils.SELECT_ID;
2222

23+
import java.lang.reflect.Modifier;
2324
import java.util.ArrayList;
2425
import java.util.Arrays;
2526
import java.util.Collection;
@@ -192,8 +193,8 @@ private String getProjectedOrDistinctFields(String b, Class resultClass, String
192193
if (distinctFields != null && distinctFields.length != 0) {
193194
return i(distinctFields).toString();
194195
}
195-
String projectedFields = i(b) + ".*";
196-
if (resultClass != null) {
196+
String projectedFields = i(b) + ".*"; // if we can't get further information of the fields needed project everything
197+
if (resultClass != null && !Modifier.isAbstract(resultClass.getModifiers())) {
197198
PersistentEntity persistentEntity = couchbaseConverter.getMappingContext().getPersistentEntity(resultClass);
198199
StringBuilder sb = new StringBuilder();
199200
getProjectedFieldsInternal(b, null, sb, persistentEntity, typeField, fields, distinctFields != null);
@@ -528,9 +529,8 @@ public N1qlSpelValues(String selectClause, String entityFields, String bucket, S
528529
}
529530

530531
// copied from StringN1qlBasedQuery
531-
private N1QLExpression getExpression(ParameterAccessor accessor,
532-
ReturnedType returnedType, SpelExpressionParser parser,
533-
QueryMethodEvaluationContextProvider evaluationContextProvider) {
532+
private N1QLExpression getExpression(ParameterAccessor accessor, ReturnedType returnedType,
533+
SpelExpressionParser parser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
534534
boolean isCountQuery = queryMethod.isCountQuery();
535535
Object[] runtimeParameters = getParameters(accessor);
536536
EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(queryMethod.getParameters(),

src/test/java/org/springframework/data/couchbase/core/CustomTypeKeyIntegrationTests.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,7 @@ void saveSimpleEntityCorrectlyWithDifferentTypeKey() {
6262
assertEquals(user, modified);
6363

6464
GetResult getResult = clientFactory.getCollection(null).get(user.getId());
65-
assertEquals("org.springframework.data.couchbase.domain.User",
66-
getResult.contentAsObject().getString(CUSTOM_TYPE_KEY));
65+
assertEquals("abstractuser", getResult.contentAsObject().getString(CUSTOM_TYPE_KEY));
6766
assertFalse(getResult.contentAsObject().containsKey(DefaultCouchbaseTypeMapper.DEFAULT_TYPE_KEY));
6867
operations.removeById(User.class).one(user.getId());
6968
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors
2+
* Copyright 2012-2022 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.
@@ -16,35 +16,21 @@
1616

1717
package org.springframework.data.couchbase.domain;
1818

19-
import java.util.Objects;
20-
21-
import org.springframework.data.annotation.CreatedBy;
22-
import org.springframework.data.annotation.CreatedDate;
2319
import org.springframework.data.annotation.Id;
24-
import org.springframework.data.annotation.LastModifiedBy;
25-
import org.springframework.data.annotation.LastModifiedDate;
26-
import org.springframework.data.annotation.PersistenceConstructor;
27-
import org.springframework.data.annotation.Transient;
28-
import org.springframework.data.annotation.Version;
29-
import org.springframework.data.couchbase.core.mapping.Document;
20+
import org.springframework.data.annotation.TypeAlias;
21+
import org.springframework.data.couchbase.core.mapping.Field;
3022

3123
/**
3224
* User entity for tests
3325
*
3426
* @author Michael Reiche
3527
*/
36-
28+
@TypeAlias(AbstractingTypeMapper.Type.ABSTRACTUSER)
3729
public abstract class AbstractUser extends ComparableEntity {
38-
39-
@Version protected long version;
4030
@Id protected String id;
4131
protected String firstname;
4232
protected String lastname;
43-
@Transient protected String transientInfo;
44-
@CreatedBy protected String createdBy;
45-
@CreatedDate protected long createdDate;
46-
@LastModifiedBy protected String lastModifiedBy;
47-
@LastModifiedDate protected long lastModifiedDate;
33+
@Field(AbstractingTypeMapper.SUBTYPE) protected String subtype;
4834

4935
public String getId() {
5036
return id;
@@ -53,53 +39,4 @@ public String getId() {
5339
public String getFirstname() {
5440
return firstname;
5541
}
56-
57-
public String getLastname() {
58-
return lastname;
59-
}
60-
61-
public long getCreatedDate() {
62-
return createdDate;
63-
}
64-
65-
public void setCreatedDate(long createdDate) {
66-
this.createdDate = createdDate;
67-
}
68-
69-
public String getCreatedBy() {
70-
return createdBy;
71-
}
72-
73-
public void setCreatedBy(String createdBy) {
74-
this.createdBy = createdBy;
75-
}
76-
77-
public long getLastModifiedDate() {
78-
return lastModifiedDate;
79-
}
80-
81-
public String getLastModifiedBy() {
82-
return lastModifiedBy;
83-
}
84-
85-
public long getVersion() {
86-
return version;
87-
}
88-
89-
public void setVersion(long version) {
90-
this.version = version;
91-
}
92-
93-
@Override
94-
public int hashCode() {
95-
return Objects.hash(getId(), firstname, lastname);
96-
}
97-
98-
public String getTransientInfo() {
99-
return transientInfo;
100-
}
101-
102-
public void setTransientInfo(String something) {
103-
transientInfo = something;
104-
}
10542
}

src/test/java/org/springframework/data/couchbase/domain/AbstractUserRepository.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 the original author or authors
2+
* Copyright 2012-2022 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.
@@ -19,28 +19,28 @@
1919
import java.util.List;
2020
import java.util.stream.Stream;
2121

22-
import com.couchbase.client.java.query.QueryScanConsistency;
2322
import org.springframework.data.couchbase.repository.CouchbaseRepository;
2423
import org.springframework.data.couchbase.repository.Query;
2524
import org.springframework.data.couchbase.repository.ScanConsistency;
2625
import org.springframework.data.repository.query.Param;
2726
import org.springframework.stereotype.Repository;
2827

2928
import com.couchbase.client.java.json.JsonArray;
29+
import com.couchbase.client.java.query.QueryScanConsistency;
3030

3131
/**
3232
* AbstractUser Repository for tests
3333
*
3434
* @author Michael Reiche
3535
*/
3636
@Repository
37-
@ScanConsistency(query=QueryScanConsistency.REQUEST_PLUS)
37+
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
3838
public interface AbstractUserRepository extends CouchbaseRepository<AbstractUser, String> {
3939

4040
@Query("#{#n1ql.selectEntity} where (meta().id = $1)")
4141
AbstractUser myFindById(String id);
4242

43-
List<User> findByFirstname(String firstname);
43+
List<AbstractUser> findByFirstname(String firstname);
4444

4545
Stream<User> findByLastname(String lastname);
4646

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2022 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+
* https://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.couchbase.domain;
17+
18+
import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter;
19+
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
20+
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
21+
import org.springframework.data.mapping.context.MappingContext;
22+
23+
/**
24+
* MappingConverter that uses AbstractTypeMapper
25+
*
26+
* @author Michael Reiche
27+
*/
28+
public class AbstractingMappingCouchbaseConverter extends MappingCouchbaseConverter {
29+
30+
/**
31+
* this constructer creates a TypeBasedCouchbaseTypeMapper with the specified typeKey while MappingCouchbaseConverter
32+
* uses a DefaultCouchbaseTypeMapper typeMapper = new DefaultCouchbaseTypeMapper(typeKey != null ? typeKey :
33+
* TYPEKEY_DEFAULT);
34+
*
35+
* @param mappingContext
36+
* @param typeKey - the typeKey to be used (normally "_class")
37+
*/
38+
public AbstractingMappingCouchbaseConverter(
39+
final MappingContext<? extends CouchbasePersistentEntity<?>, CouchbasePersistentProperty> mappingContext,
40+
final String typeKey) {
41+
super(mappingContext, typeKey);
42+
this.typeMapper = new AbstractingTypeMapper(typeKey);
43+
}
44+
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright 2022 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+
* https://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+
17+
package org.springframework.data.couchbase.domain;
18+
19+
import java.util.Collections;
20+
21+
import org.springframework.data.convert.DefaultTypeMapper;
22+
import org.springframework.data.convert.TypeAliasAccessor;
23+
import org.springframework.data.couchbase.core.convert.CouchbaseTypeMapper;
24+
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
25+
import org.springframework.data.mapping.Alias;
26+
import org.springframework.data.mapping.context.MappingContext;
27+
import org.springframework.data.util.TypeInformation;
28+
29+
/**
30+
* TypeMapper that leverages subtype
31+
*
32+
* @author Michael Reiche
33+
*/
34+
public class AbstractingTypeMapper extends DefaultTypeMapper<CouchbaseDocument> implements CouchbaseTypeMapper {
35+
36+
public static final String SUBTYPE = "subtype";
37+
private final String typeKey;
38+
39+
public static class Type {
40+
public static final String ABSTRACTUSER = "abstractuser", USER = "user", OTHERUSER = "otheruser";
41+
}
42+
43+
/**
44+
* Create a new type mapper with the type key.
45+
*
46+
* @param typeKey the typeKey to use.
47+
*/
48+
public AbstractingTypeMapper(final String typeKey) {
49+
super(new CouchbaseDocumentTypeAliasAccessor(typeKey), (MappingContext) null, Collections
50+
.singletonList(new org.springframework.data.couchbase.core.convert.TypeAwareTypeInformationMapper()));
51+
this.typeKey = typeKey;
52+
}
53+
54+
@Override
55+
public String getTypeKey() {
56+
return this.typeKey;
57+
}
58+
59+
public static final class CouchbaseDocumentTypeAliasAccessor implements TypeAliasAccessor<CouchbaseDocument> {
60+
61+
private final String typeKey;
62+
63+
public CouchbaseDocumentTypeAliasAccessor(final String typeKey) {
64+
this.typeKey = typeKey;
65+
}
66+
67+
@Override
68+
public Alias readAliasFrom(final CouchbaseDocument source) {
69+
String alias = (String) source.get(typeKey);
70+
if (Type.ABSTRACTUSER.equals(alias)) {
71+
String subtype = (String) source.get(AbstractingTypeMapper.SUBTYPE);
72+
if (Type.OTHERUSER.equals(subtype)) {
73+
alias = OtherUser.class.getName();
74+
} else if (Type.USER.equals(subtype)) {
75+
alias = User.class.getName();
76+
} else {
77+
throw new RuntimeException(
78+
"no mapping for type " + SUBTYPE + "=" + subtype + " in type " + alias + " source=" + source);
79+
}
80+
}
81+
return Alias.ofNullable(alias);
82+
}
83+
84+
@Override
85+
public void writeTypeTo(final CouchbaseDocument sink, final Object alias) {
86+
if (typeKey != null) {
87+
sink.put(typeKey, alias);
88+
}
89+
}
90+
}
91+
92+
@Override
93+
public Alias getTypeAlias(TypeInformation<?> info) {
94+
return getAliasFor(info);
95+
}
96+
97+
}

0 commit comments

Comments
 (0)