DATACOUCH-525 - support custom type field name and custome type field value

MOTIVATION
customers would like to specify a type field other than _class, and
would like to specify a type field value other than the class name
of the domain object

CHANGES
Follow the instructions at
https://stackoverflow.com/questions/38847605/how-to-use-spring-data-with-couchbase-without-class-attribute
in the test domain, supply a CustomMappingCouchbaseConvert class that leverates a
TypeBasedCouchbaseTypeMapper, which in turn uses a TypeAwareTypeInformationMapper.
The typeKey used is "t". The TypeAwareTypeInformationMapper uses the DocumentType annotation
as the value for the type map entry. The domain Person object has @DocumentType("p"), which
will result in type mapping of "t" : "p" for Person objects.

In addition, when a query is composed, the typeKey and typeValue are taken from the converter.
This commit is contained in:
mikereiche
2020-06-02 15:59:57 -07:00
parent dafff211a4
commit 95c69ea8cc
11 changed files with 231 additions and 23 deletions

View File

@@ -21,6 +21,8 @@ import org.springframework.data.convert.EntityReader;
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.mapping.Alias;
import org.springframework.data.util.TypeInformation;
/**
* Marker interface for the converter, identifying the types to and from that can be converted.
@@ -54,4 +56,9 @@ public interface CouchbaseConverter
* @return the name of the field that will hold type information.
*/
String getTypeKey();
/**
* @return the alias value for the type
*/
Alias getTypeAlias(TypeInformation<?> info);
}

View File

@@ -18,14 +18,19 @@ package org.springframework.data.couchbase.core.convert;
import org.springframework.data.convert.TypeMapper;
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
import org.springframework.data.mapping.Alias;
import org.springframework.data.util.TypeInformation;
/**
* Marker interface for the TypeMapper.
*
* @author Michael Nitschinger
* @author Michael Reiche
*/
public interface CouchbaseTypeMapper extends TypeMapper<CouchbaseDocument> {
String getTypeKey();
Alias getTypeAlias(TypeInformation<?> info);
}

View File

