Option to not use typeKey=typeAlias property and predicate. (#1777)

If the configuration getType() or the @TypeAlias is an empty string,
then do not add the typeKey:typeAlias property when storing documents,
and do not add the typeKey=typeAlias property on queries - including
removeByQuery.
This commit is contained in:
Michael Reiche
2023-07-06 16:49:26 -07:00
committed by mikereiche
parent 102d90ff10
commit dff149afa7
10 changed files with 287 additions and 29 deletions

View File

@@ -67,6 +67,9 @@ public class DefaultCouchbaseTypeMapper extends DefaultTypeMapper<CouchbaseDocum
@Override
public Alias readAliasFrom(final CouchbaseDocument source) {
if (typeKey == null || typeKey.length() == 0) {
return Alias.NONE;
}
return Alias.ofNullable(source.get(typeKey));
}

View File

@@ -15,6 +15,9 @@
*/
package org.springframework.data.couchbase.core.index;
import static org.springframework.data.couchbase.core.query.N1QLExpression.i;
import static org.springframework.data.couchbase.core.query.N1QLExpression.s;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -25,6 +28,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.couchbase.core.mapping.Document;
import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation;
import org.springframework.data.mapping.Alias;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.util.TypeInformation;
@@ -142,7 +146,15 @@ public class CouchbasePersistentEntityIndexResolver implements QueryIndexResolve
private String getPredicate(final MappingCouchbaseEntityInformation<?, Object> entityInfo) {
String typeKey = operations.getConverter().getTypeKey();
String typeValue = entityInfo.getJavaType().getName();
return "`" + typeKey + "` = \"" + typeValue + "\"";
Alias alias = operations.getConverter().getTypeAlias(TypeInformation.of(entityInfo.getJavaType()));
if (alias != null && alias.isPresent()) {
typeValue = alias.toString();
}
return !empty(typeKey) && !empty(typeValue) ? i(typeKey).eq(s(typeValue)).toString() : null;
}
private static boolean empty(String s){
return s == null || s.length() == 0;
}
public static class IndexDefinitionHolder implements IndexDefinition {

View File

@@ -353,7 +353,9 @@ public class Query {
domainClass, returnClass, isCount, distinctFields, fields);
final StringBuilder statement = new StringBuilder();
appendString(statement, n1ql.selectEntity); // select ...
appendWhereString(statement, n1ql.filter); // typeKey = typeValue
if (n1ql.filter != null) {
appendWhereString(statement, n1ql.filter); // typeKey = typeValue
}
appendWhere(statement, new int[] { 0 }, converter); // criteria on this Query
if (!isCount) {
appendSort(statement);
@@ -368,7 +370,9 @@ public class Query {
domainClass, null, false, null, null);
final StringBuilder statement = new StringBuilder();
appendString(statement, n1ql.delete); // delete ...
appendWhereString(statement, n1ql.filter); // typeKey = typeValue
if (n1ql.filter != null) {
appendWhereString(statement, n1ql.filter); // typeKey = typeValue
}
appendWhere(statement, null, converter); // criteria on this Query
appendString(statement, n1ql.returning);
return statement.toString();

View File

@@ -37,6 +37,7 @@ import com.couchbase.client.java.json.JsonValue;
* supported
*
* @author Subhashni Balakrishnan
* @author Michael Reiche
*/
public class N1qlMutateQueryCreator extends AbstractQueryCreator<N1QLExpression, N1QLExpression>
implements PartTreeN1qlQueryCreator {
@@ -82,6 +83,9 @@ public class N1qlMutateQueryCreator extends AbstractQueryCreator<N1QLExpression,
protected N1QLExpression complete(N1QLExpression criteria, Sort sort) {
N1QLExpression whereCriteria = N1qlUtils.createWhereFilterForEntity(criteria, this.converter,
this.queryMethod.getEntityInformation());
if (whereCriteria == null) {
return mutateFrom;
}
return mutateFrom.where(whereCriteria);
}

View File

@@ -124,7 +124,7 @@ public class OldN1qlQueryCreator extends AbstractQueryCreator<N1QLExpression, N1
N1QLExpression whereCriteria = N1qlUtils.createWhereFilterForEntity(criteria, this.converter,
this.queryMethod.getEntityInformation());
N1QLExpression selectFromWhere = selectFrom.where(whereCriteria);
N1QLExpression selectFromWhere = whereCriteria != null ? selectFrom.where(whereCriteria) : selectFrom;
// sort of the Pageable takes precedence over the sort in the query name
if ((queryMethod.isPageQuery() || queryMethod.isSliceQuery()) && accessor.getPageable().isPaged()) {

View File

@@ -16,6 +16,7 @@
package org.springframework.data.couchbase.repository.query;
import static org.springframework.data.couchbase.core.query.N1QLExpression.i;
import static org.springframework.data.couchbase.core.query.N1QLExpression.s;
import static org.springframework.data.couchbase.core.query.N1QLExpression.x;
import static org.springframework.data.couchbase.core.support.TemplateUtils.SELECT_CAS;
import static org.springframework.data.couchbase.core.support.TemplateUtils.SELECT_ID;
@@ -199,22 +200,22 @@ public class StringBasedN1qlQueryParser {
}
/**
* Create the n1ql spel values. The domainClass is needed, but not the returnClass. Mapping the domainClass to the
* returnClass is the responsibility of decoding.
*
* @param bucketName
* @param scope
* @param collection
* @param domainClass
* @param typeField
* @param typeValue
* @param isCount
* @param distinctFields
* @param fields
* @return
*/
* Create the n1ql spel values. The domainClass is needed, but not the returnClass. Mapping the domainClass to the
* returnClass is the responsibility of decoding.
*
* @param bucketName
* @param scope
* @param collection
* @param domainClass
* @param typeKey
* @param typeValue
* @param isCount
* @param distinctFields
* @param fields
* @return
*/
public N1qlSpelValues createN1qlSpelValues(String bucketName, String scope, String collection, Class domainClass,
String typeField, String typeValue, boolean isCount, String[] distinctFields, String[] fields) {
String typeKey, String typeValue, boolean isCount, String[] distinctFields, String[] fields) {
String b = bucketName;
String keyspace = collection != null ? collection : bucketName;
Assert.isTrue(!(distinctFields != null && fields != null),
@@ -222,7 +223,7 @@ public class StringBasedN1qlQueryParser {
String entityFields = "";
String selectEntity;
if (distinctFields != null) {
String distinctFieldsStr = getProjectedOrDistinctFields(b, domainClass, typeField, fields, distinctFields);
String distinctFieldsStr = getProjectedOrDistinctFields(b, domainClass, typeKey, fields, distinctFields);
if (isCount) {
selectEntity = N1QLExpression.select(N1QLExpression.count(N1QLExpression.distinct(x(distinctFieldsStr)))
.as(i(CountFragment.COUNT_ALIAS)).from(keyspace)).toString();
@@ -233,11 +234,12 @@ public class StringBasedN1qlQueryParser {
selectEntity = N1QLExpression.select(N1QLExpression.count(x("\"*\"")).as(i(CountFragment.COUNT_ALIAS)))
.from(keyspace).toString();
} else {
String projectedFields = getProjectedOrDistinctFields(keyspace, domainClass, typeField, fields, distinctFields);
String projectedFields = getProjectedOrDistinctFields(keyspace, domainClass, typeKey, fields,
distinctFields);
entityFields = projectedFields;
selectEntity = N1QLExpression.select(x(projectedFields)).from(keyspace).toString();
}
String typeSelection = "`" + typeField + "` = \"" + typeValue + "\"";
String typeSelection = !empty(typeKey) && !empty(typeValue) ? i(typeKey).eq(s(typeValue)).toString() : null;
String delete = N1QLExpression.delete().from(keyspace).toString();
String returning = " returning " + N1qlUtils.createReturningExpressionForDelete(keyspace);
@@ -246,6 +248,10 @@ public class StringBasedN1qlQueryParser {
i(collection).toString(), typeSelection, delete, returning);
}
private static boolean empty(String s) {
return s == null || s.length() == 0;
}
private String getProjectedOrDistinctFields(String b, Class resultClass, String typeField, String[] fields,
String[] distinctFields) {
if (distinctFields != null && distinctFields.length != 0) {

View File

@@ -16,8 +16,15 @@
package org.springframework.data.couchbase.repository.query.support;
import static org.springframework.data.couchbase.core.query.N1QLExpression.*;
import static org.springframework.data.couchbase.core.support.TemplateUtils.*;
import static org.springframework.data.couchbase.core.query.N1QLExpression.count;
import static org.springframework.data.couchbase.core.query.N1QLExpression.i;
import static org.springframework.data.couchbase.core.query.N1QLExpression.meta;
import static org.springframework.data.couchbase.core.query.N1QLExpression.path;
import static org.springframework.data.couchbase.core.query.N1QLExpression.s;
import static org.springframework.data.couchbase.core.query.N1QLExpression.select;
import static org.springframework.data.couchbase.core.query.N1QLExpression.x;
import static org.springframework.data.couchbase.core.support.TemplateUtils.SELECT_CAS;
import static org.springframework.data.couchbase.core.support.TemplateUtils.SELECT_ID;
import java.util.ArrayList;
import java.util.List;
@@ -32,10 +39,12 @@ import org.springframework.data.couchbase.core.query.N1QLQuery;
import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation;
import org.springframework.data.couchbase.repository.query.CountFragment;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.Alias;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.repository.core.EntityMetadata;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.util.TypeInformation;
import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.json.JsonObject;
@@ -50,6 +59,7 @@ import com.couchbase.client.java.query.QueryScanConsistency;
* @author Simon Baslé
* @author Subhashni Balakrishnan
* @author Mark Paluch
* @author Michael Reiche
*/
public class N1qlUtils {
@@ -170,15 +180,23 @@ public class N1qlUtils {
// add part that filters on type key
String typeKey = converter.getTypeKey();
String typeValue = entityInformation.getJavaType().getName();
N1QLExpression typeSelector = i(typeKey).eq(s(typeValue));
Alias alias = converter.getTypeAlias(TypeInformation.of(entityInformation.getJavaType()));
if (alias != null && alias.isPresent()) {
typeValue = alias.toString();
}
N1QLExpression typeSelector = !empty(typeKey) && !empty(typeValue) ? i(typeKey).eq(s(typeValue)) : null;
if (baseWhereCriteria == null) {
baseWhereCriteria = typeSelector;
} else {
} else if (typeSelector != null) {
baseWhereCriteria = x("(" + baseWhereCriteria.toString() + ")").and(typeSelector);
}
return baseWhereCriteria;
}
private static boolean empty(String s) {
return s == null || s.length() == 0;
}
/**
* Given a common {@link PropertyPath}, returns the corresponding {@link PersistentPropertyPath} of
* {@link CouchbasePersistentProperty} which will allow to discover alternative naming for fields.
@@ -241,8 +259,13 @@ public class N1qlUtils {
*/
public static <T> N1QLExpression createCountQueryForEntity(String bucketName, CouchbaseConverter converter,
CouchbaseEntityInformation<T, String> entityInformation) {
return select(count(x("*")).as(x(CountFragment.COUNT_ALIAS))).from(escapedBucket(bucketName))
.where(createWhereFilterForEntity(null, converter, entityInformation));
N1QLExpression entityFilter = createWhereFilterForEntity(null, converter, entityInformation);
N1QLExpression expression = select(
(count(x("*")).as(x(CountFragment.COUNT_ALIAS))).from(escapedBucket(bucketName)));
if (entityFilter == null) {
return expression;
}
return expression.where(entityFilter);
}
/**

View File

@@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.data.couchbase.core.query.N1QLExpression.i;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
@@ -38,15 +39,17 @@ import java.util.UUID;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.InvalidDataAccessResourceUsageException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.couchbase.core.ExecutableFindByIdOperation.ExecutableFindById;
import org.springframework.data.couchbase.core.ExecutableRemoveByIdOperation.ExecutableRemoveById;
import org.springframework.data.couchbase.core.ExecutableReplaceByIdOperation.ExecutableReplaceById;
import org.springframework.data.couchbase.core.query.Query;
import org.springframework.data.couchbase.core.query.QueryCriteria;
import org.springframework.data.couchbase.core.support.OneAndAllEntity;
import org.springframework.data.couchbase.core.support.OneAndAllId;
import org.springframework.data.couchbase.core.support.WithDurability;
@@ -66,6 +69,7 @@ import org.springframework.data.couchbase.domain.UserAnnotatedDurabilityExpressi
import org.springframework.data.couchbase.domain.UserAnnotatedPersistTo;
import org.springframework.data.couchbase.domain.UserAnnotatedReplicateTo;
import org.springframework.data.couchbase.domain.UserAnnotatedTouchOnRead;
import org.springframework.data.couchbase.domain.UserNoAlias;
import org.springframework.data.couchbase.domain.UserSubmission;
import org.springframework.data.couchbase.util.ClusterType;
import org.springframework.data.couchbase.util.IgnoreWhen;
@@ -142,6 +146,27 @@ class CouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests {
}
@Test
void findByIdNoAlias() {
String firstname = UUID.randomUUID().toString();
try {
UserNoAlias user = new UserNoAlias("1", firstname, "user1");
couchbaseTemplate.upsertById(UserNoAlias.class).one(user);
UserNoAlias foundUser = couchbaseTemplate.findById(UserNoAlias.class).one(user.getId());
user.setVersion(foundUser.getVersion());// version will have changed
assertEquals(user, foundUser);
Query query = new Query(QueryCriteria.where(i("firstname")).eq(firstname));
List<UserNoAlias> queriedUsers = couchbaseTemplate.findByQuery(UserNoAlias.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).matching(query).all();
assertEquals(1, queriedUsers.size(), "should have found exactly one");
} finally {
Query query = new Query(QueryCriteria.where(i("firstname")).eq(firstname));
List<RemoveResult> removeResult = couchbaseTemplate.removeByQuery(UserNoAlias.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).matching(query).all();
assertEquals(1, removeResult.size(), "should have removed exactly one");
}
}
@Test
void findByIdWithExpiry() {
try {
@@ -1289,6 +1314,7 @@ class CouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests {
}
@Test
@Disabled // it's finding _txn documents with source = a single 0 byte which fails to deserialize
void sampleScan() {
String id = "A";
String lower = null;

View File

@@ -63,6 +63,7 @@ import org.springframework.data.couchbase.domain.Address;
import org.springframework.data.couchbase.domain.Config;
import org.springframework.data.couchbase.domain.Person;
import org.springframework.data.couchbase.domain.User;
import org.springframework.data.couchbase.domain.UserNoAlias;
import org.springframework.data.mapping.MappingException;
/**
@@ -75,10 +76,17 @@ public class MappingCouchbaseConverterTests {
private static MappingCouchbaseConverter converter = new MappingCouchbaseConverter();
private static MappingCouchbaseConverter customConverter = (new Config()).mappingCouchbaseConverter();
private static MappingCouchbaseConverter noTypeKeyConverter = (new Config(){
@Override
public String typeKey() {
return "";
}
}).mappingCouchbaseConverter();
static {
converter.afterPropertiesSet();
customConverter.afterPropertiesSet();
noTypeKeyConverter.afterPropertiesSet();
}
@Test
@@ -152,6 +160,43 @@ public class MappingCouchbaseConverterTests {
assertThat(converted.attr0).isEqualTo(source.get("attr0"));
}
@Test
void writesStringNoTypeKey() {
CouchbaseDocument converted = new CouchbaseDocument();
StringEntity entity = new StringEntity("foobar");
noTypeKeyConverter.write(entity, converted);
Map<String, Object> result = converted.export();
assertThat(result.get("_class")).isEqualTo(null);
assertThat(result.get("attr0")).isEqualTo(entity.attr0);
assertThat(converted.getId()).isEqualTo(BaseEntity.ID);
}
@Test
void readsStringNoTypeKey() {
CouchbaseDocument source = new CouchbaseDocument();
source.put("attr0", "foobar");
StringEntity converted = noTypeKeyConverter.read(StringEntity.class, source);
assertThat(converted.attr0).isEqualTo(source.get("attr0"));
}
@Test
void writesNoTypeAlias() {
CouchbaseDocument converted = new CouchbaseDocument();
UserNoAlias entity = new UserNoAlias(UUID.randomUUID().toString(), "first", "last");
noTypeKeyConverter.write(entity, converted);
Map<String, Object> result = converted.export();
assertThat(result.get("_class")).isEqualTo(null);
assertThat(converted.getId()).isEqualTo(entity.getId());
}
@Test
void readsNoTypeAlias() {
CouchbaseDocument document = new CouchbaseDocument("001");
UserNoAlias user = noTypeKeyConverter.read(UserNoAlias.class, document);
assertThat(user.getId()).isEqualTo("001");
}
@Test
void writesBigInteger() {
CouchbaseDocument converted = new CouchbaseDocument();

View File

@@ -0,0 +1,135 @@
/*
* Copyright 2012-2023 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.io.Serializable;
import java.util.HashMap;
import java.util.Map;
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 com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.json.JsonObject;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* User entity with an empty TypeAlias for tests
*
* @author Michael Nitschinger
* @author Michael Reiche
*/
@Document
@TypeAlias("")
public class UserNoAlias extends AbstractUser implements Serializable {
public JsonNode jsonNode;
public JsonObject jsonObject;
public JsonArray jsonArray;
@PersistenceConstructor
public UserNoAlias(final String id, final String firstname, final String lastname) {
this.id = id;
this.firstname = firstname;
this.lastname = lastname;
this.subtype = AbstractingTypeMapper.Type.USER;
this.jsonNode = new ObjectNode(JsonNodeFactory.instance);
try {
jsonNode = (new ObjectNode(JsonNodeFactory.instance)).put("myNumber", uid());
} catch (Exception e) {
e.printStackTrace();
}
Map map = new HashMap();
map.put("myNumber", uid());
this.jsonObject = JsonObject.jo().put("yourNumber",Long.valueOf(uid()));
this.jsonArray = JsonArray.from(Long.valueOf(uid()), Long.valueOf(uid()));
}
@Transient int uid=1000;
long uid(){
return uid++;
}
@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;
}
}