From 5889f2b05cd142782430fb6ce2f8939cea93c48f Mon Sep 17 00:00:00 2001 From: Michael Reiche Date: Thu, 10 Mar 2022 16:02:42 -0800 Subject: [PATCH] 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. --- .../core/CouchbaseTemplateSupport.java | 24 ++++- .../BasicCouchbasePersistentProperty.java | 4 + .../query/StringBasedN1qlQueryParser.java | 10 +- .../core/CustomTypeKeyIntegrationTests.java | 3 +- .../data/couchbase/domain/AbstractUser.java | 73 +------------- .../domain/AbstractUserRepository.java | 8 +- .../AbstractingMappingCouchbaseConverter.java | 45 +++++++++ .../domain/AbstractingTypeMapper.java | 97 +++++++++++++++++++ .../data/couchbase/domain/OtherUser.java | 3 + .../data/couchbase/domain/User.java | 72 +++++++++++++- ...aseAbstractRepositoryIntegrationTests.java | 74 ++++++++++---- .../StringN1qlQueryCreatorMockedTests.java | 4 +- 12 files changed, 313 insertions(+), 104 deletions(-) create mode 100644 src/test/java/org/springframework/data/couchbase/domain/AbstractingMappingCouchbaseConverter.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/AbstractingTypeMapper.java diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java index 07087759c..b86806abb 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java @@ -87,6 +87,18 @@ public CouchbaseDocument encodeEntity(final Object entityToEncode) { public T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection) { final CouchbaseDocument converted = new CouchbaseDocument(id); converted.setId(id); + + // this is the entity class defined for the repository. It may not be the class of the document that was read + // we will reset it after reading the document + // + // This will fail for the case where: + // 1) The version is defined in the concrete class, but not in the abstract class; and + // 2) The constructor takes a "long version" argument resulting in an exception would be thrown if version in + // the source is null. + // We could expose from the MappingCouchbaseConverter determining the persistent entity from the source, + // but that is a lot of work to do every time just for this very rare and avoidable case. + // TypeInformation typeToUse = typeMapper.readType(source, type); + CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(entityClass); if (persistentEntity == null) { // method could return a Long, Boolean, String etc. @@ -99,14 +111,24 @@ public T decodeEntity(String id, String source, long cas, Class entityCla return (T) set.iterator().next().getValue(); } + // if possible, set the version property in the source so that if the constructor has a long version argument, + // it will have a value an not fail (as null is not a valid argument for a long argument). This possible failure + // can be avoid by defining the argument as Long instead of long. + // persistentEntity is still the (possibly abstract) class specified in the repository definition + // it's possible that the abstract class does not have a version property, and this won't be able to set the version if (cas != 0 && persistentEntity.getVersionProperty() != null) { converted.put(persistentEntity.getVersionProperty().getName(), cas); } + // if the constructor has an argument that is long version, then construction will fail if the 'version' + // is not available as 'null' is not a legal value for a long. Changing the arg to "Long version" would solve this. + // (Version doesn't come from 'source', it comes from the cas argument to decodeEntity) T readEntity = converter.read(entityClass, (CouchbaseDocument) translationService.decode(source, converted)); final ConvertingPropertyAccessor accessor = getPropertyAccessor(readEntity); - if (persistentEntity.getVersionProperty() != null) { + persistentEntity = couldBePersistentEntity(readEntity.getClass()); + + if (cas != 0 && persistentEntity.getVersionProperty() != null) { accessor.setProperty(persistentEntity.getVersionProperty(), cas); } N1qlJoinResolver.handleProperties(persistentEntity, accessor, template.reactive(), id, scope, collection); diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentProperty.java b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentProperty.java index 9d1d0b686..53c2561ef 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentProperty.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentProperty.java @@ -77,6 +77,10 @@ public String getFieldName() { if (fieldName != null) { return fieldName; } + if (getField() == null) { // use the name of the property - instead of getting an NPE trying to use field + return fieldName = getName(); + } + Field annotationField = getField().getAnnotation(Field.class); if (annotationField != null) { diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java b/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java index e9790c77b..d8140e89a 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java @@ -20,6 +20,7 @@ import static org.springframework.data.couchbase.core.support.TemplateUtils.SELECT_CAS; import static org.springframework.data.couchbase.core.support.TemplateUtils.SELECT_ID; +import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -192,8 +193,8 @@ private String getProjectedOrDistinctFields(String b, Class resultClass, String if (distinctFields != null && distinctFields.length != 0) { return i(distinctFields).toString(); } - String projectedFields = i(b) + ".*"; - if (resultClass != null) { + String projectedFields = i(b) + ".*"; // if we can't get further information of the fields needed project everything + if (resultClass != null && !Modifier.isAbstract(resultClass.getModifiers())) { PersistentEntity persistentEntity = couchbaseConverter.getMappingContext().getPersistentEntity(resultClass); StringBuilder sb = new StringBuilder(); getProjectedFieldsInternal(b, null, sb, persistentEntity, typeField, fields, distinctFields != null); @@ -528,9 +529,8 @@ public N1qlSpelValues(String selectClause, String entityFields, String bucket, S } // copied from StringN1qlBasedQuery - private N1QLExpression getExpression(ParameterAccessor accessor, - ReturnedType returnedType, SpelExpressionParser parser, - QueryMethodEvaluationContextProvider evaluationContextProvider) { + private N1QLExpression getExpression(ParameterAccessor accessor, ReturnedType returnedType, + SpelExpressionParser parser, QueryMethodEvaluationContextProvider evaluationContextProvider) { boolean isCountQuery = queryMethod.isCountQuery(); Object[] runtimeParameters = getParameters(accessor); EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(queryMethod.getParameters(), diff --git a/src/test/java/org/springframework/data/couchbase/core/CustomTypeKeyIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CustomTypeKeyIntegrationTests.java index 77a55c3c5..2ca6bb982 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CustomTypeKeyIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CustomTypeKeyIntegrationTests.java @@ -62,8 +62,7 @@ void saveSimpleEntityCorrectlyWithDifferentTypeKey() { assertEquals(user, modified); GetResult getResult = clientFactory.getCollection(null).get(user.getId()); - assertEquals("org.springframework.data.couchbase.domain.User", - getResult.contentAsObject().getString(CUSTOM_TYPE_KEY)); + assertEquals("abstractuser", getResult.contentAsObject().getString(CUSTOM_TYPE_KEY)); assertFalse(getResult.contentAsObject().containsKey(DefaultCouchbaseTypeMapper.DEFAULT_TYPE_KEY)); operations.removeById(User.class).one(user.getId()); } diff --git a/src/test/java/org/springframework/data/couchbase/domain/AbstractUser.java b/src/test/java/org/springframework/data/couchbase/domain/AbstractUser.java index 00d106d15..94e41aa82 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AbstractUser.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AbstractUser.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors + * Copyright 2012-2022 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. @@ -16,35 +16,21 @@ package org.springframework.data.couchbase.domain; -import java.util.Objects; - -import org.springframework.data.annotation.CreatedBy; -import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.LastModifiedBy; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.annotation.PersistenceConstructor; -import org.springframework.data.annotation.Transient; -import org.springframework.data.annotation.Version; -import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.data.annotation.TypeAlias; +import org.springframework.data.couchbase.core.mapping.Field; /** * User entity for tests * * @author Michael Reiche */ - +@TypeAlias(AbstractingTypeMapper.Type.ABSTRACTUSER) public abstract class AbstractUser extends ComparableEntity { - - @Version protected long version; @Id protected String id; protected String firstname; protected String lastname; - @Transient protected String transientInfo; - @CreatedBy protected String createdBy; - @CreatedDate protected long createdDate; - @LastModifiedBy protected String lastModifiedBy; - @LastModifiedDate protected long lastModifiedDate; + @Field(AbstractingTypeMapper.SUBTYPE) protected String subtype; public String getId() { return id; @@ -53,53 +39,4 @@ public String getId() { public String getFirstname() { return firstname; } - - public String getLastname() { - return lastname; - } - - public long getCreatedDate() { - return createdDate; - } - - public void setCreatedDate(long createdDate) { - this.createdDate = createdDate; - } - - public String getCreatedBy() { - return createdBy; - } - - public void setCreatedBy(String createdBy) { - this.createdBy = createdBy; - } - - public long getLastModifiedDate() { - return lastModifiedDate; - } - - public String getLastModifiedBy() { - return lastModifiedBy; - } - - public long getVersion() { - return version; - } - - public void setVersion(long version) { - this.version = version; - } - - @Override - public int hashCode() { - return Objects.hash(getId(), firstname, lastname); - } - - public String getTransientInfo() { - return transientInfo; - } - - public void setTransientInfo(String something) { - transientInfo = something; - } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/AbstractUserRepository.java b/src/test/java/org/springframework/data/couchbase/domain/AbstractUserRepository.java index 5b6f3f85b..1fa94d7e3 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AbstractUserRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AbstractUserRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors + * Copyright 2012-2022 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. @@ -19,7 +19,6 @@ import java.util.List; import java.util.stream.Stream; -import com.couchbase.client.java.query.QueryScanConsistency; import org.springframework.data.couchbase.repository.CouchbaseRepository; import org.springframework.data.couchbase.repository.Query; import org.springframework.data.couchbase.repository.ScanConsistency; @@ -27,6 +26,7 @@ import org.springframework.stereotype.Repository; import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.query.QueryScanConsistency; /** * AbstractUser Repository for tests @@ -34,13 +34,13 @@ * @author Michael Reiche */ @Repository -@ScanConsistency(query=QueryScanConsistency.REQUEST_PLUS) +@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) public interface AbstractUserRepository extends CouchbaseRepository { @Query("#{#n1ql.selectEntity} where (meta().id = $1)") AbstractUser myFindById(String id); - List findByFirstname(String firstname); + List findByFirstname(String firstname); Stream findByLastname(String lastname); diff --git a/src/test/java/org/springframework/data/couchbase/domain/AbstractingMappingCouchbaseConverter.java b/src/test/java/org/springframework/data/couchbase/domain/AbstractingMappingCouchbaseConverter.java new file mode 100644 index 000000000..bde87cf29 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/AbstractingMappingCouchbaseConverter.java @@ -0,0 +1,45 @@ +/* + * Copyright 2022 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.domain; + +import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.mapping.context.MappingContext; + +/** + * MappingConverter that uses AbstractTypeMapper + * + * @author Michael Reiche + */ +public class AbstractingMappingCouchbaseConverter extends MappingCouchbaseConverter { + + /** + * this constructer creates a TypeBasedCouchbaseTypeMapper with the specified typeKey while MappingCouchbaseConverter + * uses a DefaultCouchbaseTypeMapper typeMapper = new DefaultCouchbaseTypeMapper(typeKey != null ? typeKey : + * TYPEKEY_DEFAULT); + * + * @param mappingContext + * @param typeKey - the typeKey to be used (normally "_class") + */ + public AbstractingMappingCouchbaseConverter( + final MappingContext, CouchbasePersistentProperty> mappingContext, + final String typeKey) { + super(mappingContext, typeKey); + this.typeMapper = new AbstractingTypeMapper(typeKey); + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/AbstractingTypeMapper.java b/src/test/java/org/springframework/data/couchbase/domain/AbstractingTypeMapper.java new file mode 100644 index 000000000..ea9314490 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/AbstractingTypeMapper.java @@ -0,0 +1,97 @@ +/* + * Copyright 2022 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.domain; + +import java.util.Collections; + +import org.springframework.data.convert.DefaultTypeMapper; +import org.springframework.data.convert.TypeAliasAccessor; +import org.springframework.data.couchbase.core.convert.CouchbaseTypeMapper; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.mapping.Alias; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.util.TypeInformation; + +/** + * TypeMapper that leverages subtype + * + * @author Michael Reiche + */ +public class AbstractingTypeMapper extends DefaultTypeMapper implements CouchbaseTypeMapper { + + public static final String SUBTYPE = "subtype"; + private final String typeKey; + + public static class Type { + public static final String ABSTRACTUSER = "abstractuser", USER = "user", OTHERUSER = "otheruser"; + } + + /** + * Create a new type mapper with the type key. + * + * @param typeKey the typeKey to use. + */ + public AbstractingTypeMapper(final String typeKey) { + super(new CouchbaseDocumentTypeAliasAccessor(typeKey), (MappingContext) null, Collections + .singletonList(new org.springframework.data.couchbase.core.convert.TypeAwareTypeInformationMapper())); + this.typeKey = typeKey; + } + + @Override + public String getTypeKey() { + return this.typeKey; + } + + public static final class CouchbaseDocumentTypeAliasAccessor implements TypeAliasAccessor { + + private final String typeKey; + + public CouchbaseDocumentTypeAliasAccessor(final String typeKey) { + this.typeKey = typeKey; + } + + @Override + public Alias readAliasFrom(final CouchbaseDocument source) { + String alias = (String) source.get(typeKey); + if (Type.ABSTRACTUSER.equals(alias)) { + String subtype = (String) source.get(AbstractingTypeMapper.SUBTYPE); + if (Type.OTHERUSER.equals(subtype)) { + alias = OtherUser.class.getName(); + } else if (Type.USER.equals(subtype)) { + alias = User.class.getName(); + } else { + throw new RuntimeException( + "no mapping for type " + SUBTYPE + "=" + subtype + " in type " + alias + " source=" + source); + } + } + return Alias.ofNullable(alias); + } + + @Override + public void writeTypeTo(final CouchbaseDocument sink, final Object alias) { + if (typeKey != null) { + sink.put(typeKey, alias); + } + } + } + + @Override + public Alias getTypeAlias(TypeInformation info) { + return getAliasFor(info); + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/OtherUser.java b/src/test/java/org/springframework/data/couchbase/domain/OtherUser.java index 4d5468654..4829ed451 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/OtherUser.java +++ b/src/test/java/org/springframework/data/couchbase/domain/OtherUser.java @@ -17,6 +17,7 @@ package org.springframework.data.couchbase.domain; import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.TypeAlias; import org.springframework.data.couchbase.core.mapping.Document; /** @@ -26,6 +27,7 @@ */ @Document +@TypeAlias(AbstractingTypeMapper.Type.ABSTRACTUSER) public class OtherUser extends AbstractUser { @PersistenceConstructor @@ -33,6 +35,7 @@ public OtherUser(final String id, final String firstname, final String lastname) this.id = id; this.firstname = firstname; this.lastname = lastname; + this.subtype = AbstractingTypeMapper.Type.OTHERUSER; } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/User.java b/src/test/java/org/springframework/data/couchbase/domain/User.java index c228e6bf4..5a841edb8 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/User.java +++ b/src/test/java/org/springframework/data/couchbase/domain/User.java @@ -16,11 +16,19 @@ package org.springframework.data.couchbase.domain; +import java.io.Serializable; +import java.util.Objects; + +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.Transient; +import org.springframework.data.annotation.TypeAlias; +import org.springframework.data.annotation.Version; import org.springframework.data.couchbase.core.mapping.Document; -import java.io.Serializable; - /** * User entity for tests * @@ -29,13 +37,71 @@ */ @Document -public class User extends AbstractUser implements Serializable { +@TypeAlias(AbstractingTypeMapper.Type.ABSTRACTUSER) +public class User extends AbstractUser implements Serializable { @PersistenceConstructor public User(final String id, final String firstname, final String lastname) { this.id = id; this.firstname = firstname; this.lastname = lastname; + this.subtype = AbstractingTypeMapper.Type.USER; + } + + @Version protected long version; + @Transient protected String transientInfo; + @CreatedBy protected String createdBy; + @CreatedDate protected long createdDate; + @LastModifiedBy protected String lastModifiedBy; + @LastModifiedDate protected long lastModifiedDate; + + public String getLastname() { + return lastname; + } + + public long getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(long createdDate) { + this.createdDate = createdDate; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public long getLastModifiedDate() { + return lastModifiedDate; + } + + public String getLastModifiedBy() { + return lastModifiedBy; + } + + public long getVersion() { + return version; + } + + public void setVersion(long version) { + this.version = version; + } + + @Override + public int hashCode() { + return Objects.hash(getId(), firstname, lastname); + } + + public String getTransientInfo() { + return transientInfo; + } + + public void setTransientInfo(String something) { + transientInfo = something; } } diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseAbstractRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseAbstractRepositoryIntegrationTests.java index 3eb99d838..e1e4c764d 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseAbstractRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseAbstractRepositoryIntegrationTests.java @@ -18,15 +18,21 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.List; import java.util.Optional; import java.util.UUID; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions; +import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; +import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; import org.springframework.data.couchbase.domain.AbstractUser; import org.springframework.data.couchbase.domain.AbstractUserRepository; +import org.springframework.data.couchbase.domain.AbstractingMappingCouchbaseConverter; import org.springframework.data.couchbase.domain.OtherUser; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; @@ -50,38 +56,50 @@ public class CouchbaseAbstractRepositoryIntegrationTests extends ClusterAwareInt void saveAndFindAbstract() { // User extends AbstractUser // OtherUser extends Abstractuser - AbstractUser user = null; + { - user = new User(UUID.randomUUID().toString(), "userFirstname", "userLastname"); - assertEquals(User.class, user.getClass()); - abstractUserRepository.save(user); + User concreteUser = null; { + concreteUser = new User(UUID.randomUUID().toString(), "userFirstname", "userLastname"); + assertEquals(User.class, concreteUser.getClass()); + concreteUser = abstractUserRepository.save(concreteUser); // this will now have version set // Queries on repositories for abstract entities must be @Query and not include // #{#n1ql.filter} (i.e. _class = ) as the classname will not match any document - AbstractUser found = abstractUserRepository.myFindById(user.getId()); - assertEquals(user, found); - assertEquals(user.getClass(), found.getClass()); + AbstractUser found = abstractUserRepository.myFindById(concreteUser.getId()); + assertEquals(concreteUser, found); + assertEquals(concreteUser.getClass(), found.getClass()); + } + { + Optional found = abstractUserRepository.findById(concreteUser.getId()); + assertEquals(concreteUser, found.get()); } { - Optional found = abstractUserRepository.findById(user.getId()); - assertEquals(user, found.get()); + List found = abstractUserRepository.findByFirstname(concreteUser.getFirstname()); + assertEquals(1, found.size(), "should have found one user"); + assertEquals(concreteUser, found.get(0)); } - abstractUserRepository.delete(user); + abstractUserRepository.delete(concreteUser); } { - user = new OtherUser(UUID.randomUUID().toString(), "userFirstname", "userLastname"); - assertEquals(OtherUser.class, user.getClass()); - abstractUserRepository.save(user); + AbstractUser abstractUser = new OtherUser(UUID.randomUUID().toString(), "userFirstname", "userLastname"); + assertEquals(OtherUser.class, abstractUser.getClass()); + abstractUserRepository.save(abstractUser); + { + // not going to find this one as using the type _class = AbstractUser ??? + AbstractUser found = abstractUserRepository.myFindById(abstractUser.getId()); + assertEquals(abstractUser, found); + assertEquals(abstractUser.getClass(), found.getClass()); + } { - AbstractUser found = abstractUserRepository.myFindById(user.getId()); - assertEquals(user, found); - assertEquals(user.getClass(), found.getClass()); + Optional found = abstractUserRepository.findById(abstractUser.getId()); + assertEquals(abstractUser, found.get()); } { - Optional found = abstractUserRepository.findById(user.getId()); - assertEquals(user, found.get()); + List found = abstractUserRepository.findByFirstname(abstractUser.getFirstname()); + assertEquals(1, found.size(), "should have found one user"); + assertEquals(abstractUser, found.get(0)); } - abstractUserRepository.delete(user); + abstractUserRepository.delete(abstractUser); } } @@ -110,6 +128,24 @@ public String getBucketName() { return bucketName(); } + /** + * This uses a CustomMappingCouchbaseConverter instead of MappingCouchbaseConverter, which in turn uses + * AbstractTypeMapper which has special mapping for AbstractUser + */ + @Override + @Bean(name = "mappingCouchbaseConverter") + public MappingCouchbaseConverter mappingCouchbaseConverter(CouchbaseMappingContext couchbaseMappingContext, + CouchbaseCustomConversions couchbaseCustomConversions /* there is a customConversions() method bean */) { + // MappingCouchbaseConverter relies on a SimpleInformationMapper + // that has an getAliasFor(info) that just returns getType().getName(). + // Our CustomMappingCouchbaseConverter uses a TypeBasedCouchbaseTypeMapper that will + // use the DocumentType annotation + MappingCouchbaseConverter converter = new AbstractingMappingCouchbaseConverter(couchbaseMappingContext, + typeKey()); + converter.setCustomConversions(couchbaseCustomConversions); + return converter; + } + } } diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java index c4594f22a..516f4c9ca 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java @@ -85,7 +85,7 @@ void createsQueryCorrectly() throws Exception { Query query = creator.createQuery(); assertEquals( - "SELECT META(`travel-sample`).id AS __id, META(`travel-sample`).cas AS __cas, `_class`, `firstname`, `lastname`, `createdBy`, `createdDate`, `lastModifiedBy`, `lastModifiedDate` FROM `travel-sample` where `_class` = \"org.springframework.data.couchbase.domain.User\" and firstname = $1 and lastname = $2", + "SELECT META(`travel-sample`).id AS __id, META(`travel-sample`).cas AS __cas, `_class`, `createdBy`, `createdDate`, `lastModifiedBy`, `lastModifiedDate`, `firstname`, `lastname`, `subtype` FROM `travel-sample` where `_class` = \"abstractuser\" and firstname = $1 and lastname = $2", query.toN1qlSelectString(couchbaseTemplate.reactive(), User.class, false)); } @@ -104,7 +104,7 @@ void createsQueryCorrectly2() throws Exception { Query query = creator.createQuery(); assertEquals( - "SELECT META(`travel-sample`).id AS __id, META(`travel-sample`).cas AS __cas, `_class`, `firstname`, `lastname`, `createdBy`, `createdDate`, `lastModifiedBy`, `lastModifiedDate` FROM `travel-sample` where `_class` = \"org.springframework.data.couchbase.domain.User\" and (firstname = $first or lastname = $last)", + "SELECT META(`travel-sample`).id AS __id, META(`travel-sample`).cas AS __cas, `_class`, `createdBy`, `createdDate`, `lastModifiedBy`, `lastModifiedDate`, `firstname`, `lastname`, `subtype` FROM `travel-sample` where `_class` = \"abstractuser\" and (firstname = $first or lastname = $last)", query.toN1qlSelectString(couchbaseTemplate.reactive(), User.class, false)); }