Skip to content

Support readonly properties for references. #1250

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational-parent</artifactId>
<version>3.0.0-SNAPSHOT</version>
<version>3.0.0-1249-readonly-reference-SNAPSHOT</version>
<packaging>pom</packaging>

<name>Spring Data Relational Parent</name>
Expand Down
2 changes: 1 addition & 1 deletion spring-data-jdbc-distribution/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational-parent</artifactId>
<version>3.0.0-SNAPSHOT</version>
<version>3.0.0-1249-readonly-reference-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
4 changes: 2 additions & 2 deletions spring-data-jdbc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<modelVersion>4.0.0</modelVersion>

<artifactId>spring-data-jdbc</artifactId>
<version>3.0.0-SNAPSHOT</version>
<version>3.0.0-1249-readonly-reference-SNAPSHOT</version>

<name>Spring Data JDBC</name>
<description>Spring Data module for JDBC repositories.</description>
Expand All @@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational-parent</artifactId>
<version>3.0.0-SNAPSHOT</version>
<version>3.0.0-1249-readonly-reference-SNAPSHOT</version>
</parent>

<properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
*/
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.*;

import java.util.List;

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;
Expand Down Expand Up @@ -202,7 +204,7 @@ void extendBy() {
});
}

@Test // GH--1164
@Test // GH-1164
void equalsWorks() {

PersistentPropertyPathExtension root1 = extPath(entity);
Expand All @@ -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);
}
Expand All @@ -237,6 +250,7 @@ PersistentPropertyPath<RelationalPersistentProperty> 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;
Expand Down
4 changes: 2 additions & 2 deletions spring-data-r2dbc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<modelVersion>4.0.0</modelVersion>

<artifactId>spring-data-r2dbc</artifactId>
<version>3.0.0-SNAPSHOT</version>
<version>3.0.0-1249-readonly-reference-SNAPSHOT</version>

<name>Spring Data R2DBC</name>
<description>Spring Data module for R2DBC</description>
Expand All @@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational-parent</artifactId>
<version>3.0.0-SNAPSHOT</version>
<version>3.0.0-1249-readonly-reference-SNAPSHOT</version>
</parent>

<properties>
Expand Down
4 changes: 2 additions & 2 deletions spring-data-relational/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
<modelVersion>4.0.0</modelVersion>

<artifactId>spring-data-relational</artifactId>
<version>3.0.0-SNAPSHOT</version>
<version>3.0.0-1249-readonly-reference-SNAPSHOT</version>

<name>Spring Data Relational</name>
<description>Spring Data Relational support</description>

<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational-parent</artifactId>
<version>3.0.0-SNAPSHOT</version>
<version>3.0.0-1249-readonly-reference-SNAPSHOT</version>
</parent>

<properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -72,8 +74,7 @@ private List<DbAction<?>> deleteAll(Class<?> entityType) {

List<DbAction<?>> 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);

Expand Down Expand Up @@ -114,12 +115,18 @@ private List<DbAction<?>> deleteReferencedEntities(Object id, AggregateChange<?>

List<DbAction<?>> 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<PersistentPropertyPath<RelationalPersistentProperty>> pathConsumer) {

context.findPersistentPropertyPaths(entityType, property -> property.isEntity() && !property.isEmbedded()) //
.filter(PersistentPropertyPathExtension::isWritable) //
.forEach(pathConsumer);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -47,7 +47,7 @@ class WritingContext<T> {
private final RelationalMappingContext context;
private final T root;
private final Class<T> entityType;
private final PersistentPropertyPaths<?, RelationalPersistentProperty> paths;
private final List<PersistentPropertyPath<RelationalPersistentProperty>> paths;
private final Map<PathNode, DbAction<?>> previousActions = new HashMap<>();
private final Map<PersistentPropertyPath<RelationalPersistentProperty>, List<PathNode>> nodesCache = new HashMap<>();
private final IdValueSource rootIdValueSource;
Expand All @@ -63,7 +63,9 @@ class WritingContext<T> {
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();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<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) {

List<DbAction<?>> actions = new ArrayList<>();
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -75,6 +76,7 @@ public void existingEntityGetsNotConvertedToDeletePlusUpdate() {

}


private List<DbAction<?>> extractActions(MutableAggregateChange<?> aggregateChange) {

List<DbAction<?>> actions = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<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) {

List<DbAction<?>> actions = new ArrayList<>();
Expand Down Expand Up @@ -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;
}

}
13 changes: 12 additions & 1 deletion src/main/asciidoc/jdbc.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down