Browse Source

Introduce `@InsertOnlyProperty`.

You may now annotate properties of the aggregate root with `@InsertOnlyProperty`.
Properties annotated in such way will be written to the database only during insert operations, but they will not be updated afterwards.

Closes #637
Original pull request #1327
pull/1353/head
Jens Schauder 3 years ago
parent
commit
004804aad8
  1. 6
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java
  2. 2
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java
  3. 23
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java
  4. 8
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql
  5. 6
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql
  6. 6
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql
  7. 6
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql
  8. 8
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql
  9. 6
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql
  10. 8
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql
  11. 7
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql
  12. 8
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java
  13. 148
      spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplateUnitTests.java
  14. 5
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java
  15. 36
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/InsertOnlyProperty.java
  16. 9
      spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java
  17. 8
      src/main/asciidoc/jdbc.adoc

6
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java

@ -1023,6 +1023,7 @@ class SqlGenerator {
private final List<SqlIdentifier> idColumnNames = new ArrayList<>(); private final List<SqlIdentifier> idColumnNames = new ArrayList<>();
private final List<SqlIdentifier> nonIdColumnNames = new ArrayList<>(); private final List<SqlIdentifier> nonIdColumnNames = new ArrayList<>();
private final Set<SqlIdentifier> readOnlyColumnNames = new HashSet<>(); private final Set<SqlIdentifier> readOnlyColumnNames = new HashSet<>();
private final Set<SqlIdentifier> insertOnlyColumnNames = new HashSet<>();
private final Set<SqlIdentifier> insertableColumns; private final Set<SqlIdentifier> insertableColumns;
private final Set<SqlIdentifier> updatableColumns; private final Set<SqlIdentifier> updatableColumns;
@ -1044,6 +1045,7 @@ class SqlGenerator {
updatable.removeAll(idColumnNames); updatable.removeAll(idColumnNames);
updatable.removeAll(readOnlyColumnNames); updatable.removeAll(readOnlyColumnNames);
updatable.removeAll(insertOnlyColumnNames);
this.updatableColumns = Collections.unmodifiableSet(updatable); this.updatableColumns = Collections.unmodifiableSet(updatable);
} }
@ -1076,6 +1078,10 @@ class SqlGenerator {
if (!property.isWritable()) { if (!property.isWritable()) {
readOnlyColumnNames.add(columnName); readOnlyColumnNames.add(columnName);
} }
if (property.isInsertOnly()) {
insertOnlyColumnNames.add(columnName);
}
} }
private void initEmbeddedColumnNames(RelationalPersistentProperty property, String prefix) { private void initEmbeddedColumnNames(RelationalPersistentProperty property, String prefix) {

2
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java

@ -95,7 +95,7 @@ public class SqlParametersFactory {
*/ */
<T> SqlIdentifierParameterSource forUpdate(T instance, Class<T> domainType) { <T> SqlIdentifierParameterSource forUpdate(T instance, Class<T> domainType) {
return getParameterSource(instance, getRequiredPersistentEntity(domainType), "", Predicates.includeAll(), return getParameterSource(instance, getRequiredPersistentEntity(domainType), "", RelationalPersistentProperty::isInsertOnly,
dialect.getIdentifierProcessing()); dialect.getIdentifierProcessing());
} }

23
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java

@ -64,6 +64,7 @@ import org.springframework.data.jdbc.testing.TestConfiguration;
import org.springframework.data.jdbc.testing.TestDatabaseFeatures; import org.springframework.data.jdbc.testing.TestDatabaseFeatures;
import org.springframework.data.relational.core.conversion.DbActionExecutionException; import org.springframework.data.relational.core.conversion.DbActionExecutionException;
import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.InsertOnlyProperty;
import org.springframework.data.relational.core.mapping.MappedCollection; import org.springframework.data.relational.core.mapping.MappedCollection;
import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.Table; import org.springframework.data.relational.core.mapping.Table;
@ -1030,6 +1031,20 @@ class JdbcAggregateTemplateIntegrationTests {
template.save(entity); template.save(entity);
} }
@Test // GH-637
void insertOnlyPropertyDoesNotGetUpdated() {
WithInsertOnly entity = new WithInsertOnly();
entity.insertOnly = "first value";
assertThat(template.save(entity).id).isNotNull();
entity.insertOnly = "second value";
template.save(entity);
assertThat(template.findById(entity.id, WithInsertOnly.class).insertOnly).isEqualTo("first value");
}
private <T extends Number> void saveAndUpdateAggregateWithVersion(VersionedAggregate aggregate, private <T extends Number> void saveAndUpdateAggregateWithVersion(VersionedAggregate aggregate,
Function<Number, T> toConcreteNumber) { Function<Number, T> toConcreteNumber) {
saveAndUpdateAggregateWithVersion(aggregate, toConcreteNumber, 0); saveAndUpdateAggregateWithVersion(aggregate, toConcreteNumber, 0);
@ -1461,10 +1476,16 @@ class JdbcAggregateTemplateIntegrationTests {
} }
@Table @Table
class WithIdOnly { static class WithIdOnly {
@Id Long id; @Id Long id;
} }
@Table
static class WithInsertOnly {
@Id Long id;
@InsertOnlyProperty
String insertOnly;
}
@Configuration @Configuration
@Import(TestConfiguration.class) @Import(TestConfiguration.class)

8
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql

@ -39,6 +39,8 @@ DROP TABLE WITH_LOCAL_DATE_TIME;
DROP TABLE WITH_ID_ONLY; DROP TABLE WITH_ID_ONLY;
DROP TABLE WITH_INSERT_ONLY;
CREATE TABLE LEGO_SET CREATE TABLE LEGO_SET
( (
"id1" BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, "id1" BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY,
@ -358,3 +360,9 @@ CREATE TABLE WITH_ID_ONLY
( (
ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY
); );
CREATE TABLE WITH_INSERT_ONLY
(
ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY,
INSERT_ONLY VARCHAR(100)
);

6
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql

@ -327,3 +327,9 @@ CREATE TABLE WITH_ID_ONLY
( (
ID SERIAL PRIMARY KEY ID SERIAL PRIMARY KEY
); );
CREATE TABLE WITH_INSERT_ONLY
(
ID SERIAL PRIMARY KEY,
INSERT_ONLY VARCHAR(100)
);

6
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql

@ -325,6 +325,12 @@ CREATE TABLE WITH_LOCAL_DATE_TIME
TEST_TIME TIMESTAMP(9) TEST_TIME TIMESTAMP(9)
); );
CREATE TABLE WITH_INSERT_ONLY
(
ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY,
INSERT_ONLY VARCHAR(100)
);
CREATE TABLE WITH_ID_ONLY CREATE TABLE WITH_ID_ONLY
( (
ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY

6
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql

@ -302,3 +302,9 @@ CREATE TABLE WITH_ID_ONLY
( (
ID BIGINT AUTO_INCREMENT PRIMARY KEY ID BIGINT AUTO_INCREMENT PRIMARY KEY
); );
CREATE TABLE WITH_INSERT_ONLY
(
ID BIGINT AUTO_INCREMENT PRIMARY KEY,
INSERT_ONLY VARCHAR(100)
);

8
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql

@ -332,3 +332,11 @@ CREATE TABLE WITH_ID_ONLY
( (
ID BIGINT IDENTITY PRIMARY KEY ID BIGINT IDENTITY PRIMARY KEY
); );
DROP TABLE IF EXISTS WITH_INSERT_ONLY;
CREATE TABLE WITH_INSERT_ONLY
(
ID BIGINT IDENTITY PRIMARY KEY,
INSERT_ONLY VARCHAR(100)
);

6
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql

@ -307,3 +307,9 @@ CREATE TABLE WITH_ID_ONLY
( (
ID BIGINT AUTO_INCREMENT PRIMARY KEY ID BIGINT AUTO_INCREMENT PRIMARY KEY
); );
CREATE TABLE WITH_INSERT_ONLY
(
ID BIGINT AUTO_INCREMENT PRIMARY KEY,
INSERT_ONLY VARCHAR(100)
);

8
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql

@ -29,6 +29,7 @@ DROP TABLE VERSIONED_AGGREGATE CASCADE CONSTRAINTS PURGE;
DROP TABLE WITH_READ_ONLY CASCADE CONSTRAINTS PURGE; DROP TABLE WITH_READ_ONLY CASCADE CONSTRAINTS PURGE;
DROP TABLE WITH_LOCAL_DATE_TIME CASCADE CONSTRAINTS PURGE; DROP TABLE WITH_LOCAL_DATE_TIME CASCADE CONSTRAINTS PURGE;
DROP TABLE WITH_ID_ONLY CASCADE CONSTRAINTS PURGE; DROP TABLE WITH_ID_ONLY CASCADE CONSTRAINTS PURGE;
DROP TABLE WITH_INSERT_ONLY CASCADE CONSTRAINTS PURGE;
CREATE TABLE LEGO_SET CREATE TABLE LEGO_SET
( (
@ -339,3 +340,10 @@ CREATE TABLE WITH_ID_ONLY
( (
ID NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY ID NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY
); );
CREATE TABLE WITH_INSERT_ONLY
(
ID NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY,
INSERT_ONLY VARCHAR(100)
);

7
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql

@ -13,6 +13,7 @@ DROP TABLE CHAIN1;
DROP TABLE CHAIN0; DROP TABLE CHAIN0;
DROP TABLE WITH_READ_ONLY; DROP TABLE WITH_READ_ONLY;
DROP TABLE WITH_ID_ONLY; DROP TABLE WITH_ID_ONLY;
DROP TABLE WITH_INSERT_ONLY;
CREATE TABLE LEGO_SET CREATE TABLE LEGO_SET
( (
@ -342,3 +343,9 @@ CREATE TABLE WITH_ID_ONLY
( (
ID SERIAL PRIMARY KEY ID SERIAL PRIMARY KEY
); );
CREATE TABLE WITH_INSERT_ONLY
(
ID SERIAL PRIMARY KEY,
INSERT_ONLY VARCHAR(100)
);

8
spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java

@ -31,7 +31,6 @@ import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.BeanFactoryAware;
@ -594,6 +593,13 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw
SqlIdentifier idColumn = persistentEntity.getRequiredIdProperty().getColumnName(); SqlIdentifier idColumn = persistentEntity.getRequiredIdProperty().getColumnName();
Parameter id = outboundRow.remove(idColumn); Parameter id = outboundRow.remove(idColumn);
persistentEntity.forEach(p -> {
if (p.isInsertOnly()) {
outboundRow.remove(p.getColumnName());
}
});
Criteria criteria = Criteria.where(dataAccessStrategy.toSql(idColumn)).is(id); Criteria criteria = Criteria.where(dataAccessStrategy.toSql(idColumn)).is(id);
if (matchingVersionCriteria != null) { if (matchingVersionCriteria != null) {

148
spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplateUnitTests.java

@ -25,6 +25,7 @@ import io.r2dbc.spi.test.MockRow;
import io.r2dbc.spi.test.MockRowMetadata; import io.r2dbc.spi.test.MockRowMetadata;
import lombok.Value; import lombok.Value;
import lombok.With; import lombok.With;
import org.springframework.data.relational.core.mapping.InsertOnlyProperty;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
@ -69,6 +70,7 @@ import org.springframework.util.CollectionUtils;
* @author Mark Paluch * @author Mark Paluch
* @author Jose Luis Leon * @author Jose Luis Leon
* @author Robert Heim * @author Robert Heim
* @author Jens Schauder
*/ */
public class R2dbcEntityTemplateUnitTests { public class R2dbcEntityTemplateUnitTests {
@ -85,7 +87,8 @@ public class R2dbcEntityTemplateUnitTests {
entityTemplate = new R2dbcEntityTemplate(client, PostgresDialect.INSTANCE); entityTemplate = new R2dbcEntityTemplate(client, PostgresDialect.INSTANCE);
} }
@Test // gh-220 @Test
// gh-220
void shouldCountBy() { void shouldCountBy() {
MockRowMetadata metadata = MockRowMetadata.builder() MockRowMetadata metadata = MockRowMetadata.builder()
@ -105,7 +108,8 @@ public class R2dbcEntityTemplateUnitTests {
assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter")); assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter"));
} }
@Test // gh-469 @Test
// gh-469
void shouldProjectExistsResult() { void shouldProjectExistsResult() {
MockRowMetadata metadata = MockRowMetadata.builder() MockRowMetadata metadata = MockRowMetadata.builder()
@ -122,7 +126,8 @@ public class R2dbcEntityTemplateUnitTests {
.verifyComplete(); .verifyComplete();
} }
@Test // gh-1310 @Test
// gh-1310
void shouldProjectExistsResultWithoutId() { void shouldProjectExistsResultWithoutId() {
MockResult result = MockResult.builder().row(MockRow.builder().identified(0, Object.class, null).build()).build(); MockResult result = MockResult.builder().row(MockRow.builder().identified(0, Object.class, null).build()).build();
@ -134,7 +139,8 @@ public class R2dbcEntityTemplateUnitTests {
.expectNext(true).verifyComplete(); .expectNext(true).verifyComplete();
} }
@Test // gh-1310 @Test
// gh-1310
void shouldProjectCountResultWithoutId() { void shouldProjectCountResultWithoutId() {
MockResult result = MockResult.builder().row(MockRow.builder().identified(0, Long.class, 1L).build()).build(); MockResult result = MockResult.builder().row(MockRow.builder().identified(0, Long.class, 1L).build()).build();
@ -146,7 +152,8 @@ public class R2dbcEntityTemplateUnitTests {
.expectNext(1L).verifyComplete(); .expectNext(1L).verifyComplete();
} }
@Test // gh-469 @Test
// gh-469
void shouldExistsByCriteria() { void shouldExistsByCriteria() {
MockRowMetadata metadata = MockRowMetadata.builder() MockRowMetadata metadata = MockRowMetadata.builder()
@ -166,7 +173,8 @@ public class R2dbcEntityTemplateUnitTests {
assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter")); assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter"));
} }
@Test // gh-220 @Test
// gh-220
void shouldSelectByCriteria() { void shouldSelectByCriteria() {
recorder.addStubbing(s -> s.startsWith("SELECT"), Collections.emptyList()); recorder.addStubbing(s -> s.startsWith("SELECT"), Collections.emptyList());
@ -182,7 +190,8 @@ public class R2dbcEntityTemplateUnitTests {
assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter")); assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter"));
} }
@Test // gh-215 @Test
// gh-215
void selectShouldInvokeCallback() { void selectShouldInvokeCallback() {
MockRowMetadata metadata = MockRowMetadata.builder() MockRowMetadata metadata = MockRowMetadata.builder()
@ -208,7 +217,8 @@ public class R2dbcEntityTemplateUnitTests {
assertThat(callback.getValues()).hasSize(1); assertThat(callback.getValues()).hasSize(1);
} }
@Test // gh-220 @Test
// gh-220
void shouldSelectOne() { void shouldSelectOne() {
recorder.addStubbing(s -> s.startsWith("SELECT"), Collections.emptyList()); recorder.addStubbing(s -> s.startsWith("SELECT"), Collections.emptyList());
@ -224,7 +234,8 @@ public class R2dbcEntityTemplateUnitTests {
assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter")); assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter"));
} }
@Test // gh-220, gh-758 @Test
// gh-220, gh-758
void shouldSelectOneDoNotOverrideExistingLimit() { void shouldSelectOneDoNotOverrideExistingLimit() {
recorder.addStubbing(s -> s.startsWith("SELECT"), Collections.emptyList()); recorder.addStubbing(s -> s.startsWith("SELECT"), Collections.emptyList());
@ -241,7 +252,8 @@ public class R2dbcEntityTemplateUnitTests {
assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter")); assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter"));
} }
@Test // gh-220 @Test
// gh-220
void shouldUpdateByQuery() { void shouldUpdateByQuery() {
MockRowMetadata metadata = MockRowMetadata.builder() MockRowMetadata metadata = MockRowMetadata.builder()
@ -263,7 +275,8 @@ public class R2dbcEntityTemplateUnitTests {
Parameter.from("Walter")); Parameter.from("Walter"));
} }
@Test // gh-220 @Test
// gh-220
void shouldDeleteByQuery() { void shouldDeleteByQuery() {
MockRowMetadata metadata = MockRowMetadata.builder() MockRowMetadata metadata = MockRowMetadata.builder()
@ -283,7 +296,8 @@ public class R2dbcEntityTemplateUnitTests {
assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter")); assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter"));
} }
@Test // gh-220 @Test
// gh-220
void shouldDeleteEntity() { void shouldDeleteEntity() {
Person person = Person.empty() // Person person = Person.empty() //
@ -300,7 +314,8 @@ public class R2dbcEntityTemplateUnitTests {
assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter")); assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter"));
} }
@Test // gh-365 @Test
// gh-365
void shouldInsertVersioned() { void shouldInsertVersioned() {
MockRowMetadata metadata = MockRowMetadata.builder().build(); MockRowMetadata metadata = MockRowMetadata.builder().build();
@ -321,7 +336,8 @@ public class R2dbcEntityTemplateUnitTests {
Parameter.from(1L)); Parameter.from(1L));
} }
@Test // gh-557, gh-402 @Test
// gh-557, gh-402
void shouldSkipDefaultIdValueOnInsert() { void shouldSkipDefaultIdValueOnInsert() {
MockRowMetadata metadata = MockRowMetadata.builder().build(); MockRowMetadata metadata = MockRowMetadata.builder().build();
@ -339,7 +355,8 @@ public class R2dbcEntityTemplateUnitTests {
assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("bar")); assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("bar"));
} }
@Test // gh-557, gh-402 @Test
// gh-557, gh-402
void shouldSkipDefaultIdValueOnVersionedInsert() { void shouldSkipDefaultIdValueOnVersionedInsert() {
MockRowMetadata metadata = MockRowMetadata.builder().build(); MockRowMetadata metadata = MockRowMetadata.builder().build();
@ -361,7 +378,8 @@ public class R2dbcEntityTemplateUnitTests {
Parameter.from("bar")); Parameter.from("bar"));
} }
@Test // gh-451 @Test
// gh-451
void shouldInsertCorrectlyVersionedAndAudited() { void shouldInsertCorrectlyVersionedAndAudited() {
MockRowMetadata metadata = MockRowMetadata.builder().build(); MockRowMetadata metadata = MockRowMetadata.builder().build();
@ -389,7 +407,8 @@ public class R2dbcEntityTemplateUnitTests {
"INSERT INTO with_auditing_and_optimistic_locking (version, name, created_date, last_modified_date) VALUES ($1, $2, $3, $4)"); "INSERT INTO with_auditing_and_optimistic_locking (version, name, created_date, last_modified_date) VALUES ($1, $2, $3, $4)");
} }
@Test // gh-451 @Test
// gh-451
void shouldUpdateCorrectlyVersionedAndAudited() { void shouldUpdateCorrectlyVersionedAndAudited() {
MockRowMetadata metadata = MockRowMetadata.builder().build(); MockRowMetadata metadata = MockRowMetadata.builder().build();
@ -418,7 +437,8 @@ public class R2dbcEntityTemplateUnitTests {
"UPDATE with_auditing_and_optimistic_locking SET version = $1, name = $2, created_date = $3, last_modified_date = $4"); "UPDATE with_auditing_and_optimistic_locking SET version = $1, name = $2, created_date = $3, last_modified_date = $4");
} }
@Test // gh-215 @Test
// gh-215
void insertShouldInvokeCallback() { void insertShouldInvokeCallback() {
MockRowMetadata metadata = MockRowMetadata.builder().build(); MockRowMetadata metadata = MockRowMetadata.builder().build();
@ -446,7 +466,8 @@ public class R2dbcEntityTemplateUnitTests {
Parameter.from("before-save")); Parameter.from("before-save"));
} }
@Test // gh-365 @Test
// gh-365
void shouldUpdateVersioned() { void shouldUpdateVersioned() {
MockRowMetadata metadata = MockRowMetadata.builder().build(); MockRowMetadata metadata = MockRowMetadata.builder().build();
@ -468,7 +489,8 @@ public class R2dbcEntityTemplateUnitTests {
Parameter.from(1L)); Parameter.from(1L));
} }
@Test // gh-215 @Test
// gh-215
void updateShouldInvokeCallback() { void updateShouldInvokeCallback() {
MockRowMetadata metadata = MockRowMetadata.builder().build(); MockRowMetadata metadata = MockRowMetadata.builder().build();
@ -501,6 +523,48 @@ public class R2dbcEntityTemplateUnitTests {
Parameter.from("before-save")); Parameter.from("before-save"));
} }
@Test
// gh-637
void insertIncludesInsertOnlyColumns() {
MockRowMetadata metadata = MockRowMetadata.builder().build();
MockResult result = MockResult.builder().rowMetadata(metadata).rowsUpdated(1).build();
recorder.addStubbing(s -> s.startsWith("INSERT"), result);
entityTemplate.insert(new WithInsertOnly(null, "Alfred", "insert this")).as(StepVerifier::create) //
.expectNextCount(1) //
.verifyComplete();
StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("INSERT"));
assertThat(statement.getSql()).isEqualTo("INSERT INTO with_insert_only (name, insert_only) VALUES ($1, $2)");
assertThat(statement.getBindings()).hasSize(2)
.containsEntry(0, Parameter.from("Alfred"))
.containsEntry(1, Parameter.from("insert this"));
}
@Test
// gh-637
void updateExcludesInsertOnlyColumns() {
MockRowMetadata metadata = MockRowMetadata.builder().build();
MockResult result = MockResult.builder().rowMetadata(metadata).rowsUpdated(1).build();
recorder.addStubbing(s -> s.startsWith("UPDATE"), result);
entityTemplate.update(new WithInsertOnly(23L, "Alfred", "don't update this")).as(StepVerifier::create) //
.expectNextCount(1) //
.verifyComplete();
StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("UPDATE"));
assertThat(statement.getSql()).isEqualTo("UPDATE with_insert_only SET name = $1 WHERE with_insert_only.id = $2");
assertThat(statement.getBindings()).hasSize(2)
.containsEntry(0, Parameter.from("Alfred"))
.containsEntry(1, Parameter.from(23L));
}
@Value @Value
static class WithoutId { static class WithoutId {
@ -511,9 +575,11 @@ public class R2dbcEntityTemplateUnitTests {
@With @With
static class Person { static class Person {
@Id String id; @Id
String id;
@Column("THE_NAME") String name; @Column("THE_NAME")
String name;
String description; String description;
@ -526,9 +592,11 @@ public class R2dbcEntityTemplateUnitTests {
@With @With
private static class VersionedPerson { private static class VersionedPerson {
@Id String id; @Id
String id;
@Version long version; @Version
long version;
String name; String name;
} }
@ -537,7 +605,8 @@ public class R2dbcEntityTemplateUnitTests {
@With @With
private static class PersonWithPrimitiveId { private static class PersonWithPrimitiveId {
@Id int id; @Id
int id;
String name; String name;
} }
@ -546,9 +615,11 @@ public class R2dbcEntityTemplateUnitTests {
@With @With
private static class VersionedPersonWithPrimitiveId { private static class VersionedPersonWithPrimitiveId {
@Id int id; @Id
int id;
@Version long version; @Version
long version;
String name; String name;
} }
@ -557,14 +628,29 @@ public class R2dbcEntityTemplateUnitTests {
@With @With
private static class WithAuditingAndOptimisticLocking { private static class WithAuditingAndOptimisticLocking {
@Id String id; @Id
String id;
@Version
long version;
String name;
@CreatedDate
LocalDateTime createdDate;
@LastModifiedDate
LocalDateTime lastModifiedDate;
}
@Version long version; @Value
private static class WithInsertOnly {
@Id
Long id;
String name; String name;
@CreatedDate LocalDateTime createdDate; @InsertOnlyProperty
@LastModifiedDate LocalDateTime lastModifiedDate; String insertOnly;
} }
static class ValueCapturingEntityCallback<T> { static class ValueCapturingEntityCallback<T> {

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

@ -199,6 +199,11 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent
return findAnnotation != null && OnEmpty.USE_EMPTY.equals(findAnnotation.onEmpty()); return findAnnotation != null && OnEmpty.USE_EMPTY.equals(findAnnotation.onEmpty());
} }
@Override
public boolean isInsertOnly() {
return findAnnotation(InsertOnlyProperty.class) != null;
}
private boolean isListLike() { private boolean isListLike() {
return isCollectionLike() && !Set.class.isAssignableFrom(this.getType()); return isCollectionLike() && !Set.class.isAssignableFrom(this.getType());
} }

36
spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/InsertOnlyProperty.java

@ -0,0 +1,36 @@
/*
* 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.relational.core.mapping;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* A property with this annotation will only be written to the database during insert operations, not during updates.
*
* @author Jens Schauder
* @since 3.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Documented
public @interface InsertOnlyProperty {
}

9
spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java

@ -73,8 +73,13 @@ public interface RelationalPersistentProperty extends PersistentProperty<Relatio
/** /**
* Returns whether an empty embedded object is supposed to be created for this property. * Returns whether an empty embedded object is supposed to be created for this property.
*
* @return
*/ */
boolean shouldCreateEmptyEmbedded(); boolean shouldCreateEmptyEmbedded();
/**
* Returns whether this property is only to be used during inserts and read.
*
* @since 3.0
*/
boolean isInsertOnly();
} }

8
src/main/asciidoc/jdbc.adoc

@ -435,6 +435,14 @@ Therefore, you have to reload it explicitly if you want to see data that was gen
If the annotated attribute is an entity or collection of entities, it is represented by one or more separate rows in separate tables. 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. Spring Data JDBC will not perform any insert, delete or update for these rows.
[[jdbc.entity-persistence.insert-only-properties]]
=== Insert Only Properties
Attributes annotated with `@InsertOnlyProperty` will only be written to the database by Spring Data JDBC during insert operations.
For updates these properties will be ignored.
`@InsertOnlyProperty` is only supported for the aggregate root.
[[jdbc.entity-persistence.optimistic-locking]] [[jdbc.entity-persistence.optimistic-locking]]
=== Optimistic Locking === Optimistic Locking

Loading…
Cancel
Save