Restore projection of class. (#1319)

Restore projection of class in queries. This is necessary for abstract repositories.

Closes #1315.
This commit is contained in:
Michael Reiche
2022-02-03 06:19:32 -08:00
committed by GitHub
parent 8f8efbd16f
commit 895b2b270e
10 changed files with 361 additions and 97 deletions

View File

@@ -162,11 +162,12 @@ public class StringBasedN1qlQueryParser {
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 class StringBasedN1qlQueryParser {
} 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 class StringBasedN1qlQueryParser {
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 @@ public class StringBasedN1qlQueryParser {
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<String> fieldList = fields != null ? new HashSet<>(Arrays.asList(fields)) : null;
@@ -216,6 +217,8 @@ public class StringBasedN1qlQueryParser {
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 @@ public class StringBasedN1qlQueryParser {
}
} 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));
}

View File

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

View File

@@ -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<AbstractUser, String> {
@Query("#{#n1ql.selectEntity} where (meta().id = $1)")
AbstractUser myFindById(String id);
List<User> findByFirstname(String firstname);
Stream<User> findByLastname(String lastname);
List<User> findByFirstnameIn(String... firstnames);
List<User> findByFirstnameIn(JsonArray firstnames);
List<User> findByFirstnameAndLastname(String firstname, String lastname);
@Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and firstname = $1 and lastname = $2")
List<User> getByFirstnameAndLastname(String firstname, String lastname);
@Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and (firstname = $first or lastname = $last)")
List<User> getByFirstnameOrLastname(@Param("first") String firstname, @Param("last") String lastname);
List<User> findByIdIsNotNullAndFirstnameEquals(String firstname);
List<User> findByVersionEqualsAndFirstnameEquals(Long version, String firstname);
}

View File

@@ -35,7 +35,7 @@ import javax.validation.constraints.Max;
@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() {

View File

@@ -174,6 +174,9 @@ public interface AirportRepository extends CouchbaseRepository<Airport, String>,
+ " #{#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

View File

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

View File

@@ -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 @@ import org.springframework.data.couchbase.core.mapping.Document;
*/
@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 class User extends ComparableEntity {
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;
}
}

View File

@@ -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 = <classname> ) as the classname will not match any document
AbstractUser found = abstractUserRepository.myFindById(user.getId());
assertEquals(user, found);
assertEquals(user.getClass(), found.getClass());
}
{
Optional<AbstractUser> 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<AbstractUser> 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();
}
}
}

View File

@@ -249,11 +249,11 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
java.util.Collection<String> icaos = new LinkedList<String>();
icaos.add(vie.getIcao());
icaos.add("blue");
PageRequest pageable = PageRequest.of( 0, 1, Sort.by("iata"));
List<Airport>airports = airportRepository.findByIataInAndIcaoIn(iatas, icaos, pageable);
PageRequest pageable = PageRequest.of(0, 1, Sort.by("iata"));
List<Airport> airports = airportRepository.findByIataInAndIcaoIn(iatas, icaos, pageable);
assertEquals(1, airports.size());
List<Airport>airports2 = airportRepository.findByIataInAndIcaoIn(iatas, icaos, pageable);
List<Airport> airports2 = airportRepository.findByIataInAndIcaoIn(iatas, icaos, pageable);
assertEquals(1, airports2.size());
} finally {
@@ -624,11 +624,12 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
airportRepository.saveAll(
Arrays.stream(iatas).map((iata) -> new Airport("airports::" + iata, iata, iata.toLowerCase(Locale.ROOT)))
.collect(Collectors.toSet()));
List<Airport> airports = airportRepository.withOptions(QueryOptions.queryOptions().scanConsistency(REQUEST_PLUS)).findAll(Sort.by("iata"));
List<Airport> 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 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
.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

View File

@@ -85,7 +85,7 @@ class StringN1qlQueryCreatorMockedTests extends ClusterAwareIntegrationTests {
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 @@ class StringN1qlQueryCreatorMockedTests extends ClusterAwareIntegrationTests {
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));
}