@@ -20,6 +20,7 @@ import org.springframework.data.convert.DefaultTypeMapper;
import org.springframework.data.convert.TypeAliasAccessor;
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
import org.springframework.data.mapping.Alias;
import org.springframework.data.util.TypeInformation;
/**
* The Couchbase Type Mapper.
@@ -72,4 +73,8 @@ public class DefaultCouchbaseTypeMapper extends DefaultTypeMapper<CouchbaseDocum
}
}
@Override
public Alias getTypeAlias(TypeInformation<?> info) {
return getAliasFor(info);
}
}

View File

@@ -39,17 +39,14 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseList;
import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.couchbase.core.mapping.event.AfterConvertCallback;
import org.springframework.data.couchbase.core.mapping.id.GeneratedValue;
import org.springframework.data.couchbase.core.mapping.id.IdAttribute;
import org.springframework.data.couchbase.core.mapping.id.IdPrefix;
import org.springframework.data.couchbase.core.mapping.id.IdSuffix;
import org.springframework.data.couchbase.core.query.N1qlJoin;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.AssociationHandler;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.*;
import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
@@ -182,6 +179,11 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
return typeMapper.getTypeKey();
}
@Override
public Alias getTypeAlias(TypeInformation<?> info) {
return typeMapper.getTypeAlias(info);
}
@Override
public <R> R read(final Class<R> clazz, final CouchbaseDocument source) {
return read(ClassTypeInformation.from(clazz), source, null);

View File

@@ -31,6 +31,9 @@ import org.springframework.data.couchbase.repository.query.StringBasedN1qlQueryP
import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.Alias;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
/**
@@ -47,8 +50,7 @@ public class Query {
static private final Pattern WHERE_PATTERN = Pattern.compile("\\sWHERE\\s");
public Query() {
}
public Query() {}
public Query(final QueryCriteria criteriaDefinition) {
addCriteria(criteriaDefinition);
@@ -60,8 +62,8 @@ public class Query {
}
/**
* set the postional parameters on the query object
* There can only be named parameters or positional parameters - not both.
* set the postional parameters on the query object There can only be named parameters or positional parameters - not
* both.
*
* @param parameters - the positional parameters
* @return - the query
@@ -72,8 +74,8 @@ public class Query {
}
/**
* set the named parameters on the query object
* There can only be named parameters or positional parameters - not both.
* set the named parameters on the query object There can only be named parameters or positional parameters - not
* both.
*
* @param parameters - the named parameters
* @return - the query
@@ -157,10 +159,8 @@ public class Query {
sb.append(" ORDER BY ");
sort.stream().forEach(order -> {
if (order.isIgnoreCase()) {
throw new IllegalArgumentException(String.format(
"Given sort contained an Order for %s with ignore case! "
+ "Couchbase N1QL does not support sorting ignoring case currently!",
order.getProperty()));
throw new IllegalArgumentException(String.format("Given sort contained an Order for %s with ignore case! "
+ "Couchbase N1QL does not support sorting ignoring case currently!", order.getProperty()));
}
sb.append(order.getProperty()).append(" ").append(order.isAscending() ? "ASC," : "DESC,");
});
@@ -257,10 +257,15 @@ public class Query {
StringBasedN1qlQueryParser.N1qlSpelValues getN1qlSpelValues(ReactiveCouchbaseTemplate template, Class domainClass,
boolean isCount) {
String typeKey = template.getConverter().getTypeKey();
final CouchbasePersistentEntity<?> persistentEntity = template.getConverter().getMappingContext().getRequiredPersistentEntity(
domainClass);
final CouchbasePersistentEntity<?> persistentEntity = template.getConverter().getMappingContext()
.getRequiredPersistentEntity(domainClass);
MappingCouchbaseEntityInformation<?, Object> info = new MappingCouchbaseEntityInformation<>(persistentEntity);
String typeValue = info.getJavaType().getName();
TypeInformation<?> typeInfo = ClassTypeInformation.from(info.getJavaType());
Alias alias = template.getConverter().getTypeAlias(typeInfo);
if (alias != null && alias.isPresent()) {
typeValue = alias.toString();
}
return StringBasedN1qlQueryParser.createN1qlSpelValues(template.getBucketName(), typeKey, typeValue, isCount);
}

View File

@@ -28,11 +28,13 @@ import java.util.*;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.TypeAlias;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions;
import org.springframework.data.couchbase.core.convert.CouchbaseJsr310Converters.LocalDateTimeToLongConverter;
import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter;
import org.springframework.data.couchbase.domain.Config;
import org.springframework.data.couchbase.domain.User;
import org.springframework.data.mapping.MappingException;
@@ -45,9 +47,11 @@ import org.springframework.data.mapping.MappingException;
public class MappingCouchbaseConverterTests {
private static MappingCouchbaseConverter converter = new MappingCouchbaseConverter();
private static MappingCouchbaseConverter customConverter = (new Config()).mappingCouchbaseConverter();
static {
converter.afterPropertiesSet();
customConverter.afterPropertiesSet();
}
@Test
@@ -112,6 +116,18 @@ public class MappingCouchbaseConverterTests {
assertThat(converted.getId()).isEqualTo(BaseEntity.ID);
}
@Test
void writesNumberCustom() {
CouchbaseDocument converted = new CouchbaseDocument();
NumberEntity entity = new NumberEntity(42);
customConverter.write(entity, converted);
Map<String, Object> result = converted.export();
assertThat(result.get("t")).isEqualTo(entity.getClass().getName());
assertThat(result.get("attr0")).isEqualTo(42L);
assertThat(converted.getId()).isEqualTo(BaseEntity.ID);
}
@Test
void readsNumber() {
CouchbaseDocument source = new CouchbaseDocument();
@@ -363,6 +379,40 @@ public class MappingCouchbaseConverterTests {
assertThat(readConverted.listOfEmails.get(0).emailAddr).isEqualTo(listOfEmails.get(0).emailAddr);
}
@Test
void writesAndReadsValueClassCustomType() {
CouchbaseDocument converted = new CouchbaseDocument();
final String email = "foo@bar.com";
final Emailx addy = new Emailx(email);
List<Emailx> listOfEmails = new ArrayList<Emailx>();
listOfEmails.add(addy);
ValueEntityx entity = new ValueEntityx(addy, listOfEmails);
customConverter.write(entity, converted);
Map<String, Object> result = converted.export();
assertThat(result.get("t")).isEqualTo(ValueEntityx.class.getAnnotation(TypeAlias.class).value());
assertThat(result.get("email")).isEqualTo(new HashMap<String, Object>() {
{
put("emailAddr", email);
}
});
CouchbaseDocument source = new CouchbaseDocument();
source.put("_class", ValueEntity.class.getName());
CouchbaseDocument emailDoc = new CouchbaseDocument();
emailDoc.put("emailAddr", "foo@bar.com");
source.put("email", emailDoc);
CouchbaseList listOfEmailsDoc = new CouchbaseList();
listOfEmailsDoc.put(emailDoc);
source.put("listOfEmails", listOfEmailsDoc);
ValueEntity readConverted = converter.read(ValueEntity.class, source);
assertThat(readConverted.email.emailAddr).isEqualTo(addy.emailAddr);
assertThat(readConverted.listOfEmails.get(0).emailAddr).isEqualTo(listOfEmails.get(0).emailAddr);
}
@Test
void writesAndReadsCustomConvertedClass() {
List<Object> converters = new ArrayList<>();
@@ -693,6 +743,26 @@ public class MappingCouchbaseConverterTests {
}
}
@TypeAlias("x")
static class ValueEntityx extends BaseEntity {
private Emailx email;
private List<Emailx> listOfEmails;
public ValueEntityx(Emailx email, List<Emailx> listOfEmails) {
this.email = email;
this.listOfEmails = listOfEmails;
}
}
// @TypeAlias("x")
static class Emailx {
private String emailAddr;
public Emailx(String emailAddr) {
this.emailAddr = emailAddr;
}
}
static class CustomObject {
private BigDecimal weight;

View File

@@ -16,6 +16,10 @@
package org.springframework.data.couchbase.domain;
import org.springframework.data.couchbase.config.BeanNames;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.auditing.DateTimeProvider;
@@ -113,4 +117,38 @@ public class Config extends AbstractCouchbaseConfiguration {
public DateTimeProvider testDateTimeProvider() {
return new AuditingDateTimeProvider();
}
// convenience constructor for tests
public MappingCouchbaseConverter mappingCouchbaseConverter() {
MappingCouchbaseConverter converter = null;
try {
// 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
converter = new CustomMappingCouchbaseConverter(couchbaseMappingContext(customConversions()), typeKey());
} catch (Exception e) {
throw new RuntimeException(e);
}
return converter;
}
@Override
@Bean(name = "couchbaseMappingConverter")
public MappingCouchbaseConverter mappingCouchbaseConverter(CouchbaseMappingContext couchbaseMappingContext,
CouchbaseCustomConversions couchbaseCustomConversions) {
// 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 CustomMappingCouchbaseConverter(couchbaseMappingContext, typeKey());
converter.setCustomConversions(couchbaseCustomConversions);
return converter;
}
@Override
public String typeKey() {
return "t"; // this will override '_class', is passed in to new CustomMappingCouchbaseConverter
}
}

View File

@@ -0,0 +1,23 @@
package org.springframework.data.couchbase.domain;
import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter;
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
public class CustomMappingCouchbaseConverter 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 CustomMappingCouchbaseConverter(final MappingContext<? extends CouchbasePersistentEntity<?>, CouchbasePersistentProperty> mappingContext, final String typeKey) {
super(mappingContext, typeKey);
this.typeMapper = new TypeBasedCouchbaseTypeMapper(typeKey);
}
}

View File

@@ -18,14 +18,11 @@ package org.springframework.data.couchbase.domain;
import java.util.Optional;
import java.util.UUID;
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.Version;
import org.springframework.data.annotation.*;
import org.springframework.data.couchbase.core.mapping.Document;
@Document
@TypeAlias("p") // this will result in p : <className> or p : "t" using the CustomMappingCouchbaseConverter
public class Person extends AbstractEntity {
Optional<String> firstname;
Optional<String> lastname;
@@ -40,11 +37,13 @@ public class Person extends AbstractEntity {
private long lastModification;
@CreatedDate
private long creationDate; // =System.currentTimeMillis();
private long creationDate;
@Version
private long version;
private Object middlename;
public Person() {
}
@@ -132,4 +131,7 @@ public class Person extends AbstractEntity {
return "";
}
public void setMiddlename(String middlename) {
this.middlename = middlename;
}
}

View File

@@ -0,0 +1,20 @@
package org.springframework.data.couchbase.domain;
import org.springframework.data.annotation.TypeAlias;
import org.springframework.data.convert.SimpleTypeInformationMapper;
import org.springframework.data.mapping.Alias;
import org.springframework.data.util.TypeInformation;
public class TypeAwareTypeInformationMapper extends SimpleTypeInformationMapper {
@Override
public Alias createAliasFor(TypeInformation<?> type) {
TypeAlias[] typeAlias = type.getType().getAnnotationsByType(TypeAlias.class);
if (typeAlias.length == 1) {
return Alias.of(typeAlias[0].value());
}
return super.createAliasFor(type);
}
}

View File

@@ -0,0 +1,31 @@
package org.springframework.data.couchbase.domain;
import org.springframework.data.convert.DefaultTypeMapper;
import org.springframework.data.couchbase.core.convert.CouchbaseTypeMapper;
import org.springframework.data.couchbase.core.convert.DefaultCouchbaseTypeMapper;
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
import org.springframework.data.mapping.Alias;
import org.springframework.data.util.TypeInformation;
import java.util.Collections;
public class TypeBasedCouchbaseTypeMapper extends DefaultTypeMapper<CouchbaseDocument> implements CouchbaseTypeMapper {
private final String typeKey;
public TypeBasedCouchbaseTypeMapper(final String typeKey) {
super(new DefaultCouchbaseTypeMapper.CouchbaseDocumentTypeAliasAccessor(typeKey),
Collections.singletonList(new TypeAwareTypeInformationMapper()));
this.typeKey = typeKey;
}
@Override
public String getTypeKey() {
return typeKey;
}
@Override
public Alias getTypeAlias(TypeInformation<?> info) {
return getAliasFor(info);
}
}