Browse Source

Support readonly properties for references.

The `@ReadOnlyProperty` annotation is now honoured for references to entities or collections of entities.

For tables mapped to such annotated references, no insert, delete or update statements will be created.
The user has to maintain that data through some other means.
These could be triggers or external process or `ON DELETE CASCADE` configuration in the database schema.

Closes #1249
Original pull request #1250
pull/1231/head
Jens Schauder 4 years ago
parent
commit
dcd4ef0cdc
No known key found for this signature in database
GPG Key ID: 45CC872F17423DBF
  1. 16
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/PersistentPropertyPathExtensionUnitTests.java
  2. 14
      spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriter.java
  3. 2
      spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java
  4. 5
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java
  5. 42
      spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriterUnitTests.java
  6. 2
      spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriterUnitTests.java
  7. 50
      spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java
  8. 13
      src/main/asciidoc/jdbc.adoc

16
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/PersistentPropertyPathExtensionUnitTests.java

@ -15,6 +15,7 @@
*/ */
package org.springframework.data.jdbc.core; package org.springframework.data.jdbc.core;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.SoftAssertions.*; import static org.assertj.core.api.SoftAssertions.*;
import static org.springframework.data.relational.core.sql.SqlIdentifier.*; import static org.springframework.data.relational.core.sql.SqlIdentifier.*;
@ -22,6 +23,7 @@ import java.util.List;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.ReadOnlyProperty;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.relational.core.mapping.Embedded; import org.springframework.data.relational.core.mapping.Embedded;
@ -202,7 +204,7 @@ public class PersistentPropertyPathExtensionUnitTests {
}); });
} }
@Test // GH--1164 @Test // GH-1164
void equalsWorks() { void equalsWorks() {
PersistentPropertyPathExtension root1 = extPath(entity); PersistentPropertyPathExtension root1 = extPath(entity);
@ -222,6 +224,17 @@ public class PersistentPropertyPathExtensionUnitTests {
}); });
} }
@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) { private PersistentPropertyPathExtension extPath(RelationalPersistentEntity<?> entity) {
return new PersistentPropertyPathExtension(context, entity); return new PersistentPropertyPathExtension(context, entity);
} }
@ -237,6 +250,7 @@ public class PersistentPropertyPathExtensionUnitTests {
@SuppressWarnings("unused") @SuppressWarnings("unused")
static class DummyEntity { static class DummyEntity {
@Id Long entityId; @Id Long entityId;
@ReadOnlyProperty
Second second; Second second;
@Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "sec") Second second2; @Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "sec") Second second2;
@Embedded(onEmpty = OnEmpty.USE_NULL) Second second3; @Embedded(onEmpty = OnEmpty.USE_NULL) Second second3;

14
spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriter.java

@ -21,8 +21,10 @@ import java.util.List;
import org.springframework.data.convert.EntityWriter; import org.springframework.data.convert.EntityWriter;
import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension;
import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -72,8 +74,10 @@ public class RelationalEntityDeleteWriter implements EntityWriter<Object, Mutabl
List<DbAction<?>> deleteReferencedActions = new ArrayList<>(); List<DbAction<?>> deleteReferencedActions = new ArrayList<>();
context.findPersistentPropertyPaths(entityType, PersistentProperty::isEntity) context.findPersistentPropertyPaths(entityType, PersistentProperty::isEntity) //
.filter(p -> !p.getRequiredLeafProperty().isEmbedded()).forEach(p -> deleteReferencedActions.add(new DbAction.DeleteAll<>(p))); .filter(p -> !p.getRequiredLeafProperty().isEmbedded() //
&& PersistentPropertyPathExtension.isWritable(p)) //
.forEach(p -> deleteReferencedActions.add(new DbAction.DeleteAll<>(p)));
Collections.reverse(deleteReferencedActions); Collections.reverse(deleteReferencedActions);
@ -114,8 +118,10 @@ public class RelationalEntityDeleteWriter implements EntityWriter<Object, Mutabl
List<DbAction<?>> actions = new ArrayList<>(); List<DbAction<?>> actions = new ArrayList<>();
context.findPersistentPropertyPaths(aggregateChange.getEntityType(), PersistentProperty::isEntity) context.findPersistentPropertyPaths(aggregateChange.getEntityType(), p -> p.isEntity()) //
.filter(p -> !p.getRequiredLeafProperty().isEmbedded()).forEach(p -> actions.add(new DbAction.Delete<>(id, p))); .filter(p -> !p.getRequiredLeafProperty().isEmbedded() //
&& PersistentPropertyPathExtension.isWritable(p)) //
.forEach(p -> actions.add(new DbAction.Delete<>(id, p)));
Collections.reverse(actions); Collections.reverse(actions);

2
spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java

@ -63,7 +63,7 @@ class WritingContext<T> {
this.aggregateChange = aggregateChange; this.aggregateChange = aggregateChange;
this.rootIdValueSource = IdValueSource.forInstance(root, this.rootIdValueSource = IdValueSource.forInstance(root,
context.getRequiredPersistentEntity(aggregateChange.getEntityType())); context.getRequiredPersistentEntity(aggregateChange.getEntityType()));
this.paths = context.findPersistentPropertyPaths(entityType, (p) -> p.isEntity() && !p.isEmbedded()); this.paths = context.findPersistentPropertyPaths(entityType, (p) -> p.isEntity() && !p.isEmbedded() && p.isWritable());
} }
/** /**

5
spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java

@ -80,6 +80,11 @@ public class PersistentPropertyPathExtension {
this.path = path; 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. * Returns {@literal true} exactly when the path is non empty and the leaf property an embedded one.
* *

42
spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriterUnitTests.java

@ -20,12 +20,14 @@ import lombok.Data;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor;
import org.assertj.core.api.Assertions; import org.assertj.core.api.Assertions;
import org.assertj.core.groups.Tuple; import org.assertj.core.groups.Tuple;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.annotation.Id; 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.AcquireLockAllRoot;
import org.springframework.data.relational.core.conversion.DbAction.AcquireLockRoot; import org.springframework.data.relational.core.conversion.DbAction.AcquireLockRoot;
import org.springframework.data.relational.core.conversion.DbAction.Delete; import org.springframework.data.relational.core.conversion.DbAction.Delete;
@ -108,6 +110,39 @@ public class RelationalEntityDeleteWriterUnitTests {
.containsExactly(Tuple.tuple(DeleteAllRoot.class, SingleEntity.class, "")); .containsExactly(Tuple.tuple(DeleteAllRoot.class, SingleEntity.class, ""));
} }
@Test // GH-1249
public void deleteDoesNotDeleteReadOnlyReferences() {
WithReadOnlyReference entity = new WithReadOnlyReference(23L);
MutableAggregateChange<WithReadOnlyReference> 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<WithReadOnlyReference> 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<DbAction<?>> extractActions(MutableAggregateChange<?> aggregateChange) { private List<DbAction<?>> extractActions(MutableAggregateChange<?> aggregateChange) {
List<DbAction<?>> actions = new ArrayList<>(); List<DbAction<?>> actions = new ArrayList<>();
@ -141,4 +176,11 @@ public class RelationalEntityDeleteWriterUnitTests {
@Id final Long id; @Id final Long id;
String name; String name;
} }
@RequiredArgsConstructor
private static class WithReadOnlyReference {
@Id final Long id;
@ReadOnlyProperty
OtherEntity other;
}
} }

2
spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriterUnitTests.java

@ -26,6 +26,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.annotation.Id; 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.conversion.DbAction.InsertRoot;
import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalMappingContext;
@ -75,6 +76,7 @@ public class RelationalEntityInsertWriterUnitTests {
} }
private List<DbAction<?>> extractActions(MutableAggregateChange<?> aggregateChange) { private List<DbAction<?>> extractActions(MutableAggregateChange<?> aggregateChange) {
List<DbAction<?>> actions = new ArrayList<>(); List<DbAction<?>> actions = new ArrayList<>();

50
spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java

@ -33,6 +33,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.ReadOnlyProperty;
import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PersistentPropertyPaths; import org.springframework.data.mapping.PersistentPropertyPaths;
import org.springframework.data.relational.core.conversion.DbAction.Delete; import org.springframework.data.relational.core.conversion.DbAction.Delete;
@ -817,6 +818,46 @@ public class RelationalEntityWriterUnitTests {
); );
} }
@Test // GH-1249
public void readOnlyReferenceDoesNotCreateInsertsOnCreation() {
WithReadOnlyReference entity = new WithReadOnlyReference(null);
entity.readOnly = new Element(SOME_ENTITY_ID);
AggregateChangeWithRoot<WithReadOnlyReference> aggregateChange = MutableAggregateChange.forSave(entity);
new RelationalEntityWriter<WithReadOnlyReference>(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<WithReadOnlyReference> aggregateChange = MutableAggregateChange.forSave(entity);
new RelationalEntityWriter<WithReadOnlyReference>(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<DbAction<?>> extractActions(MutableAggregateChange<?> aggregateChange) { private List<DbAction<?>> extractActions(MutableAggregateChange<?> aggregateChange) {
List<DbAction<?>> actions = new ArrayList<>(); List<DbAction<?>> actions = new ArrayList<>();
@ -1015,4 +1056,13 @@ public class RelationalEntityWriterUnitTests {
// empty classes feel weird. // empty classes feel weird.
String name; String name;
} }
@RequiredArgsConstructor
private static class WithReadOnlyReference {
@Id final Long id;
@ReadOnlyProperty
Element readOnly;
}
} }

13
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. 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. 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. 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. 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. 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). 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]] [[jdbc.entity-persistence.optimistic-locking]]
=== Optimistic Locking === Optimistic Locking

Loading…
Cancel
Save