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 { @@ -1023,6 +1023,7 @@ class SqlGenerator {
private final List<SqlIdentifier> idColumnNames = new ArrayList<>();
private final List<SqlIdentifier> nonIdColumnNames = new ArrayList<>();
private final Set<SqlIdentifier> readOnlyColumnNames = new HashSet<>();
private final Set<SqlIdentifier> insertOnlyColumnNames = new HashSet<>();
private final Set<SqlIdentifier> insertableColumns;
private final Set<SqlIdentifier> updatableColumns;
@ -1044,6 +1045,7 @@ class SqlGenerator { @@ -1044,6 +1045,7 @@ class SqlGenerator {
updatable.removeAll(idColumnNames);
updatable.removeAll(readOnlyColumnNames);
updatable.removeAll(insertOnlyColumnNames);
this.updatableColumns = Collections.unmodifiableSet(updatable);
}
@ -1076,6 +1078,10 @@ class SqlGenerator { @@ -1076,6 +1078,10 @@ class SqlGenerator {
if (!property.isWritable()) {
readOnlyColumnNames.add(columnName);
}
if (property.isInsertOnly()) {
insertOnlyColumnNames.add(columnName);
}
}
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 { @@ -95,7 +95,7 @@ public class SqlParametersFactory {
*/
<T> SqlIdentifierParameterSource forUpdate(T instance, Class<T> domainType) {
return getParameterSource(instance, getRequiredPersistentEntity(domainType), "", Predicates.includeAll(),
return getParameterSource(instance, getRequiredPersistentEntity(domainType), "", RelationalPersistentProperty::isInsertOnly,
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; @@ -64,6 +64,7 @@ import org.springframework.data.jdbc.testing.TestConfiguration;
import org.springframework.data.jdbc.testing.TestDatabaseFeatures;
import org.springframework.data.relational.core.conversion.DbActionExecutionException;
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.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.Table;
@ -1030,6 +1031,20 @@ class JdbcAggregateTemplateIntegrationTests { @@ -1030,6 +1031,20 @@ class JdbcAggregateTemplateIntegrationTests {
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,
Function<Number, T> toConcreteNumber) {
saveAndUpdateAggregateWithVersion(aggregate, toConcreteNumber, 0);
@ -1461,10 +1476,16 @@ class JdbcAggregateTemplateIntegrationTests { @@ -1461,10 +1476,16 @@ class JdbcAggregateTemplateIntegrationTests {
}
@Table
class WithIdOnly {
static class WithIdOnly {
@Id Long id;
}
@Table
static class WithInsertOnly {
@Id Long id;
@InsertOnlyProperty
String insertOnly;
}
@Configuration
@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; @@ -39,6 +39,8 @@ DROP TABLE WITH_LOCAL_DATE_TIME;
DROP TABLE WITH_ID_ONLY;
DROP TABLE WITH_INSERT_ONLY;
CREATE TABLE LEGO_SET
(
"id1" BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY,
@ -358,3 +360,9 @@ CREATE TABLE WITH_ID_ONLY @@ -358,3 +360,9 @@ CREATE TABLE WITH_ID_ONLY
(
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 @@ -327,3 +327,9 @@ CREATE TABLE WITH_ID_ONLY
(
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 @@ -325,6 +325,12 @@ CREATE TABLE WITH_LOCAL_DATE_TIME
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
(
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 @@ -302,3 +302,9 @@ CREATE TABLE WITH_ID_ONLY
(
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 @@ -332,3 +332,11 @@ CREATE TABLE WITH_ID_ONLY
(
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 @@ -307,3 +307,9 @@ CREATE TABLE WITH_ID_ONLY
(
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; @@ -29,6 +29,7 @@ DROP TABLE VERSIONED_AGGREGATE CASCADE CONSTRAINTS PURGE;
DROP TABLE WITH_READ_ONLY CASCADE CONSTRAINTS PURGE;
DROP TABLE WITH_LOCAL_DATE_TIME CASCADE CONSTRAINTS PURGE;
DROP TABLE WITH_ID_ONLY CASCADE CONSTRAINTS PURGE;
DROP TABLE WITH_INSERT_ONLY CASCADE CONSTRAINTS PURGE;
CREATE TABLE LEGO_SET
(
@ -339,3 +340,10 @@ CREATE TABLE WITH_ID_ONLY @@ -339,3 +340,10 @@ CREATE TABLE WITH_ID_ONLY
(
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; @@ -13,6 +13,7 @@ DROP TABLE CHAIN1;
DROP TABLE CHAIN0;
DROP TABLE WITH_READ_ONLY;
DROP TABLE WITH_ID_ONLY;
DROP TABLE WITH_INSERT_ONLY;
CREATE TABLE LEGO_SET
(
@ -342,3 +343,9 @@ CREATE TABLE WITH_ID_ONLY @@ -342,3 +343,9 @@ CREATE TABLE WITH_ID_ONLY
(
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; @@ -31,7 +31,6 @@ import java.util.function.Function;
import java.util.stream.Collectors;
import org.reactivestreams.Publisher;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
@ -594,6 +593,13 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw @@ -594,6 +593,13 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw
SqlIdentifier idColumn = persistentEntity.getRequiredIdProperty().getColumnName();
Parameter id = outboundRow.remove(idColumn);
persistentEntity.forEach(p -> {
if (p.isInsertOnly()) {
outboundRow.remove(p.getColumnName());
}
});
Criteria criteria = Criteria.where(dataAccessStrategy.toSql(idColumn)).is(id);
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; @@ -25,6 +25,7 @@ import io.r2dbc.spi.test.MockRow;
import io.r2dbc.spi.test.MockRowMetadata;
import lombok.Value;
import lombok.With;
import org.springframework.data.relational.core.mapping.InsertOnlyProperty;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@ -69,6 +70,7 @@ import org.springframework.util.CollectionUtils; @@ -69,6 +70,7 @@ import org.springframework.util.CollectionUtils;
* @author Mark Paluch
* @author Jose Luis Leon
* @author Robert Heim
* @author Jens Schauder
*/
public class R2dbcEntityTemplateUnitTests {
@ -85,7 +87,8 @@ public class R2dbcEntityTemplateUnitTests { @@ -85,7 +87,8 @@ public class R2dbcEntityTemplateUnitTests {
entityTemplate = new R2dbcEntityTemplate(client, PostgresDialect.INSTANCE);
}
@Test // gh-220
@Test
// gh-220
void shouldCountBy() {
MockRowMetadata metadata = MockRowMetadata.builder()
@ -105,7 +108,8 @@ public class R2dbcEntityTemplateUnitTests { @@ -105,7 +108,8 @@ public class R2dbcEntityTemplateUnitTests {
assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter"));
}
@Test // gh-469
@Test
// gh-469
void shouldProjectExistsResult() {
MockRowMetadata metadata = MockRowMetadata.builder()
@ -122,7 +126,8 @@ public class R2dbcEntityTemplateUnitTests { @@ -122,7 +126,8 @@ public class R2dbcEntityTemplateUnitTests {
.verifyComplete();
}
@Test // gh-1310
@Test
// gh-1310
void shouldProjectExistsResultWithoutId() {
MockResult result = MockResult.builder().row(MockRow.builder().identified(0, Object.class, null).build()).build();
@ -134,7 +139,8 @@ public class R2dbcEntityTemplateUnitTests { @@ -134,7 +139,8 @@ public class R2dbcEntityTemplateUnitTests {
.expectNext(true).verifyComplete();
}
@Test // gh-1310
@Test
// gh-1310
void shouldProjectCountResultWithoutId() {
MockResult result = MockResult.builder().row(MockRow.builder().identified(0, Long.class, 1L).build()).build();
@ -146,7 +152,8 @@ public class R2dbcEntityTemplateUnitTests { @@ -146,7 +152,8 @@ public class R2dbcEntityTemplateUnitTests {
.expectNext(1L).verifyComplete();
}
@Test // gh-469
@Test
// gh-469
void shouldExistsByCriteria() {
MockRowMetadata metadata = MockRowMetadata.builder()
@ -166,7 +173,8 @@ public class R2dbcEntityTemplateUnitTests { @@ -166,7 +173,8 @@ public class R2dbcEntityTemplateUnitTests {
assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter"));
}
@Test // gh-220
@Test
// gh-220
void shouldSelectByCriteria() {
recorder.addStubbing(s -> s.startsWith("SELECT"), Collections.emptyList());
@ -182,7 +190,8 @@ public class R2dbcEntityTemplateUnitTests { @@ -182,7 +190,8 @@ public class R2dbcEntityTemplateUnitTests {
assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter"));
}
@Test // gh-215
@Test
// gh-215
void selectShouldInvokeCallback() {
MockRowMetadata metadata = MockRowMetadata.builder()
@ -208,7 +217,8 @@ public class R2dbcEntityTemplateUnitTests { @@ -208,7 +217,8 @@ public class R2dbcEntityTemplateUnitTests {
assertThat(callback.getValues()).hasSize(1);
}
@Test // gh-220
@Test
// gh-220
void shouldSelectOne() {
recorder.addStubbing(s -> s.startsWith("SELECT"), Collections.emptyList());
@ -224,7 +234,8 @@ public class R2dbcEntityTemplateUnitTests { @@ -224,7 +234,8 @@ public class R2dbcEntityTemplateUnitTests {
assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter"));
}
@Test // gh-220, gh-758
@Test
// gh-220, gh-758
void shouldSelectOneDoNotOverrideExistingLimit() {
recorder.addStubbing(s -> s.startsWith("SELECT"), Collections.emptyList());
@ -241,7 +252,8 @@ public class R2dbcEntityTemplateUnitTests { @@ -241,7 +252,8 @@ public class R2dbcEntityTemplateUnitTests {
assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter"));
}
@Test // gh-220
@Test
// gh-220
void shouldUpdateByQuery() {
MockRowMetadata metadata = MockRowMetadata.builder()
@ -263,7 +275,8 @@ public class R2dbcEntityTemplateUnitTests { @@ -263,7 +275,8 @@ public class R2dbcEntityTemplateUnitTests {
Parameter.from("Walter"));
}
@Test // gh-220
@Test
// gh-220
void shouldDeleteByQuery() {
MockRowMetadata metadata = MockRowMetadata.builder()
@ -283,7 +296,8 @@ public class R2dbcEntityTemplateUnitTests { @@ -283,7 +296,8 @@ public class R2dbcEntityTemplateUnitTests {
assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter"));
}
@Test // gh-220
@Test
// gh-220
void shouldDeleteEntity() {
Person person = Person.empty() //
@ -300,7 +314,8 @@ public class R2dbcEntityTemplateUnitTests { @@ -300,7 +314,8 @@ public class R2dbcEntityTemplateUnitTests {
assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter"));
}
@Test // gh-365
@Test
// gh-365
void shouldInsertVersioned() {
MockRowMetadata metadata = MockRowMetadata.builder().build();
@ -321,7 +336,8 @@ public class R2dbcEntityTemplateUnitTests { @@ -321,7 +336,8 @@ public class R2dbcEntityTemplateUnitTests {
Parameter.from(1L));
}
@Test // gh-557, gh-402
@Test
// gh-557, gh-402
void shouldSkipDefaultIdValueOnInsert() {
MockRowMetadata metadata = MockRowMetadata.builder().build();
@ -339,7 +355,8 @@ public class R2dbcEntityTemplateUnitTests { @@ -339,7 +355,8 @@ public class R2dbcEntityTemplateUnitTests {
assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("bar"));
}
@Test // gh-557, gh-402
@Test
// gh-557, gh-402
void shouldSkipDefaultIdValueOnVersionedInsert() {
MockRowMetadata metadata = MockRowMetadata.builder().build();
@ -361,7 +378,8 @@ public class R2dbcEntityTemplateUnitTests { @@ -361,7 +378,8 @@ public class R2dbcEntityTemplateUnitTests {
Parameter.from("bar"));
}
@Test // gh-451
@Test
// gh-451
void shouldInsertCorrectlyVersionedAndAudited() {
MockRowMetadata metadata = MockRowMetadata.builder().build();
@ -389,7 +407,8 @@ public class R2dbcEntityTemplateUnitTests { @@ -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)");
}
@Test // gh-451
@Test
// gh-451
void shouldUpdateCorrectlyVersionedAndAudited() {
MockRowMetadata metadata = MockRowMetadata.builder().build();
@ -418,7 +437,8 @@ public class R2dbcEntityTemplateUnitTests { @@ -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");
}
@Test // gh-215
@Test
// gh-215
void insertShouldInvokeCallback() {
MockRowMetadata metadata = MockRowMetadata.builder().build();
@ -446,7 +466,8 @@ public class R2dbcEntityTemplateUnitTests { @@ -446,7 +466,8 @@ public class R2dbcEntityTemplateUnitTests {
Parameter.from("before-save"));
}
@Test // gh-365
@Test
// gh-365
void shouldUpdateVersioned() {
MockRowMetadata metadata = MockRowMetadata.builder().build();
@ -468,7 +489,8 @@ public class R2dbcEntityTemplateUnitTests { @@ -468,7 +489,8 @@ public class R2dbcEntityTemplateUnitTests {
Parameter.from(1L));
}
@Test // gh-215
@Test
// gh-215
void updateShouldInvokeCallback() {
MockRowMetadata metadata = MockRowMetadata.builder().build();
@ -501,6 +523,48 @@ public class R2dbcEntityTemplateUnitTests { @@ -501,6 +523,48 @@ public class R2dbcEntityTemplateUnitTests {
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
static class WithoutId {
@ -511,9 +575,11 @@ public class R2dbcEntityTemplateUnitTests { @@ -511,9 +575,11 @@ public class R2dbcEntityTemplateUnitTests {
@With
static class Person {
@Id String id;
@Id
String id;
@Column("THE_NAME") String name;
@Column("THE_NAME")
String name;
String description;
@ -526,9 +592,11 @@ public class R2dbcEntityTemplateUnitTests { @@ -526,9 +592,11 @@ public class R2dbcEntityTemplateUnitTests {
@With
private static class VersionedPerson {
@Id String id;
@Id
String id;
@Version long version;
@Version
long version;
String name;
}
@ -537,7 +605,8 @@ public class R2dbcEntityTemplateUnitTests { @@ -537,7 +605,8 @@ public class R2dbcEntityTemplateUnitTests {
@With
private static class PersonWithPrimitiveId {
@Id int id;
@Id
int id;
String name;
}
@ -546,9 +615,11 @@ public class R2dbcEntityTemplateUnitTests { @@ -546,9 +615,11 @@ public class R2dbcEntityTemplateUnitTests {
@With
private static class VersionedPersonWithPrimitiveId {
@Id int id;
@Id
int id;
@Version long version;
@Version
long version;
String name;
}
@ -557,14 +628,29 @@ public class R2dbcEntityTemplateUnitTests { @@ -557,14 +628,29 @@ public class R2dbcEntityTemplateUnitTests {
@With
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;
@CreatedDate LocalDateTime createdDate;
@LastModifiedDate LocalDateTime lastModifiedDate;
@InsertOnlyProperty
String insertOnly;
}
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 @@ -199,6 +199,11 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent
return findAnnotation != null && OnEmpty.USE_EMPTY.equals(findAnnotation.onEmpty());
}
@Override
public boolean isInsertOnly() {
return findAnnotation(InsertOnlyProperty.class) != null;
}
private boolean isListLike() {
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 @@ @@ -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 @@ -73,8 +73,13 @@ public interface RelationalPersistentProperty extends PersistentProperty<Relatio
/**
* Returns whether an empty embedded object is supposed to be created for this property.
*
* @return
*/
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 @@ -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.
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]]
=== Optimistic Locking

Loading…
Cancel
Save