Skip to content

Support derived queries on repositories defined with an abstract enti… #1366

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ public CouchbaseDocument encodeEntity(final Object entityToEncode) {
public <T> T decodeEntity(String id, String source, long cas, Class<T> 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<? extends R> typeToUse = typeMapper.readType(source, type);

CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(entityClass);

if (persistentEntity == null) { // method could return a Long, Boolean, String etc.
Expand All @@ -99,14 +111,24 @@ public <T> T decodeEntity(String id, String source, long cas, Class<T> 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<T> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -19,28 +19,28 @@
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;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.query.QueryScanConsistency;

/**
* AbstractUser Repository for tests
*
* @author Michael Reiche
*/
@Repository
@ScanConsistency(query=QueryScanConsistency.REQUEST_PLUS)
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
public interface AbstractUserRepository extends CouchbaseRepository<AbstractUser, String> {

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

List<User> findByFirstname(String firstname);
List<AbstractUser> findByFirstname(String firstname);

Stream<User> findByLastname(String lastname);

Expand Down
Original file line number Diff line number Diff line change
@@ -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<? extends CouchbasePersistentEntity<?>, CouchbasePersistentProperty> mappingContext,
final String typeKey) {
super(mappingContext, typeKey);
this.typeMapper = new AbstractingTypeMapper(typeKey);
}

}
Original file line number Diff line number Diff line change
@@ -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<CouchbaseDocument> 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<CouchbaseDocument> {

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

}
Loading