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 6a5e4c584..81492eb44 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 @@ -162,11 +162,12 @@ public N1qlSpelValues createN1qlSpelValues(String bucketName, String collection, String b = collection != null ? collection : bucketName; Assert.isTrue(!(distinctFields != null && fields != null), "only one of project(fields) and distinct(distinctFields) can be specified"); - String entity = "META(" + i(b) + ").id AS " + SELECT_ID + ", META(" + i(b) + ").cas AS " + SELECT_CAS; + String entity = "META(" + i(b) + ").id AS " + SELECT_ID + ", META(" + i(b) + ").cas AS " + SELECT_CAS + ", " + + i(typeField); String count = "COUNT(*) AS " + CountFragment.COUNT_ALIAS; String selectEntity; if (distinctFields != null) { - String distinctFieldsStr = getProjectedOrDistinctFields(b, domainClass, fields, distinctFields); + String distinctFieldsStr = getProjectedOrDistinctFields(b, domainClass, typeField, fields, distinctFields); if (isCount) { selectEntity = "SELECT COUNT( DISTINCT {" + distinctFieldsStr + "} ) " + CountFragment.COUNT_ALIAS + " FROM " + i(b); @@ -176,7 +177,7 @@ public N1qlSpelValues createN1qlSpelValues(String bucketName, String collection, } else if (isCount) { selectEntity = "SELECT " + count + " FROM " + i(b); } else { - String projectedFields = getProjectedOrDistinctFields(b, domainClass, fields, distinctFields); + String projectedFields = getProjectedOrDistinctFields(b, domainClass, typeField, fields, distinctFields); selectEntity = "SELECT " + entity + (!projectedFields.isEmpty() ? ", " : " ") + projectedFields + " FROM " + i(b); } String typeSelection = "`" + typeField + "` = \"" + typeValue + "\""; @@ -187,7 +188,7 @@ public N1qlSpelValues createN1qlSpelValues(String bucketName, String collection, return new N1qlSpelValues(selectEntity, entity, i(b).toString(), typeSelection, delete, returning); } - private String getProjectedOrDistinctFields(String b, Class resultClass, String[] fields, String[] distinctFields) { + private String getProjectedOrDistinctFields(String b, Class resultClass, String typeField, String[] fields, String[] distinctFields) { if (distinctFields != null && distinctFields.length != 0) { return i(distinctFields).toString(); } @@ -195,14 +196,14 @@ private String getProjectedOrDistinctFields(String b, Class resultClass, String[ if (resultClass != null) { PersistentEntity persistentEntity = couchbaseConverter.getMappingContext().getPersistentEntity(resultClass); StringBuilder sb = new StringBuilder(); - getProjectedFieldsInternal(b, null, sb, persistentEntity, fields, distinctFields != null); + getProjectedFieldsInternal(b, null, sb, persistentEntity, typeField, fields, distinctFields != null); projectedFields = sb.toString(); } return projectedFields; } private void getProjectedFieldsInternal(String bucketName, CouchbasePersistentProperty parent, StringBuilder sb, - PersistentEntity persistentEntity, String[] fields, boolean forDistinct) { + PersistentEntity persistentEntity, String typeField, String[] fields, boolean forDistinct) { if (persistentEntity != null) { Set fieldList = fields != null ? new HashSet<>(Arrays.asList(fields)) : null; @@ -216,6 +217,8 @@ private void getProjectedFieldsInternal(String bucketName, CouchbasePersistentPr if (prop == persistentEntity.getVersionProperty() && parent == null) { return; } + if( prop.getFieldName().equals(typeField)) // typeField already projected + return; // for distinct when no distinctFields were provided, do not include the expiration field. if (forDistinct && prop.findAnnotation(Expiration.class) != null && parent == null) { return; @@ -253,8 +256,10 @@ private void getProjectedFieldsInternal(String bucketName, CouchbasePersistentPr } } else { for (String field : fields) { - if (sb.length() > 0) { - sb.append(", "); + if (!field.equals(typeField)) { // typeField is already projected + if (sb.length() > 0) { + sb.append(", "); + } } sb.append(x(field)); } diff --git a/src/test/java/org/springframework/data/couchbase/domain/AbstractUser.java b/src/test/java/org/springframework/data/couchbase/domain/AbstractUser.java new file mode 100644 index 000000000..00d106d15 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/AbstractUser.java @@ -0,0 +1,105 @@ +/* + * Copyright 2012-2020 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.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; + +/** + * User entity for tests + * + * @author Michael Reiche + */ + +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; + + public String getId() { + return id; + } + + 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 new file mode 100644 index 000000000..5b6f3f85b --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/AbstractUserRepository.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2021 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.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; + +/** + * AbstractUser Repository for tests + * + * @author Michael Reiche + */ +@Repository +@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); + + Stream findByLastname(String lastname); + + List findByFirstnameIn(String... firstnames); + + List findByFirstnameIn(JsonArray firstnames); + + List findByFirstnameAndLastname(String firstname, String lastname); + + @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and firstname = $1 and lastname = $2") + List getByFirstnameAndLastname(String firstname, String lastname); + + @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and (firstname = $first or lastname = $last)") + List getByFirstnameOrLastname(@Param("first") String firstname, @Param("last") String lastname); + + List findByIdIsNotNullAndFirstnameEquals(String firstname); + + List findByVersionEqualsAndFirstnameEquals(Long version, String firstname); + +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/Airport.java b/src/test/java/org/springframework/data/couchbase/domain/Airport.java index 041436264..bb436d6b5 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Airport.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Airport.java @@ -35,7 +35,7 @@ @Document @TypeAlias("airport") public class Airport extends ComparableEntity { - @Id String id; + @Id String key; String iata; @@ -49,14 +49,14 @@ public class Airport extends ComparableEntity { long size; @PersistenceConstructor - public Airport(String id, String iata, String icao) { - this.id = id; + public Airport(String key, String iata, String icao) { + this.key = key; this.iata = iata; this.icao = icao; } public String getId() { - return id; + return key; } public String getIata() { diff --git a/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java b/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java index 7890b28ca..e5f460182 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java @@ -174,6 +174,9 @@ Long countFancyExpression(@Param("projectIds") List projectIds, @Param(" + " #{#planIds != null ? 'AND blahblah IN $2' : ''} " + " #{#active != null ? 'AND false = $3' : ''} ") Long countOne(); + @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) + Airport findByKey(String id); + @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE }) // @Meta diff --git a/src/test/java/org/springframework/data/couchbase/domain/OtherUser.java b/src/test/java/org/springframework/data/couchbase/domain/OtherUser.java new file mode 100644 index 000000000..4d5468654 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/OtherUser.java @@ -0,0 +1,38 @@ +/* + * 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.annotation.PersistenceConstructor; +import org.springframework.data.couchbase.core.mapping.Document; + +/** + * OtherUser entity for tests. Both User and OtherUser extend AbstractUser + * + * @author Michael Reiche + */ + +@Document +public class OtherUser extends AbstractUser { + + @PersistenceConstructor + public OtherUser(final String id, final String firstname, final String lastname) { + this.id = id; + this.firstname = firstname; + this.lastname = lastname; + } + +} 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 d005e7a99..55ca67ebf 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/User.java +++ b/src/test/java/org/springframework/data/couchbase/domain/User.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,16 +16,7 @@ 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; /** @@ -36,17 +27,7 @@ */ @Document -public class User extends ComparableEntity { - - @Version long version; - @Id private String id; - private String firstname; - private String lastname; - @Transient private String transientInfo; - @CreatedBy private String createdBy; - @CreatedDate private long createdDate; - @LastModifiedBy private String lastModifiedBy; - @LastModifiedDate private long lastModifiedDate; +public class User extends AbstractUser { @PersistenceConstructor public User(final String id, final String firstname, final String lastname) { @@ -55,60 +36,4 @@ public User(final String id, final String firstname, final String lastname) { this.lastname = lastname; } - public String getId() { - return id; - } - - 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(id, 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 new file mode 100644 index 000000000..3eb99d838 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseAbstractRepositoryIntegrationTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2017-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.repository; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +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.Configuration; +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +import org.springframework.data.couchbase.domain.AbstractUser; +import org.springframework.data.couchbase.domain.AbstractUserRepository; +import org.springframework.data.couchbase.domain.OtherUser; +import org.springframework.data.couchbase.domain.User; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +/** + * Abstract Repository tests + * + * @author Michael Reiche + */ +@SpringJUnitConfig(CouchbaseAbstractRepositoryIntegrationTests.Config.class) +@IgnoreWhen(clusterTypes = ClusterType.MOCKED) +public class CouchbaseAbstractRepositoryIntegrationTests extends ClusterAwareIntegrationTests { + + @Autowired AbstractUserRepository abstractUserRepository; + + @Test + 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); + { + // 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()); + } + { + Optional found = abstractUserRepository.findById(user.getId()); + assertEquals(user, found.get()); + } + abstractUserRepository.delete(user); + } + { + user = new OtherUser(UUID.randomUUID().toString(), "userFirstname", "userLastname"); + assertEquals(OtherUser.class, user.getClass()); + abstractUserRepository.save(user); + { + AbstractUser found = abstractUserRepository.myFindById(user.getId()); + assertEquals(user, found); + assertEquals(user.getClass(), found.getClass()); + } + { + Optional found = abstractUserRepository.findById(user.getId()); + assertEquals(user, found.get()); + } + abstractUserRepository.delete(user); + } + + } + + @Configuration + @EnableCouchbaseRepositories("org.springframework.data.couchbase") + static class Config extends AbstractCouchbaseConfiguration { + + @Override + public String getConnectionString() { + return connectionString(); + } + + @Override + public String getUserName() { + return config().adminUsername(); + } + + @Override + public String getPassword() { + return config().adminPassword(); + } + + @Override + public String getBucketName() { + return bucketName(); + } + + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java index 343157d75..a954494a0 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -249,11 +249,11 @@ void issue1304CollectionParameter() { java.util.Collection icaos = new LinkedList(); icaos.add(vie.getIcao()); icaos.add("blue"); - PageRequest pageable = PageRequest.of( 0, 1, Sort.by("iata")); - Listairports = airportRepository.findByIataInAndIcaoIn(iatas, icaos, pageable); + PageRequest pageable = PageRequest.of(0, 1, Sort.by("iata")); + List airports = airportRepository.findByIataInAndIcaoIn(iatas, icaos, pageable); assertEquals(1, airports.size()); - Listairports2 = airportRepository.findByIataInAndIcaoIn(iatas, icaos, pageable); + List airports2 = airportRepository.findByIataInAndIcaoIn(iatas, icaos, pageable); assertEquals(1, airports2.size()); } finally { @@ -624,11 +624,12 @@ void sortedRepository() { airportRepository.saveAll( Arrays.stream(iatas).map((iata) -> new Airport("airports::" + iata, iata, iata.toLowerCase(Locale.ROOT))) .collect(Collectors.toSet())); - List airports = airportRepository.withOptions(QueryOptions.queryOptions().scanConsistency(REQUEST_PLUS)).findAll(Sort.by("iata")); + List airports = airportRepository.withOptions(QueryOptions.queryOptions().scanConsistency(REQUEST_PLUS)) + .findAll(Sort.by("iata")); String[] sortedIatas = iatas.clone(); - System.out.println(""+iatas.length+" "+sortedIatas.length); + System.out.println("" + iatas.length + " " + sortedIatas.length); Arrays.sort(sortedIatas); - for(int i=0; i< sortedIatas.length; i++){ + for (int i = 0; i < sortedIatas.length; i++) { assertEquals(sortedIatas[i], airports.get(i).getIata()); } } finally { @@ -924,6 +925,15 @@ void findPlusN1qlJoin() throws Exception { .all(Arrays.asList(address1.getId(), address2.getId(), address3.getId(), user.getId())); } + @Test + void findByKey() { + Airport airport = new Airport(UUID.randomUUID().toString(), "iata1038", "icao"); + airportRepository.save(airport); + Airport found = airportRepository.findByKey(airport.getId()); + assertEquals(airport, found); + airportRepository.delete(airport); + } + private void sleep(int millis) { try { Thread.sleep(millis); // so they are executed out-of-order 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 66f124e3a..c4594f22a 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, `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`, `firstname`, `lastname`, `createdBy`, `createdDate`, `lastModifiedBy`, `lastModifiedDate` FROM `travel-sample` where `_class` = \"org.springframework.data.couchbase.domain.User\" 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, `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`, `firstname`, `lastname`, `createdBy`, `createdDate`, `lastModifiedBy`, `lastModifiedDate` FROM `travel-sample` where `_class` = \"org.springframework.data.couchbase.domain.User\" and (firstname = $first or lastname = $last)", query.toN1qlSelectString(couchbaseTemplate.reactive(), User.class, false)); }