diff --git a/pom.xml b/pom.xml
index 298dcb7341..9a48c88472 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data
spring-data-relational-parent
- 3.0.0-SNAPSHOT
+ 3.0.0-1249-readonly-reference-SNAPSHOT
pom
Spring Data Relational Parent
diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml
index db3b7ddd1a..60ee583a39 100644
--- a/spring-data-jdbc-distribution/pom.xml
+++ b/spring-data-jdbc-distribution/pom.xml
@@ -14,7 +14,7 @@
org.springframework.data
spring-data-relational-parent
- 3.0.0-SNAPSHOT
+ 3.0.0-1249-readonly-reference-SNAPSHOT
../pom.xml
diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml
index 547ff62b8b..fc7e0ab454 100644
--- a/spring-data-jdbc/pom.xml
+++ b/spring-data-jdbc/pom.xml
@@ -6,7 +6,7 @@
4.0.0
spring-data-jdbc
- 3.0.0-SNAPSHOT
+ 3.0.0-1249-readonly-reference-SNAPSHOT
Spring Data JDBC
Spring Data module for JDBC repositories.
@@ -15,7 +15,7 @@
org.springframework.data
spring-data-relational-parent
- 3.0.0-SNAPSHOT
+ 3.0.0-1249-readonly-reference-SNAPSHOT
diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/PersistentPropertyPathExtensionUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/PersistentPropertyPathExtensionUnitTests.java
index 9751351a2d..d343e4fc70 100644
--- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/PersistentPropertyPathExtensionUnitTests.java
+++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/PersistentPropertyPathExtensionUnitTests.java
@@ -15,6 +15,7 @@
*/
package org.springframework.data.jdbc.core;
+import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.SoftAssertions.*;
import static org.springframework.data.relational.core.sql.SqlIdentifier.*;
@@ -22,6 +23,7 @@
import org.junit.jupiter.api.Test;
import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.ReadOnlyProperty;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.relational.core.mapping.Embedded;
@@ -202,7 +204,7 @@ void extendBy() {
});
}
- @Test // GH--1164
+ @Test // GH-1164
void equalsWorks() {
PersistentPropertyPathExtension root1 = extPath(entity);
@@ -222,6 +224,17 @@ void equalsWorks() {
});
}
+ @Test // GH-1249
+ void isWritable() {
+
+ assertSoftly(softly -> {
+ softly.assertThat(PersistentPropertyPathExtension.isWritable(createSimplePath("withId"))).describedAs("simple path is writable").isTrue();
+ softly.assertThat(PersistentPropertyPathExtension.isWritable(createSimplePath("secondList.third2"))).describedAs("long path is writable").isTrue();
+ softly.assertThat(PersistentPropertyPathExtension.isWritable(createSimplePath("second"))).describedAs("simple read only path is not writable").isFalse();
+ softly.assertThat(PersistentPropertyPathExtension.isWritable(createSimplePath("second.third"))).describedAs("long path containing read only element is not writable").isFalse();
+ });
+ }
+
private PersistentPropertyPathExtension extPath(RelationalPersistentEntity> entity) {
return new PersistentPropertyPathExtension(context, entity);
}
@@ -237,6 +250,7 @@ PersistentPropertyPath createSimplePath(String pat
@SuppressWarnings("unused")
static class DummyEntity {
@Id Long entityId;
+ @ReadOnlyProperty
Second second;
@Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "sec") Second second2;
@Embedded(onEmpty = OnEmpty.USE_NULL) Second second3;
diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml
index 22914016c3..f8f11f42b0 100644
--- a/spring-data-r2dbc/pom.xml
+++ b/spring-data-r2dbc/pom.xml
@@ -6,7 +6,7 @@
4.0.0
spring-data-r2dbc
- 3.0.0-SNAPSHOT
+ 3.0.0-1249-readonly-reference-SNAPSHOT
Spring Data R2DBC
Spring Data module for R2DBC
@@ -15,7 +15,7 @@
org.springframework.data
spring-data-relational-parent
- 3.0.0-SNAPSHOT
+ 3.0.0-1249-readonly-reference-SNAPSHOT
diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml
index 717ac86edf..4b447ebe6f 100644
--- a/spring-data-relational/pom.xml
+++ b/spring-data-relational/pom.xml
@@ -6,7 +6,7 @@
4.0.0
spring-data-relational
- 3.0.0-SNAPSHOT
+ 3.0.0-1249-readonly-reference-SNAPSHOT
Spring Data Relational
Spring Data Relational support
@@ -14,7 +14,7 @@
org.springframework.data
spring-data-relational-parent
- 3.0.0-SNAPSHOT
+ 3.0.0-1249-readonly-reference-SNAPSHOT
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriter.java
index 1aa436374f..047276fef8 100644
--- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriter.java
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriter.java
@@ -18,11 +18,13 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.function.Consumer;
import org.springframework.data.convert.EntityWriter;
-import org.springframework.data.mapping.PersistentProperty;
+import org.springframework.data.mapping.PersistentPropertyPath;
+import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
-import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
+import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -72,8 +74,7 @@ private List> deleteAll(Class> entityType) {
List> deleteReferencedActions = new ArrayList<>();
- context.findPersistentPropertyPaths(entityType, PersistentProperty::isEntity)
- .filter(p -> !p.getRequiredLeafProperty().isEmbedded()).forEach(p -> deleteReferencedActions.add(new DbAction.DeleteAll<>(p)));
+ forAllTableRepresentingPaths(entityType, p -> deleteReferencedActions.add(new DbAction.DeleteAll<>(p)));
Collections.reverse(deleteReferencedActions);
@@ -114,12 +115,18 @@ private List> deleteReferencedEntities(Object id, AggregateChange>
List> actions = new ArrayList<>();
- context.findPersistentPropertyPaths(aggregateChange.getEntityType(), PersistentProperty::isEntity)
- .filter(p -> !p.getRequiredLeafProperty().isEmbedded()).forEach(p -> actions.add(new DbAction.Delete<>(id, p)));
+ forAllTableRepresentingPaths(aggregateChange.getEntityType(), p -> actions.add(new DbAction.Delete<>(id, p)));
Collections.reverse(actions);
return actions;
}
+ private void forAllTableRepresentingPaths(Class> entityType,
+ Consumer> pathConsumer) {
+
+ context.findPersistentPropertyPaths(entityType, property -> property.isEntity() && !property.isEmbedded()) //
+ .filter(PersistentPropertyPathExtension::isWritable) //
+ .forEach(pathConsumer);
+ }
}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java
index a453d279fb..8ac604d8ea 100644
--- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java
@@ -25,7 +25,7 @@
import java.util.stream.Collectors;
import org.springframework.data.mapping.PersistentPropertyPath;
-import org.springframework.data.mapping.PersistentPropertyPaths;
+import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
@@ -47,7 +47,7 @@ class WritingContext {
private final RelationalMappingContext context;
private final T root;
private final Class entityType;
- private final PersistentPropertyPaths, RelationalPersistentProperty> paths;
+ private final List> paths;
private final Map> previousActions = new HashMap<>();
private final Map, List> nodesCache = new HashMap<>();
private final IdValueSource rootIdValueSource;
@@ -63,7 +63,9 @@ class WritingContext {
this.aggregateChange = aggregateChange;
this.rootIdValueSource = IdValueSource.forInstance(root,
context.getRequiredPersistentEntity(aggregateChange.getEntityType()));
- this.paths = context.findPersistentPropertyPaths(entityType, (p) -> p.isEntity() && !p.isEmbedded());
+ this.paths = context.findPersistentPropertyPaths(entityType, (p) -> p.isEntity() && !p.isEmbedded()) //
+ .filter(PersistentPropertyPathExtension::isWritable) //
+ .stream().toList();
}
/**
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java
index 690128553a..b60ceb202a 100644
--- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java
@@ -80,6 +80,11 @@ public PersistentPropertyPathExtension(
this.path = path;
}
+ public static boolean isWritable(PersistentPropertyPath extends RelationalPersistentProperty> path) {
+
+ return path.isEmpty() || (path.getRequiredLeafProperty().isWritable() && isWritable(path.getParentPath()));
+ }
+
/**
* Returns {@literal true} exactly when the path is non empty and the leaf property an embedded one.
*
diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriterUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriterUnitTests.java
index c7c2b02547..6dae85fa00 100644
--- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriterUnitTests.java
+++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriterUnitTests.java
@@ -20,12 +20,14 @@
import java.util.ArrayList;
import java.util.List;
+import lombok.RequiredArgsConstructor;
import org.assertj.core.api.Assertions;
import org.assertj.core.groups.Tuple;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.ReadOnlyProperty;
import org.springframework.data.relational.core.conversion.DbAction.AcquireLockAllRoot;
import org.springframework.data.relational.core.conversion.DbAction.AcquireLockRoot;
import org.springframework.data.relational.core.conversion.DbAction.Delete;
@@ -108,6 +110,39 @@ public void deleteAllDeletesAllEntitiesAndNoReferencedEntities() {
.containsExactly(Tuple.tuple(DeleteAllRoot.class, SingleEntity.class, ""));
}
+ @Test // GH-1249
+ public void deleteDoesNotDeleteReadOnlyReferences() {
+
+ WithReadOnlyReference entity = new WithReadOnlyReference(23L);
+
+ MutableAggregateChange aggregateChange = MutableAggregateChange.forDelete(WithReadOnlyReference.class);
+
+ converter.write(entity.id, aggregateChange);
+
+ Assertions.assertThat(extractActions(aggregateChange))
+ .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath) //
+ .containsExactly( //
+ Tuple.tuple(DeleteRoot.class, WithReadOnlyReference.class, "") //
+ );
+ }
+
+ @Test // GH-1249
+ public void deleteAllDoesNotDeleteReadOnlyReferences() {
+
+ WithReadOnlyReference entity = new WithReadOnlyReference(23L);
+
+ MutableAggregateChange aggregateChange = MutableAggregateChange.forDelete(WithReadOnlyReference.class);
+
+ converter.write(null, aggregateChange);
+
+ Assertions.assertThat(extractActions(aggregateChange))
+ .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath) //
+ .containsExactly( //
+ Tuple.tuple(DeleteAllRoot.class, WithReadOnlyReference.class, "") //
+ );
+ }
+
+
private List> extractActions(MutableAggregateChange> aggregateChange) {
List> actions = new ArrayList<>();
@@ -141,4 +176,11 @@ private class SingleEntity {
@Id final Long id;
String name;
}
+
+ @RequiredArgsConstructor
+ private static class WithReadOnlyReference {
+ @Id final Long id;
+ @ReadOnlyProperty
+ OtherEntity other;
+ }
}
diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriterUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriterUnitTests.java
index d0e483ec0f..b3a7bed67e 100644
--- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriterUnitTests.java
+++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriterUnitTests.java
@@ -26,6 +26,7 @@
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.ReadOnlyProperty;
import org.springframework.data.relational.core.conversion.DbAction.InsertRoot;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
@@ -75,6 +76,7 @@ public void existingEntityGetsNotConvertedToDeletePlusUpdate() {
}
+
private List> extractActions(MutableAggregateChange> aggregateChange) {
List> actions = new ArrayList<>();
diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java
index 8405479aac..9870ed9fe6 100644
--- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java
+++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java
@@ -33,6 +33,7 @@
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.ReadOnlyProperty;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PersistentPropertyPaths;
import org.springframework.data.relational.core.conversion.DbAction.Delete;
@@ -817,6 +818,46 @@ void newEntityWithCollection_whenElementHasPrimitiveId_batchInsertDoesNotInclude
);
}
+ @Test // GH-1249
+ public void readOnlyReferenceDoesNotCreateInsertsOnCreation() {
+
+ WithReadOnlyReference entity = new WithReadOnlyReference(null);
+ entity.readOnly = new Element(SOME_ENTITY_ID);
+
+ AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity);
+
+ new RelationalEntityWriter(context).write(entity, aggregateChange);
+
+ assertThat(extractActions(aggregateChange)) //
+ .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath,
+ DbActionTestSupport::actualEntityType, DbActionTestSupport::isWithDependsOn) //
+ .containsExactly( //
+ tuple(InsertRoot.class, WithReadOnlyReference.class, "", WithReadOnlyReference.class, false) //
+ // no insert for element
+ );
+
+ }
+
+ @Test // GH-1249
+ public void readOnlyReferenceDoesNotCreateDeletesOrInsertsDuringUpdate() {
+
+ WithReadOnlyReference entity = new WithReadOnlyReference(SOME_ENTITY_ID);
+ entity.readOnly = new Element(SOME_ENTITY_ID);
+
+ AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity);
+
+ new RelationalEntityWriter(context).write(entity, aggregateChange);
+
+ assertThat(extractActions(aggregateChange)) //
+ .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath,
+ DbActionTestSupport::actualEntityType, DbActionTestSupport::isWithDependsOn) //
+ .containsExactly( //
+ tuple(UpdateRoot.class, WithReadOnlyReference.class, "", WithReadOnlyReference.class, false) //
+ // no insert for element
+ );
+
+ }
+
private List> extractActions(MutableAggregateChange> aggregateChange) {
List> actions = new ArrayList<>();
@@ -1015,4 +1056,13 @@ private static class NoIdElement {
// empty classes feel weird.
String name;
}
+
+ @RequiredArgsConstructor
+ private static class WithReadOnlyReference {
+
+ @Id final Long id;
+ @ReadOnlyProperty
+ Element readOnly;
+ }
+
}
diff --git a/src/main/asciidoc/jdbc.adoc b/src/main/asciidoc/jdbc.adoc
index a0d2cc37e7..5ecad2f46b 100644
--- a/src/main/asciidoc/jdbc.adoc
+++ b/src/main/asciidoc/jdbc.adoc
@@ -417,13 +417,24 @@ include::{spring-data-commons-docs}/is-new-state-detection.adoc[leveloffset=+2]
Spring Data JDBC uses the ID to identify entities.
The ID of an entity must be annotated with Spring Data's https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/annotation/Id.html[`@Id`] annotation.
-When your data base has an auto-increment column for the ID column, the generated value gets set in the entity after inserting it into the database.
+When your database has an auto-increment column for the ID column, the generated value gets set in the entity after inserting it into the database.
One important constraint is that, after saving an entity, the entity must not be new any more.
Note that whether an entity is new is part of the entity's state.
With auto-increment columns, this happens automatically, because the ID gets set by Spring Data with the value from the ID column.
If you are not using auto-increment columns, you can use a `BeforeConvert` listener, which sets the ID of the entity (covered later in this document).
+[[jdbc.entity-persistence.read-only-properties]]
+=== Read Only Properties
+
+Attributes annotated with `@ReadOnlyProperty` will not be written to the database by Spring Data JDBC, but they will be read when an entity gets loaded.
+
+Spring Data JDBC will not automatically reload an entity after writing it.
+Therefore, you have to reload it explicitly if you want to see data that was generated in the database for such columns.
+
+If the annotated attribute is an entity or collection of entities, it is represented by one or more separate rows in separate tables.
+Spring Data JDBC will not perform any insert, delete or update for these rows.
+
[[jdbc.entity-persistence.optimistic-locking]]
=== Optimistic Locking