diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java index 9e5bc8b01..6be2ea505 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java @@ -402,7 +402,7 @@ public void write(final Object source, final CouchbaseDocument target) { typeMapper.writeType(type, target); } - writeInternal(source, target, type); + writeInternal(source, target, type, true); if (target.getId() == null) { throw new MappingException("An ID property is needed, but not found/could not be generated on this entity."); } @@ -416,7 +416,8 @@ public void write(final Object source, final CouchbaseDocument target) { * @param typeHint the type information for the source. */ @SuppressWarnings("unchecked") - protected void writeInternal(final Object source, CouchbaseDocument target, final TypeInformation typeHint) { + protected void writeInternal(final Object source, CouchbaseDocument target, final TypeInformation typeHint, + boolean withId) { if (source == null) { return; } @@ -437,7 +438,7 @@ protected void writeInternal(final Object source, CouchbaseDocument target, fina } CouchbasePersistentEntity entity = mappingContext.getPersistentEntity(source.getClass()); - writeInternal(source, target, entity); + writeInternal(source, target, entity, withId); addCustomTypeKeyIfNecessary(typeHint, source, target); } @@ -471,9 +472,10 @@ private String convertToString(Object propertyObj) { * @param source the source object. * @param target the target document. * @param entity the persistent entity to convert from. + * @param withId one of the top-level properties is the id for the document */ protected void writeInternal(final Object source, final CouchbaseDocument target, - final CouchbasePersistentEntity entity) { + final CouchbasePersistentEntity entity, boolean withId) { if (source == null) { return; } @@ -483,7 +485,7 @@ protected void writeInternal(final Object source, final CouchbaseDocument target } final ConvertingPropertyAccessor accessor = getPropertyAccessor(source); - final CouchbasePersistentProperty idProperty = entity.getIdProperty(); + final CouchbasePersistentProperty idProperty = withId ? entity.getIdProperty() : null; final CouchbasePersistentProperty versionProperty = entity.getVersionProperty(); GeneratedValue generatedValueInfo = null; @@ -525,7 +527,7 @@ public void doWithPersistentProperty(final CouchbasePersistentProperty prop) { } if (!conversions.isSimpleType(propertyObj.getClass())) { - writePropertyInternal(propertyObj, target, prop); + writePropertyInternal(propertyObj, target, prop, false); } else { writeSimpleInternal(propertyObj, target, prop.getFieldName()); } @@ -552,7 +554,7 @@ public void doWithAssociation(final Association ass Class type = inverseProp.getType(); Object propertyObj = accessor.getProperty(inverseProp, type); if (null != propertyObj) { - writePropertyInternal(propertyObj, target, inverseProp); + writePropertyInternal(propertyObj, target, inverseProp, false); } } }); @@ -568,7 +570,7 @@ public void doWithAssociation(final Association ass */ @SuppressWarnings("unchecked") private void writePropertyInternal(final Object source, final CouchbaseDocument target, - final CouchbasePersistentProperty prop) { + final CouchbasePersistentProperty prop, boolean withId) { if (source == null) { return; } @@ -617,7 +619,7 @@ private void writePropertyInternal(final Object source, final CouchbaseDocument CouchbasePersistentEntity entity = isSubtype(prop.getType(), source.getClass()) ? mappingContext.getRequiredPersistentEntity(source.getClass()) : mappingContext.getRequiredPersistentEntity(type); - writeInternal(source, propertyDoc, entity); + writeInternal(source, propertyDoc, entity, false); target.put(name, propertyDoc); } @@ -660,7 +662,7 @@ private CouchbaseDocument writeMapInternal(final Map source, fin } else { CouchbaseDocument embeddedDoc = new CouchbaseDocument(); TypeInformation valueTypeInfo = type.isMap() ? type.getMapValueType() : ClassTypeInformation.OBJECT; - writeInternal(val, embeddedDoc, valueTypeInfo); + writeInternal(val, embeddedDoc, valueTypeInfo, false); target.put(simpleKey, embeddedDoc); } } else { @@ -706,7 +708,7 @@ private CouchbaseList writeCollectionInternal(final Collection source, final } else { CouchbaseDocument embeddedDoc = new CouchbaseDocument(); - writeInternal(element, embeddedDoc, componentType); + writeInternal(element, embeddedDoc, componentType, false); target.put(embeddedDoc); } @@ -937,7 +939,7 @@ public R getPropertyValue(final CouchbasePersistentProperty property) { String expression = property.getSpelExpression(); Object value = expression != null ? evaluator.evaluate(expression) : source.get(property.getFieldName()); - if (property.isIdProperty()) { + if (property.isIdProperty() && parent == null) { return (R) source.getId(); } if (value == null) { diff --git a/src/test/java/org/springframework/data/couchbase/domain/Course.java b/src/test/java/org/springframework/data/couchbase/domain/Course.java new file mode 100644 index 000000000..50bd02460 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/Course.java @@ -0,0 +1,93 @@ +/* + * Copyright 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 org.springframework.data.annotation.Id; + +import java.lang.reflect.Field; + +/** + * Course entity for tests + * + * @author Michael Reiche + */ +public class Course { + @Id private final String id; + private final String userId; + private final String room; + + public Course(String id, String userId, String room) { + this.id = id; + this.userId = userId; + this.room = room; + } + + public String getId() { + return id; + } + + public String toString() { + StringBuffer sb = new StringBuffer("Course("); + sb.append("id="); + sb.append(id); + sb.append(", userId="); + sb.append(userId); + sb.append(", room="); + sb.append(room); + sb.append(")"); + return sb.toString(); + } + + @Override + public boolean equals(Object that) throws RuntimeException { + if (this == that) + return true; + if (that == null || getClass() != that.getClass()) + return false; + for (Field f : getClass().getFields()) { + if (!same(f, this, that)) + return false; + } + for (Field f : getClass().getDeclaredFields()) { + if (!same(f, this, that)) + return false; + } + return true; + } + + private static boolean same(Field f, Object a, Object b) { + Object thisField = null; + Object thatField = null; + + try { + f.get(a); + f.get(b); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + if (thisField == null && thatField == null) { + return true; + } + if (thisField == null && thatField != null) { + return false; + } + if (!thisField.equals(thatField)) { + return false; + } + return true; + } +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/Submission.java b/src/test/java/org/springframework/data/couchbase/domain/Submission.java new file mode 100644 index 000000000..1522693a2 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/Submission.java @@ -0,0 +1,99 @@ +/* + * Copyright 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.lang.reflect.Field; + +/** + * Submission entity for tests + * + * @author Michael Reiche + */ +public class Submission { + private final String id; + private final String userId; + private final String talkId; + private final String status; + private final long number; + + public Submission(String id, String userId, String talkId, String status, long number) { + this.id = id; + this.userId = userId; + this.talkId = talkId; + this.status = status; + this.number = number; + } + + public String getId() { + return id; + } + + public String toString() { + StringBuffer sb = new StringBuffer("Submission("); + sb.append("id="); + sb.append(id); + sb.append(", userId="); + sb.append(userId); + sb.append(", talkId="); + sb.append(talkId); + sb.append(", status="); + sb.append(status); + sb.append(", number="); + sb.append(number); + sb.append(")"); + return sb.toString(); + } + + @Override + public boolean equals(Object that) throws RuntimeException { + if (this == that) + return true; + if (that == null || getClass() != that.getClass()) + return false; + for (Field f : getClass().getFields()) { + if (!same(f, this, that)) + return false; + } + for (Field f : getClass().getDeclaredFields()) { + if (!same(f, this, that)) + return false; + } + return true; + } + + private static boolean same(Field f, Object a, Object b) { + Object thisField = null; + Object thatField = null; + + try { + f.get(a); + f.get(b); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + if (thisField == null && thatField == null) { + return true; + } + if (thisField == null && thatField != null) { + return false; + } + if (!thisField.equals(thatField)) { + return false; + } + return true; + } +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserSubmission.java b/src/test/java/org/springframework/data/couchbase/domain/UserSubmission.java new file mode 100644 index 000000000..78d4975b5 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/UserSubmission.java @@ -0,0 +1,93 @@ +/* + * 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 lombok.Data; +import org.springframework.data.annotation.TypeAlias; +import org.springframework.data.couchbase.core.index.CompositeQueryIndex; +import org.springframework.data.couchbase.core.mapping.Document; + +import java.lang.reflect.Field; +import java.util.List; + +/** + * UserSubmission entity for tests + * + * @author Michael Reiche + */ +@Data +@Document +@TypeAlias("user") +@CompositeQueryIndex(fields = { "id", "username", "email" }) +public class UserSubmission { + private String id; + private String username; + private String email; + private String password; + private List roles; + private Address address; + private int credits; + private List submissions; + private List courses; + + public void setSubmissions(List submissions) { + this.submissions = submissions; + } + + public void setCourses(List courses) { + this.courses = courses; + } + + @Override + public boolean equals(Object that) throws RuntimeException { + if (this == that) + return true; + if (that == null || getClass() != that.getClass()) + return false; + for (Field f : getClass().getFields()) { + if (!same(f, this, that)) + return false; + } + for (Field f : getClass().getDeclaredFields()) { + if (!same(f, this, that)) + return false; + } + return true; + } + + private static boolean same(Field f, Object a, Object b) { + Object thisField = null; + Object thatField = null; + + try { + f.get(a); + f.get(b); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + if (thisField == null && thatField == null) { + return true; + } + if (thisField == null && thatField != null) { + return false; + } + if (!thisField.equals(thatField)) { + return false; + } + return true; + } +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionRepository.java b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionRepository.java new file mode 100644 index 000000000..d73f8b725 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionRepository.java @@ -0,0 +1,30 @@ +/* + * 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 org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.stereotype.Repository; + +/** + * UserSubmission Repository for tests + * + * @author Michael Reiche + */ +@Repository +public interface UserSubmissionRepository extends PagingAndSortingRepository { + +} diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java index b375c6370..232f7aee0 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java @@ -19,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.*; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -27,10 +28,14 @@ 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.Course; import org.springframework.data.couchbase.domain.Library; import org.springframework.data.couchbase.domain.LibraryRepository; +import org.springframework.data.couchbase.domain.Submission; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserRepository; +import org.springframework.data.couchbase.domain.UserSubmission; +import org.springframework.data.couchbase.domain.UserSubmissionRepository; import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; import org.springframework.data.couchbase.util.ClusterType; @@ -49,6 +54,7 @@ public class CouchbaseRepositoryKeyValueIntegrationTests extends ClusterAwareInt @Autowired UserRepository userRepository; @Autowired LibraryRepository libraryRepository; + @Autowired UserSubmissionRepository userSubmissionRepository; @Test @IgnoreWhen(clusterTypes = ClusterType.MOCKED) @@ -89,6 +95,31 @@ void saveAndFindByIdWithList() { assertFalse(userRepository.existsById(library.getId())); } + @Test + @IgnoreWhen(clusterTypes = ClusterType.MOCKED) + void saveAndFindByWithNestedId() { + UserSubmission user = new UserSubmission(); + user.setId(UUID.randomUUID().toString()); + user.setSubmissions( + Arrays.asList(new Submission(UUID.randomUUID().toString(), user.getId(), "tid", "status", 123))); + user.setCourses(Arrays.asList(new Course(UUID.randomUUID().toString(), user.getId(), "581"))); + + // this currently fails when using mocked in integration.properties with status "UNKNOWN" + assertFalse(userRepository.existsById(user.getId())); + + userSubmissionRepository.save(user); + + Optional found = userSubmissionRepository.findById(user.getId()); + assertTrue(found.isPresent()); + found.ifPresent(u -> assertEquals(user, u)); + + assertTrue(userRepository.existsById(user.getId())); + assertEquals(user.getSubmissions().get(0).getId(), found.get().getSubmissions().get(0).getId()); + assertEquals(user.getCourses().get(0).getId(), found.get().getCourses().get(0).getId()); + assertEquals(user, found.get()); + userSubmissionRepository.delete(user); + } + @Configuration @EnableCouchbaseRepositories("org.springframework.data.couchbase") static class Config extends AbstractCouchbaseConfiguration {