From e9629a2853f40eb3098143694dd247a2d53935e8 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Thu, 8 Sep 2022 15:57:22 +0200 Subject: [PATCH 1/4] 637-insert-only-property - Prepare branch --- pom.xml | 2 +- spring-data-jdbc-distribution/pom.xml | 2 +- spring-data-jdbc/pom.xml | 4 ++-- spring-data-r2dbc/pom.xml | 4 ++-- spring-data-relational/pom.xml | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index b2d4b1d1ae..49cf9f9342 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-637-insert-only-property-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index db3b7ddd1a..47a6cfcd7c 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-637-insert-only-property-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index 547ff62b8b..d87837549d 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-637-insert-only-property-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-637-insert-only-property-SNAPSHOT diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml index 14d7909d7b..88e99283e5 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-637-insert-only-property-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-637-insert-only-property-SNAPSHOT diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index e9d0455c68..fb81ca76b3 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-637-insert-only-property-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-637-insert-only-property-SNAPSHOT From 401c885e2f1f348082f27f47b6ca44e42f2b6807 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Thu, 8 Sep 2022 16:16:06 +0200 Subject: [PATCH 2/4] Introduce `@InsertOnlyProperty`. For Spring Data JDBC 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 --- .../data/jdbc/core/convert/SqlGenerator.java | 6 ++++ .../core/convert/SqlParametersFactory.java | 2 +- ...JdbcAggregateTemplateIntegrationTests.java | 23 ++++++++++++++- ...AggregateTemplateIntegrationTests-hsql.sql | 6 ++++ .../BasicRelationalPersistentProperty.java | 5 ++++ .../core/mapping/InsertOnlyProperty.java | 29 +++++++++++++++++++ .../mapping/RelationalPersistentProperty.java | 9 ++++-- src/main/asciidoc/jdbc.adoc | 8 +++++ 8 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/InsertOnlyProperty.java diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java index 797efafd23..e07cdb62b5 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java @@ -1024,6 +1024,7 @@ static class Columns { private final List idColumnNames = new ArrayList<>(); private final List nonIdColumnNames = new ArrayList<>(); private final Set readOnlyColumnNames = new HashSet<>(); + private final Set insertOnlyColumnNames = new HashSet<>(); private final Set insertableColumns; private final Set updatableColumns; @@ -1045,6 +1046,7 @@ static class Columns { updatable.removeAll(idColumnNames); updatable.removeAll(readOnlyColumnNames); + updatable.removeAll(insertOnlyColumnNames); this.updatableColumns = Collections.unmodifiableSet(updatable); } @@ -1077,6 +1079,10 @@ private void initSimpleColumnName(RelationalPersistentProperty property, String if (!property.isWritable()) { readOnlyColumnNames.add(columnName); } + + if (property.isInsertOnly()) { + insertOnlyColumnNames.add(columnName); + } } private void initEmbeddedColumnNames(RelationalPersistentProperty property, String prefix) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java index b7f247f762..504aa33fe3 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java @@ -95,7 +95,7 @@ SqlIdentifierParameterSource forInsert(T instance, Class domainType, Iden */ SqlIdentifierParameterSource forUpdate(T instance, Class domainType) { - return getParameterSource(instance, getRequiredPersistentEntity(domainType), "", Predicates.includeAll(), + return getParameterSource(instance, getRequiredPersistentEntity(domainType), "", RelationalPersistentProperty::isInsertOnly, dialect.getIdentifierProcessing()); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java index dd2987aac0..160161f3dd 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateTemplateIntegrationTests.java @@ -64,6 +64,7 @@ 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; @@ -1031,6 +1032,20 @@ void updateIdOnlyAggregate() { 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 void saveAndUpdateAggregateWithVersion(VersionedAggregate aggregate, Function toConcreteNumber) { saveAndUpdateAggregateWithVersion(aggregate, toConcreteNumber, 0); @@ -1462,10 +1477,16 @@ static class WithLocalDateTime { } @Table - class WithIdOnly { + static class WithIdOnly { @Id Long id; } + @Table + static class WithInsertOnly { + @Id Long id; + @InsertOnlyProperty + String insertOnly; + } @Configuration @Import(TestConfiguration.class) diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql index 6d04923f17..4dd1294ab2 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql +++ b/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) ); +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 diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java index 6dbfc5d817..922b5b8a5b 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java @@ -199,6 +199,11 @@ public boolean shouldCreateEmptyEmbedded() { 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()); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/InsertOnlyProperty.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/InsertOnlyProperty.java new file mode 100644 index 0000000000..797af81378 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/InsertOnlyProperty.java @@ -0,0 +1,29 @@ +/* + * 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; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Documented +public @interface InsertOnlyProperty { +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java index e8fe3b5d2a..3f86d3d70c 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java @@ -73,8 +73,13 @@ default String getEmbeddedPrefix() { /** * 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(); } diff --git a/src/main/asciidoc/jdbc.adoc b/src/main/asciidoc/jdbc.adoc index 21f5dcc859..996204c5fa 100644 --- a/src/main/asciidoc/jdbc.adoc +++ b/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. 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 From 93d1bc1158f2aadbb8d8be846a173cdae75d23a8 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 20 Sep 2022 13:18:51 +0200 Subject: [PATCH 3/4] 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 --- .../data/r2dbc/core/R2dbcEntityTemplate.java | 1418 +++++++++-------- .../core/R2dbcEntityTemplateUnitTests.java | 896 ++++++----- 2 files changed, 1204 insertions(+), 1110 deletions(-) diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java index 7379f4eda8..431e5fe2af 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplate.java @@ -92,859 +92,867 @@ */ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAware, ApplicationContextAware { - private final DatabaseClient databaseClient; - - private final ReactiveDataAccessStrategy dataAccessStrategy; - - private final MappingContext, ? extends RelationalPersistentProperty> mappingContext; - - private final SpelAwareProxyProjectionFactory projectionFactory; - - private @Nullable ReactiveEntityCallbacks entityCallbacks; - - /** - * Create a new {@link R2dbcEntityTemplate} given {@link ConnectionFactory}. - * - * @param connectionFactory must not be {@literal null}. - * @since 1.2 - */ - public R2dbcEntityTemplate(ConnectionFactory connectionFactory) { - - Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); - - R2dbcDialect dialect = DialectResolver.getDialect(connectionFactory); - - this.databaseClient = DatabaseClient.builder().connectionFactory(connectionFactory) - .bindMarkers(dialect.getBindMarkersFactory()).build(); - this.dataAccessStrategy = new DefaultReactiveDataAccessStrategy(dialect); - this.mappingContext = dataAccessStrategy.getConverter().getMappingContext(); - this.projectionFactory = new SpelAwareProxyProjectionFactory(); - } - - /** - * Create a new {@link R2dbcEntityTemplate} given {@link DatabaseClient}. - * - * @param databaseClient must not be {@literal null}. - * @param dialect the dialect to use, must not be {@literal null}. - * @since 1.2 - */ - public R2dbcEntityTemplate(DatabaseClient databaseClient, R2dbcDialect dialect) { - this(databaseClient, new DefaultReactiveDataAccessStrategy(dialect)); - } - - /** - * Create a new {@link R2dbcEntityTemplate} given {@link DatabaseClient}, {@link R2dbcDialect} and - * {@link R2dbcConverter}. - * - * @param databaseClient must not be {@literal null}. - * @param dialect the dialect to use, must not be {@literal null}. - * @param converter the dialect to use, must not be {@literal null}. - * @since 1.2 - */ - public R2dbcEntityTemplate(DatabaseClient databaseClient, R2dbcDialect dialect, R2dbcConverter converter) { - this(databaseClient, new DefaultReactiveDataAccessStrategy(dialect, converter)); - } - - /** - * Create a new {@link R2dbcEntityTemplate} given {@link DatabaseClient} and {@link ReactiveDataAccessStrategy}. - * - * @param databaseClient must not be {@literal null}. - * @since 1.2 - */ - public R2dbcEntityTemplate(DatabaseClient databaseClient, ReactiveDataAccessStrategy strategy) { - - Assert.notNull(databaseClient, "DatabaseClient must not be null"); - Assert.notNull(strategy, "ReactiveDataAccessStrategy must not be null"); - - this.databaseClient = databaseClient; - this.dataAccessStrategy = strategy; - this.mappingContext = strategy.getConverter().getMappingContext(); - this.projectionFactory = new SpelAwareProxyProjectionFactory(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#getDatabaseClient() - */ - @Override - public DatabaseClient getDatabaseClient() { - return this.databaseClient; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#getDataAccessStrategy() - */ - @Override - public ReactiveDataAccessStrategy getDataAccessStrategy() { - return this.dataAccessStrategy; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#getConverter() - */ - @Override - public R2dbcConverter getConverter() { - return this.dataAccessStrategy.getConverter(); - } - - /* - * (non-Javadoc) - * @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory) - * @deprecated since 1.2 in favor of #setApplicationContext. - */ - @Override - @Deprecated - public void setBeanFactory(BeanFactory beanFactory) throws BeansException {} - - /* - * (non-Javadoc) - * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) - */ - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - - if (entityCallbacks == null) { - setEntityCallbacks(ReactiveEntityCallbacks.create(applicationContext)); - } - - projectionFactory.setBeanFactory(applicationContext); - projectionFactory.setBeanClassLoader(applicationContext.getClassLoader()); - } - - /** - * Set the {@link ReactiveEntityCallbacks} instance to use when invoking - * {@link org.springframework.data.mapping.callback.ReactiveEntityCallbacks callbacks} like the - * {@link BeforeSaveCallback}. - *

- * Overrides potentially existing {@link ReactiveEntityCallbacks}. - * - * @param entityCallbacks must not be {@literal null}. - * @throws IllegalArgumentException if the given instance is {@literal null}. - * @since 1.2 - */ - public void setEntityCallbacks(ReactiveEntityCallbacks entityCallbacks) { - - Assert.notNull(entityCallbacks, "EntityCallbacks must not be null"); - this.entityCallbacks = entityCallbacks; - } - - // ------------------------------------------------------------------------- - // Methods dealing with org.springframework.data.r2dbc.core.FluentR2dbcOperations - // ------------------------------------------------------------------------- - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.core.ReactiveSelectOperation#select(java.lang.Class) - */ - @Override - public ReactiveSelect select(Class domainType) { - return new ReactiveSelectOperationSupport(this).select(domainType); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.core.ReactiveInsertOperation#insert(java.lang.Class) - */ - @Override - public ReactiveInsert insert(Class domainType) { - return new ReactiveInsertOperationSupport(this).insert(domainType); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.core.ReactiveUpdateOperation#update(java.lang.Class) - */ - @Override - public ReactiveUpdate update(Class domainType) { - return new ReactiveUpdateOperationSupport(this).update(domainType); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.core.ReactiveDeleteOperation#delete(java.lang.Class) - */ - @Override - public ReactiveDelete delete(Class domainType) { - return new ReactiveDeleteOperationSupport(this).delete(domainType); - } - - // ------------------------------------------------------------------------- - // Methods dealing with org.springframework.data.r2dbc.query.Query - // ------------------------------------------------------------------------- - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#count(org.springframework.data.r2dbc.query.Query, java.lang.Class) - */ - @Override - public Mono count(Query query, Class entityClass) throws DataAccessException { - - Assert.notNull(query, "Query must not be null"); - Assert.notNull(entityClass, "Entity class must not be null"); - - return doCount(query, entityClass, getTableName(entityClass)); - } - - Mono doCount(Query query, Class entityClass, SqlIdentifier tableName) { - - RelationalPersistentEntity entity = getRequiredEntity(entityClass); - StatementMapper statementMapper = dataAccessStrategy.getStatementMapper().forType(entityClass); - - StatementMapper.SelectSpec selectSpec = statementMapper // - .createSelect(tableName) // - .doWithTable((table, spec) -> { - - Expression countExpression = entity.hasIdProperty() - ? table.column(entity.getRequiredIdProperty().getColumnName()) - : Expressions.just("1"); - return spec.withProjection(Functions.count(countExpression)); - }); - - Optional criteria = query.getCriteria(); - if (criteria.isPresent()) { - selectSpec = criteria.map(selectSpec::withCriteria).orElse(selectSpec); - } - - PreparedOperation operation = statementMapper.getMappedObject(selectSpec); - - return this.databaseClient.sql(operation) // - .map((r, md) -> r.get(0, Long.class)) // - .first() // - .defaultIfEmpty(0L); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#exists(org.springframework.data.r2dbc.query.Query, java.lang.Class) - */ - @Override - public Mono exists(Query query, Class entityClass) throws DataAccessException { - - Assert.notNull(query, "Query must not be null"); - Assert.notNull(entityClass, "Entity class must not be null"); - - return doExists(query, entityClass, getTableName(entityClass)); - } - - Mono doExists(Query query, Class entityClass, SqlIdentifier tableName) { - - RelationalPersistentEntity entity = getRequiredEntity(entityClass); - StatementMapper statementMapper = dataAccessStrategy.getStatementMapper().forType(entityClass); - - StatementMapper.SelectSpec selectSpec = statementMapper.createSelect(tableName).limit(1); - if (entity.hasIdProperty()) { - selectSpec = selectSpec // - .withProjection(entity.getRequiredIdProperty().getColumnName()); - - } else { - selectSpec = selectSpec.withProjection(Expressions.just("1")); - } - - Optional criteria = query.getCriteria(); - if (criteria.isPresent()) { - selectSpec = criteria.map(selectSpec::withCriteria).orElse(selectSpec); - } - - PreparedOperation operation = statementMapper.getMappedObject(selectSpec); - - return this.databaseClient.sql(operation) // - .map((r, md) -> r) // - .first() // - .hasElement(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#select(org.springframework.data.r2dbc.query.Query, java.lang.Class) - */ - @Override - public Flux select(Query query, Class entityClass) throws DataAccessException { - - Assert.notNull(query, "Query must not be null"); - Assert.notNull(entityClass, "Entity class must not be null"); + private final DatabaseClient databaseClient; + + private final ReactiveDataAccessStrategy dataAccessStrategy; + + private final MappingContext, ? extends RelationalPersistentProperty> mappingContext; + + private final SpelAwareProxyProjectionFactory projectionFactory; + + private @Nullable ReactiveEntityCallbacks entityCallbacks; + + /** + * Create a new {@link R2dbcEntityTemplate} given {@link ConnectionFactory}. + * + * @param connectionFactory must not be {@literal null}. + * @since 1.2 + */ + public R2dbcEntityTemplate(ConnectionFactory connectionFactory) { + + Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); + + R2dbcDialect dialect = DialectResolver.getDialect(connectionFactory); + + this.databaseClient = DatabaseClient.builder().connectionFactory(connectionFactory) + .bindMarkers(dialect.getBindMarkersFactory()).build(); + this.dataAccessStrategy = new DefaultReactiveDataAccessStrategy(dialect); + this.mappingContext = dataAccessStrategy.getConverter().getMappingContext(); + this.projectionFactory = new SpelAwareProxyProjectionFactory(); + } + + /** + * Create a new {@link R2dbcEntityTemplate} given {@link DatabaseClient}. + * + * @param databaseClient must not be {@literal null}. + * @param dialect the dialect to use, must not be {@literal null}. + * @since 1.2 + */ + public R2dbcEntityTemplate(DatabaseClient databaseClient, R2dbcDialect dialect) { + this(databaseClient, new DefaultReactiveDataAccessStrategy(dialect)); + } + + /** + * Create a new {@link R2dbcEntityTemplate} given {@link DatabaseClient}, {@link R2dbcDialect} and + * {@link R2dbcConverter}. + * + * @param databaseClient must not be {@literal null}. + * @param dialect the dialect to use, must not be {@literal null}. + * @param converter the dialect to use, must not be {@literal null}. + * @since 1.2 + */ + public R2dbcEntityTemplate(DatabaseClient databaseClient, R2dbcDialect dialect, R2dbcConverter converter) { + this(databaseClient, new DefaultReactiveDataAccessStrategy(dialect, converter)); + } + + /** + * Create a new {@link R2dbcEntityTemplate} given {@link DatabaseClient} and {@link ReactiveDataAccessStrategy}. + * + * @param databaseClient must not be {@literal null}. + * @since 1.2 + */ + public R2dbcEntityTemplate(DatabaseClient databaseClient, ReactiveDataAccessStrategy strategy) { + + Assert.notNull(databaseClient, "DatabaseClient must not be null"); + Assert.notNull(strategy, "ReactiveDataAccessStrategy must not be null"); + + this.databaseClient = databaseClient; + this.dataAccessStrategy = strategy; + this.mappingContext = strategy.getConverter().getMappingContext(); + this.projectionFactory = new SpelAwareProxyProjectionFactory(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#getDatabaseClient() + */ + @Override + public DatabaseClient getDatabaseClient() { + return this.databaseClient; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#getDataAccessStrategy() + */ + @Override + public ReactiveDataAccessStrategy getDataAccessStrategy() { + return this.dataAccessStrategy; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#getConverter() + */ + @Override + public R2dbcConverter getConverter() { + return this.dataAccessStrategy.getConverter(); + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory) + * @deprecated since 1.2 in favor of #setApplicationContext. + */ + @Override + @Deprecated + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + } + + /* + * (non-Javadoc) + * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) + */ + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + + if (entityCallbacks == null) { + setEntityCallbacks(ReactiveEntityCallbacks.create(applicationContext)); + } + + projectionFactory.setBeanFactory(applicationContext); + projectionFactory.setBeanClassLoader(applicationContext.getClassLoader()); + } + + /** + * Set the {@link ReactiveEntityCallbacks} instance to use when invoking + * {@link org.springframework.data.mapping.callback.ReactiveEntityCallbacks callbacks} like the + * {@link BeforeSaveCallback}. + *

+ * Overrides potentially existing {@link ReactiveEntityCallbacks}. + * + * @param entityCallbacks must not be {@literal null}. + * @throws IllegalArgumentException if the given instance is {@literal null}. + * @since 1.2 + */ + public void setEntityCallbacks(ReactiveEntityCallbacks entityCallbacks) { + + Assert.notNull(entityCallbacks, "EntityCallbacks must not be null"); + this.entityCallbacks = entityCallbacks; + } + + // ------------------------------------------------------------------------- + // Methods dealing with org.springframework.data.r2dbc.core.FluentR2dbcOperations + // ------------------------------------------------------------------------- + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.core.ReactiveSelectOperation#select(java.lang.Class) + */ + @Override + public ReactiveSelect select(Class domainType) { + return new ReactiveSelectOperationSupport(this).select(domainType); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.core.ReactiveInsertOperation#insert(java.lang.Class) + */ + @Override + public ReactiveInsert insert(Class domainType) { + return new ReactiveInsertOperationSupport(this).insert(domainType); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.core.ReactiveUpdateOperation#update(java.lang.Class) + */ + @Override + public ReactiveUpdate update(Class domainType) { + return new ReactiveUpdateOperationSupport(this).update(domainType); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.core.ReactiveDeleteOperation#delete(java.lang.Class) + */ + @Override + public ReactiveDelete delete(Class domainType) { + return new ReactiveDeleteOperationSupport(this).delete(domainType); + } + + // ------------------------------------------------------------------------- + // Methods dealing with org.springframework.data.r2dbc.query.Query + // ------------------------------------------------------------------------- + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#count(org.springframework.data.r2dbc.query.Query, java.lang.Class) + */ + @Override + public Mono count(Query query, Class entityClass) throws DataAccessException { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entityClass, "Entity class must not be null"); + + return doCount(query, entityClass, getTableName(entityClass)); + } + + Mono doCount(Query query, Class entityClass, SqlIdentifier tableName) { + + RelationalPersistentEntity entity = getRequiredEntity(entityClass); + StatementMapper statementMapper = dataAccessStrategy.getStatementMapper().forType(entityClass); + + StatementMapper.SelectSpec selectSpec = statementMapper // + .createSelect(tableName) // + .doWithTable((table, spec) -> { + + Expression countExpression = entity.hasIdProperty() + ? table.column(entity.getRequiredIdProperty().getColumnName()) + : Expressions.just("1"); + return spec.withProjection(Functions.count(countExpression)); + }); + + Optional criteria = query.getCriteria(); + if (criteria.isPresent()) { + selectSpec = criteria.map(selectSpec::withCriteria).orElse(selectSpec); + } + + PreparedOperation operation = statementMapper.getMappedObject(selectSpec); + + return this.databaseClient.sql(operation) // + .map((r, md) -> r.get(0, Long.class)) // + .first() // + .defaultIfEmpty(0L); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#exists(org.springframework.data.r2dbc.query.Query, java.lang.Class) + */ + @Override + public Mono exists(Query query, Class entityClass) throws DataAccessException { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entityClass, "Entity class must not be null"); + + return doExists(query, entityClass, getTableName(entityClass)); + } + + Mono doExists(Query query, Class entityClass, SqlIdentifier tableName) { + + RelationalPersistentEntity entity = getRequiredEntity(entityClass); + StatementMapper statementMapper = dataAccessStrategy.getStatementMapper().forType(entityClass); + + StatementMapper.SelectSpec selectSpec = statementMapper.createSelect(tableName).limit(1); + if (entity.hasIdProperty()) { + selectSpec = selectSpec // + .withProjection(entity.getRequiredIdProperty().getColumnName()); + + } else { + selectSpec = selectSpec.withProjection(Expressions.just("1")); + } + + Optional criteria = query.getCriteria(); + if (criteria.isPresent()) { + selectSpec = criteria.map(selectSpec::withCriteria).orElse(selectSpec); + } + + PreparedOperation operation = statementMapper.getMappedObject(selectSpec); + + return this.databaseClient.sql(operation) // + .map((r, md) -> r) // + .first() // + .hasElement(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#select(org.springframework.data.r2dbc.query.Query, java.lang.Class) + */ + @Override + public Flux select(Query query, Class entityClass) throws DataAccessException { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entityClass, "Entity class must not be null"); - SqlIdentifier tableName = getTableName(entityClass); - return doSelect(query, entityClass, tableName, entityClass, RowsFetchSpec::all); - } + SqlIdentifier tableName = getTableName(entityClass); + return doSelect(query, entityClass, tableName, entityClass, RowsFetchSpec::all); + } - @SuppressWarnings("unchecked") - > P doSelect(Query query, Class entityClass, SqlIdentifier tableName, - Class returnType, Function, P> resultHandler) { + @SuppressWarnings("unchecked") + > P doSelect(Query query, Class entityClass, SqlIdentifier tableName, + Class returnType, Function, P> resultHandler) { - RowsFetchSpec fetchSpec = doSelect(query, entityClass, tableName, returnType); + RowsFetchSpec fetchSpec = doSelect(query, entityClass, tableName, returnType); - P result = resultHandler.apply(fetchSpec); + P result = resultHandler.apply(fetchSpec); - if (result instanceof Mono) { - return (P) ((Mono) result).flatMap(it -> maybeCallAfterConvert(it, tableName)); - } + if (result instanceof Mono) { + return (P) ((Mono) result).flatMap(it -> maybeCallAfterConvert(it, tableName)); + } - return (P) ((Flux) result).concatMap(it -> maybeCallAfterConvert(it, tableName)); - } + return (P) ((Flux) result).concatMap(it -> maybeCallAfterConvert(it, tableName)); + } - private RowsFetchSpec doSelect(Query query, Class entityClass, SqlIdentifier tableName, - Class returnType) { + private RowsFetchSpec doSelect(Query query, Class entityClass, SqlIdentifier tableName, + Class returnType) { - StatementMapper statementMapper = dataAccessStrategy.getStatementMapper().forType(entityClass); + StatementMapper statementMapper = dataAccessStrategy.getStatementMapper().forType(entityClass); - StatementMapper.SelectSpec selectSpec = statementMapper // - .createSelect(tableName) // - .doWithTable((table, spec) -> spec.withProjection(getSelectProjection(table, query, returnType))); + StatementMapper.SelectSpec selectSpec = statementMapper // + .createSelect(tableName) // + .doWithTable((table, spec) -> spec.withProjection(getSelectProjection(table, query, returnType))); - if (query.getLimit() > 0) { - selectSpec = selectSpec.limit(query.getLimit()); - } + if (query.getLimit() > 0) { + selectSpec = selectSpec.limit(query.getLimit()); + } - if (query.getOffset() > 0) { - selectSpec = selectSpec.offset(query.getOffset()); - } + if (query.getOffset() > 0) { + selectSpec = selectSpec.offset(query.getOffset()); + } - if (query.isSorted()) { - selectSpec = selectSpec.withSort(query.getSort()); - } + if (query.isSorted()) { + selectSpec = selectSpec.withSort(query.getSort()); + } - Optional criteria = query.getCriteria(); - if (criteria.isPresent()) { - selectSpec = criteria.map(selectSpec::withCriteria).orElse(selectSpec); - } + Optional criteria = query.getCriteria(); + if (criteria.isPresent()) { + selectSpec = criteria.map(selectSpec::withCriteria).orElse(selectSpec); + } - PreparedOperation operation = statementMapper.getMappedObject(selectSpec); + PreparedOperation operation = statementMapper.getMappedObject(selectSpec); - return getRowsFetchSpec(databaseClient.sql(operation), entityClass, returnType); - } + return getRowsFetchSpec(databaseClient.sql(operation), entityClass, returnType); + } - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#selectOne(org.springframework.data.r2dbc.query.Query, java.lang.Class) - */ - @Override - public Mono selectOne(Query query, Class entityClass) throws DataAccessException { - return doSelect(query.isLimited() ? query : query.limit(2), entityClass, getTableName(entityClass), entityClass, - RowsFetchSpec::one); - } + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#selectOne(org.springframework.data.r2dbc.query.Query, java.lang.Class) + */ + @Override + public Mono selectOne(Query query, Class entityClass) throws DataAccessException { + return doSelect(query.isLimited() ? query : query.limit(2), entityClass, getTableName(entityClass), entityClass, + RowsFetchSpec::one); + } - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#update(org.springframework.data.r2dbc.query.Query, org.springframework.data.r2dbc.query.Update, java.lang.Class) - */ - @Override - public Mono update(Query query, Update update, Class entityClass) throws DataAccessException { + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#update(org.springframework.data.r2dbc.query.Query, org.springframework.data.r2dbc.query.Update, java.lang.Class) + */ + @Override + public Mono update(Query query, Update update, Class entityClass) throws DataAccessException { - Assert.notNull(query, "Query must not be null"); - Assert.notNull(update, "Update must not be null"); - Assert.notNull(entityClass, "Entity class must not be null"); + Assert.notNull(query, "Query must not be null"); + Assert.notNull(update, "Update must not be null"); + Assert.notNull(entityClass, "Entity class must not be null"); - return doUpdate(query, update, entityClass, getTableName(entityClass)); - } - - Mono doUpdate(Query query, Update update, Class entityClass, SqlIdentifier tableName) { + return doUpdate(query, update, entityClass, getTableName(entityClass)); + } + + Mono doUpdate(Query query, Update update, Class entityClass, SqlIdentifier tableName) { - StatementMapper statementMapper = dataAccessStrategy.getStatementMapper().forType(entityClass); + StatementMapper statementMapper = dataAccessStrategy.getStatementMapper().forType(entityClass); - StatementMapper.UpdateSpec selectSpec = statementMapper // - .createUpdate(tableName, update); + StatementMapper.UpdateSpec selectSpec = statementMapper // + .createUpdate(tableName, update); - Optional criteria = query.getCriteria(); - if (criteria.isPresent()) { - selectSpec = criteria.map(selectSpec::withCriteria).orElse(selectSpec); - } + Optional criteria = query.getCriteria(); + if (criteria.isPresent()) { + selectSpec = criteria.map(selectSpec::withCriteria).orElse(selectSpec); + } - PreparedOperation operation = statementMapper.getMappedObject(selectSpec); - return this.databaseClient.sql(operation).fetch().rowsUpdated(); - } + PreparedOperation operation = statementMapper.getMappedObject(selectSpec); + return this.databaseClient.sql(operation).fetch().rowsUpdated(); + } - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#delete(org.springframework.data.r2dbc.query.Query, java.lang.Class) - */ - @Override - public Mono delete(Query query, Class entityClass) throws DataAccessException { + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#delete(org.springframework.data.r2dbc.query.Query, java.lang.Class) + */ + @Override + public Mono delete(Query query, Class entityClass) throws DataAccessException { - Assert.notNull(query, "Query must not be null"); - Assert.notNull(entityClass, "Entity class must not be null"); + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entityClass, "Entity class must not be null"); - return doDelete(query, entityClass, getTableName(entityClass)); - } + return doDelete(query, entityClass, getTableName(entityClass)); + } - Mono doDelete(Query query, Class entityClass, SqlIdentifier tableName) { + Mono doDelete(Query query, Class entityClass, SqlIdentifier tableName) { - StatementMapper statementMapper = dataAccessStrategy.getStatementMapper().forType(entityClass); + StatementMapper statementMapper = dataAccessStrategy.getStatementMapper().forType(entityClass); - StatementMapper.DeleteSpec deleteSpec = statementMapper // - .createDelete(tableName); + StatementMapper.DeleteSpec deleteSpec = statementMapper // + .createDelete(tableName); - Optional criteria = query.getCriteria(); - if (criteria.isPresent()) { - deleteSpec = criteria.map(deleteSpec::withCriteria).orElse(deleteSpec); - } + Optional criteria = query.getCriteria(); + if (criteria.isPresent()) { + deleteSpec = criteria.map(deleteSpec::withCriteria).orElse(deleteSpec); + } - PreparedOperation operation = statementMapper.getMappedObject(deleteSpec); - return this.databaseClient.sql(operation).fetch().rowsUpdated().defaultIfEmpty(0L); - } + PreparedOperation operation = statementMapper.getMappedObject(deleteSpec); + return this.databaseClient.sql(operation).fetch().rowsUpdated().defaultIfEmpty(0L); + } - // ------------------------------------------------------------------------- - // Methods dealing with org.springframework.r2dbc.core.PreparedOperation - // ------------------------------------------------------------------------- + // ------------------------------------------------------------------------- + // Methods dealing with org.springframework.r2dbc.core.PreparedOperation + // ------------------------------------------------------------------------- - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#query(org.springframework.r2dbc.core.PreparedOperation, java.lang.Class) - */ - @Override - public RowsFetchSpec query(PreparedOperation operation, Class entityClass) { + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#query(org.springframework.r2dbc.core.PreparedOperation, java.lang.Class) + */ + @Override + public RowsFetchSpec query(PreparedOperation operation, Class entityClass) { - Assert.notNull(operation, "PreparedOperation must not be null"); - Assert.notNull(entityClass, "Entity class must not be null"); + Assert.notNull(operation, "PreparedOperation must not be null"); + Assert.notNull(entityClass, "Entity class must not be null"); - return new EntityCallbackAdapter<>(getRowsFetchSpec(databaseClient.sql(operation), entityClass, entityClass), - getTableNameOrEmpty(entityClass)); - } + return new EntityCallbackAdapter<>(getRowsFetchSpec(databaseClient.sql(operation), entityClass, entityClass), + getTableNameOrEmpty(entityClass)); + } - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#query(org.springframework.r2dbc.core.PreparedOperation, java.util.function.BiFunction) - */ - @Override - public RowsFetchSpec query(PreparedOperation operation, BiFunction rowMapper) { + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#query(org.springframework.r2dbc.core.PreparedOperation, java.util.function.BiFunction) + */ + @Override + public RowsFetchSpec query(PreparedOperation operation, BiFunction rowMapper) { - Assert.notNull(operation, "PreparedOperation must not be null"); - Assert.notNull(rowMapper, "Row mapper must not be null"); + Assert.notNull(operation, "PreparedOperation must not be null"); + Assert.notNull(rowMapper, "Row mapper must not be null"); - return new EntityCallbackAdapter<>(databaseClient.sql(operation).map(rowMapper), SqlIdentifier.EMPTY); - } + return new EntityCallbackAdapter<>(databaseClient.sql(operation).map(rowMapper), SqlIdentifier.EMPTY); + } - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#query(org.springframework.r2dbc.core.PreparedOperation, java.lang.Class, java.util.function.BiFunction) - */ - @Override - public RowsFetchSpec query(PreparedOperation operation, Class entityClass, - BiFunction rowMapper) { + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#query(org.springframework.r2dbc.core.PreparedOperation, java.lang.Class, java.util.function.BiFunction) + */ + @Override + public RowsFetchSpec query(PreparedOperation operation, Class entityClass, + BiFunction rowMapper) { - Assert.notNull(operation, "PreparedOperation must not be null"); - Assert.notNull(entityClass, "Entity class must not be null"); - Assert.notNull(rowMapper, "Row mapper must not be null"); + Assert.notNull(operation, "PreparedOperation must not be null"); + Assert.notNull(entityClass, "Entity class must not be null"); + Assert.notNull(rowMapper, "Row mapper must not be null"); - return new EntityCallbackAdapter<>(databaseClient.sql(operation).map(rowMapper), getTableNameOrEmpty(entityClass)); - } + return new EntityCallbackAdapter<>(databaseClient.sql(operation).map(rowMapper), getTableNameOrEmpty(entityClass)); + } - // ------------------------------------------------------------------------- - // Methods dealing with entities - // ------------------------------------------------------------------------- + // ------------------------------------------------------------------------- + // Methods dealing with entities + // ------------------------------------------------------------------------- - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#insert(java.lang.Object) - */ - @Override - public Mono insert(T entity) throws DataAccessException { + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#insert(java.lang.Object) + */ + @Override + public Mono insert(T entity) throws DataAccessException { - Assert.notNull(entity, "Entity must not be null"); + Assert.notNull(entity, "Entity must not be null"); - return doInsert(entity, getRequiredEntity(entity).getTableName()); - } + return doInsert(entity, getRequiredEntity(entity).getTableName()); + } - Mono doInsert(T entity, SqlIdentifier tableName) { + Mono doInsert(T entity, SqlIdentifier tableName) { - RelationalPersistentEntity persistentEntity = getRequiredEntity(entity); + RelationalPersistentEntity persistentEntity = getRequiredEntity(entity); - return maybeCallBeforeConvert(entity, tableName).flatMap(onBeforeConvert -> { + return maybeCallBeforeConvert(entity, tableName).flatMap(onBeforeConvert -> { - T initializedEntity = setVersionIfNecessary(persistentEntity, onBeforeConvert); + T initializedEntity = setVersionIfNecessary(persistentEntity, onBeforeConvert); - OutboundRow outboundRow = dataAccessStrategy.getOutboundRow(initializedEntity); + OutboundRow outboundRow = dataAccessStrategy.getOutboundRow(initializedEntity); - potentiallyRemoveId(persistentEntity, outboundRow); + potentiallyRemoveId(persistentEntity, outboundRow); - return maybeCallBeforeSave(initializedEntity, outboundRow, tableName) // - .flatMap(entityToSave -> doInsert(entityToSave, tableName, outboundRow)); - }); - } + return maybeCallBeforeSave(initializedEntity, outboundRow, tableName) // + .flatMap(entityToSave -> doInsert(entityToSave, tableName, outboundRow)); + }); + } - private void potentiallyRemoveId(RelationalPersistentEntity persistentEntity, OutboundRow outboundRow) { + private void potentiallyRemoveId(RelationalPersistentEntity persistentEntity, OutboundRow outboundRow) { - RelationalPersistentProperty idProperty = persistentEntity.getIdProperty(); - if (idProperty == null) { - return; - } + RelationalPersistentProperty idProperty = persistentEntity.getIdProperty(); + if (idProperty == null) { + return; + } - SqlIdentifier columnName = idProperty.getColumnName(); - Parameter parameter = outboundRow.get(columnName); + SqlIdentifier columnName = idProperty.getColumnName(); + Parameter parameter = outboundRow.get(columnName); - if (shouldSkipIdValue(parameter, idProperty)) { - outboundRow.remove(columnName); - } - } + if (shouldSkipIdValue(parameter, idProperty)) { + outboundRow.remove(columnName); + } + } - private boolean shouldSkipIdValue(@Nullable Parameter value, RelationalPersistentProperty property) { + private boolean shouldSkipIdValue(@Nullable Parameter value, RelationalPersistentProperty property) { - if (value == null || value.getValue() == null) { - return true; - } + if (value == null || value.getValue() == null) { + return true; + } - if (value.getValue() instanceof Number) { - return ((Number) value.getValue()).longValue() == 0L; - } + if (value.getValue() instanceof Number) { + return ((Number) value.getValue()).longValue() == 0L; + } - return false; - } + return false; + } - private Mono doInsert(T entity, SqlIdentifier tableName, OutboundRow outboundRow) { + private Mono doInsert(T entity, SqlIdentifier tableName, OutboundRow outboundRow) { - StatementMapper mapper = dataAccessStrategy.getStatementMapper(); - StatementMapper.InsertSpec insert = mapper.createInsert(tableName); + StatementMapper mapper = dataAccessStrategy.getStatementMapper(); + StatementMapper.InsertSpec insert = mapper.createInsert(tableName); - for (SqlIdentifier column : outboundRow.keySet()) { - Parameter settableValue = outboundRow.get(column); - if (settableValue.hasValue()) { - insert = insert.withColumn(column, settableValue); - } - } + for (SqlIdentifier column : outboundRow.keySet()) { + Parameter settableValue = outboundRow.get(column); + if (settableValue.hasValue()) { + insert = insert.withColumn(column, settableValue); + } + } - PreparedOperation operation = mapper.getMappedObject(insert); + PreparedOperation operation = mapper.getMappedObject(insert); - List identifierColumns = dataAccessStrategy.getIdentifierColumns(entity.getClass()); + List identifierColumns = dataAccessStrategy.getIdentifierColumns(entity.getClass()); - return this.databaseClient.sql(operation) // - .filter(statement -> { + return this.databaseClient.sql(operation) // + .filter(statement -> { - if (identifierColumns.isEmpty()) { - return statement.returnGeneratedValues(); - } + if (identifierColumns.isEmpty()) { + return statement.returnGeneratedValues(); + } - return statement.returnGeneratedValues(dataAccessStrategy.renderForGeneratedValues(identifierColumns.get(0))); - }).map(this.dataAccessStrategy.getConverter().populateIdIfNecessary(entity)) // - .all() // - .last(entity).flatMap(saved -> maybeCallAfterSave(saved, outboundRow, tableName)); - } + return statement.returnGeneratedValues(dataAccessStrategy.renderForGeneratedValues(identifierColumns.get(0))); + }).map(this.dataAccessStrategy.getConverter().populateIdIfNecessary(entity)) // + .all() // + .last(entity).flatMap(saved -> maybeCallAfterSave(saved, outboundRow, tableName)); + } - @SuppressWarnings("unchecked") - private T setVersionIfNecessary(RelationalPersistentEntity persistentEntity, T entity) { + @SuppressWarnings("unchecked") + private T setVersionIfNecessary(RelationalPersistentEntity persistentEntity, T entity) { - RelationalPersistentProperty versionProperty = persistentEntity.getVersionProperty(); - if (versionProperty == null) { - return entity; - } + RelationalPersistentProperty versionProperty = persistentEntity.getVersionProperty(); + if (versionProperty == null) { + return entity; + } - Class versionPropertyType = versionProperty.getType(); - Long version = versionPropertyType.isPrimitive() ? 1L : 0L; - ConversionService conversionService = this.dataAccessStrategy.getConverter().getConversionService(); - PersistentPropertyAccessor propertyAccessor = persistentEntity.getPropertyAccessor(entity); - propertyAccessor.setProperty(versionProperty, conversionService.convert(version, versionPropertyType)); + Class versionPropertyType = versionProperty.getType(); + Long version = versionPropertyType.isPrimitive() ? 1L : 0L; + ConversionService conversionService = this.dataAccessStrategy.getConverter().getConversionService(); + PersistentPropertyAccessor propertyAccessor = persistentEntity.getPropertyAccessor(entity); + propertyAccessor.setProperty(versionProperty, conversionService.convert(version, versionPropertyType)); - return (T) propertyAccessor.getBean(); - } + return (T) propertyAccessor.getBean(); + } - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#update(java.lang.Object) - */ - @Override - public Mono update(T entity) throws DataAccessException { + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#update(java.lang.Object) + */ + @Override + public Mono update(T entity) throws DataAccessException { - Assert.notNull(entity, "Entity must not be null"); + Assert.notNull(entity, "Entity must not be null"); - return doUpdate(entity, getRequiredEntity(entity).getTableName()); - } + return doUpdate(entity, getRequiredEntity(entity).getTableName()); + } - private Mono doUpdate(T entity, SqlIdentifier tableName) { + private Mono doUpdate(T entity, SqlIdentifier tableName) { - RelationalPersistentEntity persistentEntity = getRequiredEntity(entity); + RelationalPersistentEntity persistentEntity = getRequiredEntity(entity); - return maybeCallBeforeConvert(entity, tableName).flatMap(onBeforeConvert -> { + return maybeCallBeforeConvert(entity, tableName).flatMap(onBeforeConvert -> { - T entityToUse; - Criteria matchingVersionCriteria; + T entityToUse; + Criteria matchingVersionCriteria; - if (persistentEntity.hasVersionProperty()) { + if (persistentEntity.hasVersionProperty()) { - matchingVersionCriteria = createMatchingVersionCriteria(onBeforeConvert, persistentEntity); - entityToUse = incrementVersion(persistentEntity, onBeforeConvert); - } else { + matchingVersionCriteria = createMatchingVersionCriteria(onBeforeConvert, persistentEntity); + entityToUse = incrementVersion(persistentEntity, onBeforeConvert); + } else { - entityToUse = onBeforeConvert; - matchingVersionCriteria = null; - } + entityToUse = onBeforeConvert; + matchingVersionCriteria = null; + } - OutboundRow outboundRow = dataAccessStrategy.getOutboundRow(entityToUse); + OutboundRow outboundRow = dataAccessStrategy.getOutboundRow(entityToUse); - return maybeCallBeforeSave(entityToUse, outboundRow, tableName) // - .flatMap(onBeforeSave -> { + return maybeCallBeforeSave(entityToUse, outboundRow, tableName) // + .flatMap(onBeforeSave -> { - SqlIdentifier idColumn = persistentEntity.getRequiredIdProperty().getColumnName(); - Parameter id = outboundRow.remove(idColumn); - Criteria criteria = Criteria.where(dataAccessStrategy.toSql(idColumn)).is(id); + SqlIdentifier idColumn = persistentEntity.getRequiredIdProperty().getColumnName(); + Parameter id = outboundRow.remove(idColumn); - if (matchingVersionCriteria != null) { - criteria = criteria.and(matchingVersionCriteria); - } + persistentEntity.forEach(p -> { + if (p.isInsertOnly()) { + outboundRow.remove(p.getColumnName()); + } + }); - return doUpdate(onBeforeSave, tableName, persistentEntity, criteria, outboundRow); - }); - }); - } + Criteria criteria = Criteria.where(dataAccessStrategy.toSql(idColumn)).is(id); - @SuppressWarnings({ "unchecked", "rawtypes" }) - private Mono doUpdate(T entity, SqlIdentifier tableName, RelationalPersistentEntity persistentEntity, - Criteria criteria, OutboundRow outboundRow) { + if (matchingVersionCriteria != null) { + criteria = criteria.and(matchingVersionCriteria); + } - Update update = Update.from((Map) outboundRow); + return doUpdate(onBeforeSave, tableName, persistentEntity, criteria, outboundRow); + }); + }); + } - StatementMapper mapper = dataAccessStrategy.getStatementMapper(); - StatementMapper.UpdateSpec updateSpec = mapper.createUpdate(tableName, update).withCriteria(criteria); + @SuppressWarnings({"unchecked", "rawtypes"}) + private Mono doUpdate(T entity, SqlIdentifier tableName, RelationalPersistentEntity persistentEntity, + Criteria criteria, OutboundRow outboundRow) { - PreparedOperation operation = mapper.getMappedObject(updateSpec); + Update update = Update.from((Map) outboundRow); - return this.databaseClient.sql(operation) // - .fetch() // - .rowsUpdated() // - .handle((rowsUpdated, sink) -> { + StatementMapper mapper = dataAccessStrategy.getStatementMapper(); + StatementMapper.UpdateSpec updateSpec = mapper.createUpdate(tableName, update).withCriteria(criteria); - if (rowsUpdated != 0) { - return; - } + PreparedOperation operation = mapper.getMappedObject(updateSpec); - if (persistentEntity.hasVersionProperty()) { - sink.error(new OptimisticLockingFailureException( - formatOptimisticLockingExceptionMessage(entity, persistentEntity))); - } else { - sink.error(new TransientDataAccessResourceException( - formatTransientEntityExceptionMessage(entity, persistentEntity))); - } - }).then(maybeCallAfterSave(entity, outboundRow, tableName)); - } + return this.databaseClient.sql(operation) // + .fetch() // + .rowsUpdated() // + .handle((rowsUpdated, sink) -> { - private String formatOptimisticLockingExceptionMessage(T entity, RelationalPersistentEntity persistentEntity) { + if (rowsUpdated != 0) { + return; + } - return String.format("Failed to update table [%s]; Version does not match for row with Id [%s]", - persistentEntity.getTableName(), persistentEntity.getIdentifierAccessor(entity).getIdentifier()); - } + if (persistentEntity.hasVersionProperty()) { + sink.error(new OptimisticLockingFailureException( + formatOptimisticLockingExceptionMessage(entity, persistentEntity))); + } else { + sink.error(new TransientDataAccessResourceException( + formatTransientEntityExceptionMessage(entity, persistentEntity))); + } + }).then(maybeCallAfterSave(entity, outboundRow, tableName)); + } - private String formatTransientEntityExceptionMessage(T entity, RelationalPersistentEntity persistentEntity) { + private String formatOptimisticLockingExceptionMessage(T entity, RelationalPersistentEntity persistentEntity) { - return String.format("Failed to update table [%s]; Row with Id [%s] does not exist", - persistentEntity.getTableName(), persistentEntity.getIdentifierAccessor(entity).getIdentifier()); - } + return String.format("Failed to update table [%s]; Version does not match for row with Id [%s]", + persistentEntity.getTableName(), persistentEntity.getIdentifierAccessor(entity).getIdentifier()); + } - @SuppressWarnings("unchecked") - private T incrementVersion(RelationalPersistentEntity persistentEntity, T entity) { + private String formatTransientEntityExceptionMessage(T entity, RelationalPersistentEntity persistentEntity) { - PersistentPropertyAccessor propertyAccessor = persistentEntity.getPropertyAccessor(entity); - RelationalPersistentProperty versionProperty = persistentEntity.getVersionProperty(); + return String.format("Failed to update table [%s]; Row with Id [%s] does not exist", + persistentEntity.getTableName(), persistentEntity.getIdentifierAccessor(entity).getIdentifier()); + } - ConversionService conversionService = this.dataAccessStrategy.getConverter().getConversionService(); - Object currentVersionValue = propertyAccessor.getProperty(versionProperty); - long newVersionValue = 1L; - if (currentVersionValue != null) { - newVersionValue = conversionService.convert(currentVersionValue, Long.class) + 1; - } - Class versionPropertyType = versionProperty.getType(); - propertyAccessor.setProperty(versionProperty, conversionService.convert(newVersionValue, versionPropertyType)); + @SuppressWarnings("unchecked") + private T incrementVersion(RelationalPersistentEntity persistentEntity, T entity) { - return (T) propertyAccessor.getBean(); - } + PersistentPropertyAccessor propertyAccessor = persistentEntity.getPropertyAccessor(entity); + RelationalPersistentProperty versionProperty = persistentEntity.getVersionProperty(); - private Criteria createMatchingVersionCriteria(T entity, RelationalPersistentEntity persistentEntity) { + ConversionService conversionService = this.dataAccessStrategy.getConverter().getConversionService(); + Object currentVersionValue = propertyAccessor.getProperty(versionProperty); + long newVersionValue = 1L; + if (currentVersionValue != null) { + newVersionValue = conversionService.convert(currentVersionValue, Long.class) + 1; + } + Class versionPropertyType = versionProperty.getType(); + propertyAccessor.setProperty(versionProperty, conversionService.convert(newVersionValue, versionPropertyType)); - PersistentPropertyAccessor propertyAccessor = persistentEntity.getPropertyAccessor(entity); - RelationalPersistentProperty versionProperty = persistentEntity.getVersionProperty(); + return (T) propertyAccessor.getBean(); + } - Object version = propertyAccessor.getProperty(versionProperty); - Criteria.CriteriaStep versionColumn = Criteria.where(dataAccessStrategy.toSql(versionProperty.getColumnName())); - if (version == null) { - return versionColumn.isNull(); - } else { - return versionColumn.is(version); - } - } + private Criteria createMatchingVersionCriteria(T entity, RelationalPersistentEntity persistentEntity) { - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#delete(java.lang.Object) - */ - @Override - public Mono delete(T entity) throws DataAccessException { + PersistentPropertyAccessor propertyAccessor = persistentEntity.getPropertyAccessor(entity); + RelationalPersistentProperty versionProperty = persistentEntity.getVersionProperty(); - Assert.notNull(entity, "Entity must not be null"); + Object version = propertyAccessor.getProperty(versionProperty); + Criteria.CriteriaStep versionColumn = Criteria.where(dataAccessStrategy.toSql(versionProperty.getColumnName())); + if (version == null) { + return versionColumn.isNull(); + } else { + return versionColumn.is(version); + } + } - RelationalPersistentEntity persistentEntity = getRequiredEntity(entity); + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.core.R2dbcEntityOperations#delete(java.lang.Object) + */ + @Override + public Mono delete(T entity) throws DataAccessException { - return delete(getByIdQuery(entity, persistentEntity), persistentEntity.getType()).thenReturn(entity); - } + Assert.notNull(entity, "Entity must not be null"); - protected Mono maybeCallBeforeConvert(T object, SqlIdentifier table) { + RelationalPersistentEntity persistentEntity = getRequiredEntity(entity); - if (entityCallbacks != null) { - return entityCallbacks.callback(BeforeConvertCallback.class, object, table); - } + return delete(getByIdQuery(entity, persistentEntity), persistentEntity.getType()).thenReturn(entity); + } - return Mono.just(object); - } + protected Mono maybeCallBeforeConvert(T object, SqlIdentifier table) { - protected Mono maybeCallBeforeSave(T object, OutboundRow row, SqlIdentifier table) { + if (entityCallbacks != null) { + return entityCallbacks.callback(BeforeConvertCallback.class, object, table); + } - if (entityCallbacks != null) { - return entityCallbacks.callback(BeforeSaveCallback.class, object, row, table); - } + return Mono.just(object); + } - return Mono.just(object); - } + protected Mono maybeCallBeforeSave(T object, OutboundRow row, SqlIdentifier table) { - protected Mono maybeCallAfterSave(T object, OutboundRow row, SqlIdentifier table) { + if (entityCallbacks != null) { + return entityCallbacks.callback(BeforeSaveCallback.class, object, row, table); + } - if (entityCallbacks != null) { - return entityCallbacks.callback(AfterSaveCallback.class, object, row, table); - } + return Mono.just(object); + } - return Mono.just(object); - } + protected Mono maybeCallAfterSave(T object, OutboundRow row, SqlIdentifier table) { - protected Mono maybeCallAfterConvert(T object, SqlIdentifier table) { + if (entityCallbacks != null) { + return entityCallbacks.callback(AfterSaveCallback.class, object, row, table); + } - if (entityCallbacks != null) { - return entityCallbacks.callback(AfterConvertCallback.class, object, table); - } + return Mono.just(object); + } - return Mono.just(object); - } + protected Mono maybeCallAfterConvert(T object, SqlIdentifier table) { - private Query getByIdQuery(T entity, RelationalPersistentEntity persistentEntity) { + if (entityCallbacks != null) { + return entityCallbacks.callback(AfterConvertCallback.class, object, table); + } - if (!persistentEntity.hasIdProperty()) { - throw new MappingException("No id property found for object of type " + persistentEntity.getType()); - } + return Mono.just(object); + } - IdentifierAccessor identifierAccessor = persistentEntity.getIdentifierAccessor(entity); - Object id = identifierAccessor.getRequiredIdentifier(); + private Query getByIdQuery(T entity, RelationalPersistentEntity persistentEntity) { - return Query.query(Criteria.where(persistentEntity.getRequiredIdProperty().getName()).is(id)); - } + if (!persistentEntity.hasIdProperty()) { + throw new MappingException("No id property found for object of type " + persistentEntity.getType()); + } - SqlIdentifier getTableName(Class entityClass) { - return getRequiredEntity(entityClass).getTableName(); - } + IdentifierAccessor identifierAccessor = persistentEntity.getIdentifierAccessor(entity); + Object id = identifierAccessor.getRequiredIdentifier(); - SqlIdentifier getTableNameOrEmpty(Class entityClass) { + return Query.query(Criteria.where(persistentEntity.getRequiredIdProperty().getName()).is(id)); + } - RelationalPersistentEntity entity = this.mappingContext.getPersistentEntity(entityClass); + SqlIdentifier getTableName(Class entityClass) { + return getRequiredEntity(entityClass).getTableName(); + } - return entity != null ? entity.getTableName() : SqlIdentifier.EMPTY; - } + SqlIdentifier getTableNameOrEmpty(Class entityClass) { - private RelationalPersistentEntity getRequiredEntity(Class entityClass) { - return this.mappingContext.getRequiredPersistentEntity(entityClass); - } + RelationalPersistentEntity entity = this.mappingContext.getPersistentEntity(entityClass); - private RelationalPersistentEntity getRequiredEntity(T entity) { - Class entityType = ProxyUtils.getUserClass(entity); - return (RelationalPersistentEntity) getRequiredEntity(entityType); - } + return entity != null ? entity.getTableName() : SqlIdentifier.EMPTY; + } - private List getSelectProjection(Table table, Query query, Class returnType) { + private RelationalPersistentEntity getRequiredEntity(Class entityClass) { + return this.mappingContext.getRequiredPersistentEntity(entityClass); + } - if (query.getColumns().isEmpty()) { + private RelationalPersistentEntity getRequiredEntity(T entity) { + Class entityType = ProxyUtils.getUserClass(entity); + return (RelationalPersistentEntity) getRequiredEntity(entityType); + } - if (returnType.isInterface()) { + private List getSelectProjection(Table table, Query query, Class returnType) { - ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(returnType); + if (query.getColumns().isEmpty()) { - if (projectionInformation.isClosed()) { - return projectionInformation.getInputProperties().stream().map(FeatureDescriptor::getName).map(table::column) - .collect(Collectors.toList()); - } - } + if (returnType.isInterface()) { - return Collections.singletonList(table.asterisk()); - } + ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(returnType); - return query.getColumns().stream().map(table::column).collect(Collectors.toList()); - } + if (projectionInformation.isClosed()) { + return projectionInformation.getInputProperties().stream().map(FeatureDescriptor::getName).map(table::column) + .collect(Collectors.toList()); + } + } - private RowsFetchSpec getRowsFetchSpec(DatabaseClient.GenericExecuteSpec executeSpec, Class entityClass, - Class returnType) { + return Collections.singletonList(table.asterisk()); + } - boolean simpleType; + return query.getColumns().stream().map(table::column).collect(Collectors.toList()); + } - BiFunction rowMapper; - if (returnType.isInterface()) { - simpleType = getConverter().isSimpleType(entityClass); - rowMapper = dataAccessStrategy.getRowMapper(entityClass) - .andThen(o -> projectionFactory.createProjection(returnType, o)); - } else { - simpleType = getConverter().isSimpleType(returnType); - rowMapper = dataAccessStrategy.getRowMapper(returnType); - } + private RowsFetchSpec getRowsFetchSpec(DatabaseClient.GenericExecuteSpec executeSpec, Class entityClass, + Class returnType) { - // avoid top-level null values if the read type is a simple one (e.g. SELECT MAX(age) via Integer.class) - if (simpleType) { - return new UnwrapOptionalFetchSpecAdapter<>( - executeSpec.map((row, metadata) -> Optional.ofNullable(rowMapper.apply(row, metadata)))); - } + boolean simpleType; - return executeSpec.map(rowMapper); - } + BiFunction rowMapper; + if (returnType.isInterface()) { + simpleType = getConverter().isSimpleType(entityClass); + rowMapper = dataAccessStrategy.getRowMapper(entityClass) + .andThen(o -> projectionFactory.createProjection(returnType, o)); + } else { + simpleType = getConverter().isSimpleType(returnType); + rowMapper = dataAccessStrategy.getRowMapper(returnType); + } - /** - * {@link RowsFetchSpec} adapter emitting values from {@link Optional} if they exist. - * - * @param - */ - private static class UnwrapOptionalFetchSpecAdapter implements RowsFetchSpec { + // avoid top-level null values if the read type is a simple one (e.g. SELECT MAX(age) via Integer.class) + if (simpleType) { + return new UnwrapOptionalFetchSpecAdapter<>( + executeSpec.map((row, metadata) -> Optional.ofNullable(rowMapper.apply(row, metadata)))); + } - private final RowsFetchSpec> delegate; + return executeSpec.map(rowMapper); + } - private UnwrapOptionalFetchSpecAdapter(RowsFetchSpec> delegate) { - this.delegate = delegate; - } + /** + * {@link RowsFetchSpec} adapter emitting values from {@link Optional} if they exist. + * + * @param + */ + private static class UnwrapOptionalFetchSpecAdapter implements RowsFetchSpec { - @Override - public Mono one() { - return delegate.one().handle((optional, sink) -> optional.ifPresent(sink::next)); - } + private final RowsFetchSpec> delegate; - @Override - public Mono first() { - return delegate.first().handle((optional, sink) -> optional.ifPresent(sink::next)); - } + private UnwrapOptionalFetchSpecAdapter(RowsFetchSpec> delegate) { + this.delegate = delegate; + } - @Override - public Flux all() { - return delegate.all().handle((optional, sink) -> optional.ifPresent(sink::next)); - } - } - - /** - * {@link RowsFetchSpec} adapter applying {@link #maybeCallAfterConvert(Object, SqlIdentifier)} to each emitted - * object. - * - * @param - */ - private class EntityCallbackAdapter implements RowsFetchSpec { - - private final RowsFetchSpec delegate; - private final SqlIdentifier tableName; - - private EntityCallbackAdapter(RowsFetchSpec delegate, SqlIdentifier tableName) { - this.delegate = delegate; - this.tableName = tableName; - } - - @Override - public Mono one() { - return delegate.one().flatMap(it -> maybeCallAfterConvert(it, tableName)); - } - - @Override - public Mono first() { - return delegate.first().flatMap(it -> maybeCallAfterConvert(it, tableName)); - } - - @Override - public Flux all() { - return delegate.all().concatMap(it -> maybeCallAfterConvert(it, tableName)); - } - } + @Override + public Mono one() { + return delegate.one().handle((optional, sink) -> optional.ifPresent(sink::next)); + } + + @Override + public Mono first() { + return delegate.first().handle((optional, sink) -> optional.ifPresent(sink::next)); + } + + @Override + public Flux all() { + return delegate.all().handle((optional, sink) -> optional.ifPresent(sink::next)); + } + } + + /** + * {@link RowsFetchSpec} adapter applying {@link #maybeCallAfterConvert(Object, SqlIdentifier)} to each emitted + * object. + * + * @param + */ + private class EntityCallbackAdapter implements RowsFetchSpec { + + private final RowsFetchSpec delegate; + private final SqlIdentifier tableName; + + private EntityCallbackAdapter(RowsFetchSpec delegate, SqlIdentifier tableName) { + this.delegate = delegate; + this.tableName = tableName; + } + + @Override + public Mono one() { + return delegate.one().flatMap(it -> maybeCallAfterConvert(it, tableName)); + } + + @Override + public Mono first() { + return delegate.first().flatMap(it -> maybeCallAfterConvert(it, tableName)); + } + + @Override + public Flux all() { + return delegate.all().concatMap(it -> maybeCallAfterConvert(it, tableName)); + } + } } diff --git a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplateUnitTests.java b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplateUnitTests.java index b9c0a12a06..fc2402d932 100644 --- a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplateUnitTests.java +++ b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplateUnitTests.java @@ -25,6 +25,7 @@ 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,574 +70,659 @@ * @author Mark Paluch * @author Jose Luis Leon * @author Robert Heim + * @author Jens Schauder */ public class R2dbcEntityTemplateUnitTests { - private org.springframework.r2dbc.core.DatabaseClient client; - private R2dbcEntityTemplate entityTemplate; - private StatementRecorder recorder; + private org.springframework.r2dbc.core.DatabaseClient client; + private R2dbcEntityTemplate entityTemplate; + private StatementRecorder recorder; - @BeforeEach - void before() { + @BeforeEach + void before() { - recorder = StatementRecorder.newInstance(); - client = DatabaseClient.builder().connectionFactory(recorder) - .bindMarkers(PostgresDialect.INSTANCE.getBindMarkersFactory()).build(); - entityTemplate = new R2dbcEntityTemplate(client, PostgresDialect.INSTANCE); - } + recorder = StatementRecorder.newInstance(); + client = DatabaseClient.builder().connectionFactory(recorder) + .bindMarkers(PostgresDialect.INSTANCE.getBindMarkersFactory()).build(); + entityTemplate = new R2dbcEntityTemplate(client, PostgresDialect.INSTANCE); + } - @Test // gh-220 - void shouldCountBy() { + @Test + // gh-220 + void shouldCountBy() { - MockRowMetadata metadata = MockRowMetadata.builder() - .columnMetadata(MockColumnMetadata.builder().name("name").type(R2dbcType.VARCHAR).build()).build(); - MockResult result = MockResult.builder().row(MockRow.builder().identified(0, Long.class, 1L).build()).build(); + MockRowMetadata metadata = MockRowMetadata.builder() + .columnMetadata(MockColumnMetadata.builder().name("name").type(R2dbcType.VARCHAR).build()).build(); + MockResult result = MockResult.builder().row(MockRow.builder().identified(0, Long.class, 1L).build()).build(); - recorder.addStubbing(s -> s.startsWith("SELECT"), result); + recorder.addStubbing(s -> s.startsWith("SELECT"), result); - entityTemplate.count(Query.query(Criteria.where("name").is("Walter")), Person.class) // - .as(StepVerifier::create) // - .expectNext(1L) // - .verifyComplete(); + entityTemplate.count(Query.query(Criteria.where("name").is("Walter")), Person.class) // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); - StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("SELECT")); + StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("SELECT")); - assertThat(statement.getSql()).isEqualTo("SELECT COUNT(person.id) FROM person WHERE person.THE_NAME = $1"); - assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter")); - } + assertThat(statement.getSql()).isEqualTo("SELECT COUNT(person.id) FROM person WHERE person.THE_NAME = $1"); + assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter")); + } - @Test // gh-469 - void shouldProjectExistsResult() { + @Test + // gh-469 + void shouldProjectExistsResult() { - MockRowMetadata metadata = MockRowMetadata.builder() - .columnMetadata(MockColumnMetadata.builder().name("name").type(R2dbcType.VARCHAR).build()).build(); - MockResult result = MockResult.builder().row(MockRow.builder().identified(0, Object.class, null).build()).build(); + MockRowMetadata metadata = MockRowMetadata.builder() + .columnMetadata(MockColumnMetadata.builder().name("name").type(R2dbcType.VARCHAR).build()).build(); + MockResult result = MockResult.builder().row(MockRow.builder().identified(0, Object.class, null).build()).build(); - recorder.addStubbing(s -> s.startsWith("SELECT"), result); + recorder.addStubbing(s -> s.startsWith("SELECT"), result); - entityTemplate.select(Person.class) // - .as(Integer.class) // - .matching(Query.empty().columns("MAX(age)")) // - .all() // - .as(StepVerifier::create) // - .verifyComplete(); - } + entityTemplate.select(Person.class) // + .as(Integer.class) // + .matching(Query.empty().columns("MAX(age)")) // + .all() // + .as(StepVerifier::create) // + .verifyComplete(); + } - @Test // gh-1310 - void shouldProjectExistsResultWithoutId() { + @Test + // gh-1310 + 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(); - recorder.addStubbing(s -> s.startsWith("SELECT 1"), result); + recorder.addStubbing(s -> s.startsWith("SELECT 1"), result); - entityTemplate.select(WithoutId.class).exists() // - .as(StepVerifier::create) // - .expectNext(true).verifyComplete(); - } + entityTemplate.select(WithoutId.class).exists() // + .as(StepVerifier::create) // + .expectNext(true).verifyComplete(); + } - @Test // gh-1310 - void shouldProjectCountResultWithoutId() { + @Test + // gh-1310 + 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(); - recorder.addStubbing(s -> s.startsWith("SELECT COUNT(1)"), result); + recorder.addStubbing(s -> s.startsWith("SELECT COUNT(1)"), result); - entityTemplate.select(WithoutId.class).count() // - .as(StepVerifier::create) // - .expectNext(1L).verifyComplete(); - } + entityTemplate.select(WithoutId.class).count() // + .as(StepVerifier::create) // + .expectNext(1L).verifyComplete(); + } - @Test // gh-469 - void shouldExistsByCriteria() { + @Test + // gh-469 + void shouldExistsByCriteria() { - MockRowMetadata metadata = MockRowMetadata.builder() - .columnMetadata(MockColumnMetadata.builder().name("name").type(R2dbcType.VARCHAR).build()).build(); - MockResult result = MockResult.builder().row(MockRow.builder().identified(0, Long.class, 1L).build()).build(); + MockRowMetadata metadata = MockRowMetadata.builder() + .columnMetadata(MockColumnMetadata.builder().name("name").type(R2dbcType.VARCHAR).build()).build(); + MockResult result = MockResult.builder().row(MockRow.builder().identified(0, Long.class, 1L).build()).build(); - recorder.addStubbing(s -> s.startsWith("SELECT"), result); + recorder.addStubbing(s -> s.startsWith("SELECT"), result); - entityTemplate.exists(Query.query(Criteria.where("name").is("Walter")), Person.class) // - .as(StepVerifier::create) // - .expectNext(true) // - .verifyComplete(); + entityTemplate.exists(Query.query(Criteria.where("name").is("Walter")), Person.class) // + .as(StepVerifier::create) // + .expectNext(true) // + .verifyComplete(); - StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("SELECT")); + StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("SELECT")); - assertThat(statement.getSql()).isEqualTo("SELECT person.id FROM person WHERE person.THE_NAME = $1 LIMIT 1"); - assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter")); - } + assertThat(statement.getSql()).isEqualTo("SELECT person.id FROM person WHERE person.THE_NAME = $1 LIMIT 1"); + assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter")); + } - @Test // gh-220 - void shouldSelectByCriteria() { + @Test + // gh-220 + void shouldSelectByCriteria() { - recorder.addStubbing(s -> s.startsWith("SELECT"), Collections.emptyList()); + recorder.addStubbing(s -> s.startsWith("SELECT"), Collections.emptyList()); - entityTemplate.select(Query.query(Criteria.where("name").is("Walter")).sort(Sort.by("name")), Person.class) // - .as(StepVerifier::create) // - .verifyComplete(); + entityTemplate.select(Query.query(Criteria.where("name").is("Walter")).sort(Sort.by("name")), Person.class) // + .as(StepVerifier::create) // + .verifyComplete(); - StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("SELECT")); + StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("SELECT")); - assertThat(statement.getSql()) - .isEqualTo("SELECT person.* FROM person WHERE person.THE_NAME = $1 ORDER BY person.THE_NAME ASC"); - assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter")); - } + assertThat(statement.getSql()) + .isEqualTo("SELECT person.* FROM person WHERE person.THE_NAME = $1 ORDER BY person.THE_NAME ASC"); + assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter")); + } - @Test // gh-215 - void selectShouldInvokeCallback() { + @Test + // gh-215 + void selectShouldInvokeCallback() { - MockRowMetadata metadata = MockRowMetadata.builder() - .columnMetadata(MockColumnMetadata.builder().name("id").type(R2dbcType.INTEGER).build()) - .columnMetadata(MockColumnMetadata.builder().name("THE_NAME").type(R2dbcType.VARCHAR).build()).build(); - MockResult result = MockResult.builder().row(MockRow.builder().identified("id", Object.class, "Walter") - .identified("THE_NAME", Object.class, "some-name").metadata(metadata).build()).build(); + MockRowMetadata metadata = MockRowMetadata.builder() + .columnMetadata(MockColumnMetadata.builder().name("id").type(R2dbcType.INTEGER).build()) + .columnMetadata(MockColumnMetadata.builder().name("THE_NAME").type(R2dbcType.VARCHAR).build()).build(); + MockResult result = MockResult.builder().row(MockRow.builder().identified("id", Object.class, "Walter") + .identified("THE_NAME", Object.class, "some-name").metadata(metadata).build()).build(); - recorder.addStubbing(s -> s.startsWith("SELECT"), result); + recorder.addStubbing(s -> s.startsWith("SELECT"), result); - ValueCapturingAfterConvertCallback callback = new ValueCapturingAfterConvertCallback(); + ValueCapturingAfterConvertCallback callback = new ValueCapturingAfterConvertCallback(); - entityTemplate.setEntityCallbacks(ReactiveEntityCallbacks.create(callback)); + entityTemplate.setEntityCallbacks(ReactiveEntityCallbacks.create(callback)); - entityTemplate.select(Query.empty(), Person.class) // - .as(StepVerifier::create) // - .consumeNextWith(actual -> { + entityTemplate.select(Query.empty(), Person.class) // + .as(StepVerifier::create) // + .consumeNextWith(actual -> { - assertThat(actual.id).isEqualTo("after-convert"); - assertThat(actual.name).isEqualTo("some-name"); - }).verifyComplete(); + assertThat(actual.id).isEqualTo("after-convert"); + assertThat(actual.name).isEqualTo("some-name"); + }).verifyComplete(); - assertThat(callback.getValues()).hasSize(1); - } + assertThat(callback.getValues()).hasSize(1); + } - @Test // gh-220 - void shouldSelectOne() { + @Test + // gh-220 + void shouldSelectOne() { - recorder.addStubbing(s -> s.startsWith("SELECT"), Collections.emptyList()); + recorder.addStubbing(s -> s.startsWith("SELECT"), Collections.emptyList()); - entityTemplate.selectOne(Query.query(Criteria.where("name").is("Walter")).sort(Sort.by("name")), Person.class) // - .as(StepVerifier::create) // - .verifyComplete(); + entityTemplate.selectOne(Query.query(Criteria.where("name").is("Walter")).sort(Sort.by("name")), Person.class) // + .as(StepVerifier::create) // + .verifyComplete(); - StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("SELECT")); + StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("SELECT")); - assertThat(statement.getSql()) - .isEqualTo("SELECT person.* FROM person WHERE person.THE_NAME = $1 ORDER BY person.THE_NAME ASC LIMIT 2"); - assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter")); - } + assertThat(statement.getSql()) + .isEqualTo("SELECT person.* FROM person WHERE person.THE_NAME = $1 ORDER BY person.THE_NAME ASC LIMIT 2"); + assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter")); + } - @Test // gh-220, gh-758 - void shouldSelectOneDoNotOverrideExistingLimit() { + @Test + // gh-220, gh-758 + void shouldSelectOneDoNotOverrideExistingLimit() { - recorder.addStubbing(s -> s.startsWith("SELECT"), Collections.emptyList()); + recorder.addStubbing(s -> s.startsWith("SELECT"), Collections.emptyList()); - entityTemplate - .selectOne(Query.query(Criteria.where("name").is("Walter")).sort(Sort.by("name")).limit(1), Person.class) // - .as(StepVerifier::create) // - .verifyComplete(); + entityTemplate + .selectOne(Query.query(Criteria.where("name").is("Walter")).sort(Sort.by("name")).limit(1), Person.class) // + .as(StepVerifier::create) // + .verifyComplete(); - StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("SELECT")); + StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("SELECT")); - assertThat(statement.getSql()) - .isEqualTo("SELECT person.* FROM person WHERE person.THE_NAME = $1 ORDER BY person.THE_NAME ASC LIMIT 1"); - assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter")); - } + assertThat(statement.getSql()) + .isEqualTo("SELECT person.* FROM person WHERE person.THE_NAME = $1 ORDER BY person.THE_NAME ASC LIMIT 1"); + assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter")); + } - @Test // gh-220 - void shouldUpdateByQuery() { + @Test + // gh-220 + void shouldUpdateByQuery() { - MockRowMetadata metadata = MockRowMetadata.builder() - .columnMetadata(MockColumnMetadata.builder().name("name").type(R2dbcType.VARCHAR).build()).build(); - MockResult result = MockResult.builder().rowsUpdated(1).build(); + MockRowMetadata metadata = MockRowMetadata.builder() + .columnMetadata(MockColumnMetadata.builder().name("name").type(R2dbcType.VARCHAR).build()).build(); + MockResult result = MockResult.builder().rowsUpdated(1).build(); - recorder.addStubbing(s -> s.startsWith("UPDATE"), result); + recorder.addStubbing(s -> s.startsWith("UPDATE"), result); - entityTemplate - .update(Query.query(Criteria.where("name").is("Walter")), Update.update("name", "Heisenberg"), Person.class) // - .as(StepVerifier::create) // - .expectNext(1L) // - .verifyComplete(); + entityTemplate + .update(Query.query(Criteria.where("name").is("Walter")), Update.update("name", "Heisenberg"), Person.class) // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); - StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("UPDATE")); + StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("UPDATE")); - assertThat(statement.getSql()).isEqualTo("UPDATE person SET THE_NAME = $1 WHERE person.THE_NAME = $2"); - assertThat(statement.getBindings()).hasSize(2).containsEntry(0, Parameter.from("Heisenberg")).containsEntry(1, - Parameter.from("Walter")); - } + assertThat(statement.getSql()).isEqualTo("UPDATE person SET THE_NAME = $1 WHERE person.THE_NAME = $2"); + assertThat(statement.getBindings()).hasSize(2).containsEntry(0, Parameter.from("Heisenberg")).containsEntry(1, + Parameter.from("Walter")); + } - @Test // gh-220 - void shouldDeleteByQuery() { + @Test + // gh-220 + void shouldDeleteByQuery() { - MockRowMetadata metadata = MockRowMetadata.builder() - .columnMetadata(MockColumnMetadata.builder().name("name").type(R2dbcType.VARCHAR).build()).build(); - MockResult result = MockResult.builder().rowsUpdated(1).build(); + MockRowMetadata metadata = MockRowMetadata.builder() + .columnMetadata(MockColumnMetadata.builder().name("name").type(R2dbcType.VARCHAR).build()).build(); + MockResult result = MockResult.builder().rowsUpdated(1).build(); - recorder.addStubbing(s -> s.startsWith("DELETE"), result); + recorder.addStubbing(s -> s.startsWith("DELETE"), result); - entityTemplate.delete(Query.query(Criteria.where("name").is("Walter")), Person.class) // - .as(StepVerifier::create) // - .expectNext(1L) // - .verifyComplete(); + entityTemplate.delete(Query.query(Criteria.where("name").is("Walter")), Person.class) // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); - StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("DELETE")); + StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("DELETE")); - assertThat(statement.getSql()).isEqualTo("DELETE FROM person WHERE person.THE_NAME = $1"); - assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter")); - } + assertThat(statement.getSql()).isEqualTo("DELETE FROM person WHERE person.THE_NAME = $1"); + assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter")); + } - @Test // gh-220 - void shouldDeleteEntity() { + @Test + // gh-220 + void shouldDeleteEntity() { - Person person = Person.empty() // - .withId("Walter"); - recorder.addStubbing(s -> s.startsWith("DELETE"), Collections.emptyList()); + Person person = Person.empty() // + .withId("Walter"); + recorder.addStubbing(s -> s.startsWith("DELETE"), Collections.emptyList()); - entityTemplate.delete(person) // - .as(StepVerifier::create) // - .expectNext(person).verifyComplete(); + entityTemplate.delete(person) // + .as(StepVerifier::create) // + .expectNext(person).verifyComplete(); - StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("DELETE")); + StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("DELETE")); - assertThat(statement.getSql()).isEqualTo("DELETE FROM person WHERE person.id = $1"); - assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter")); - } + assertThat(statement.getSql()).isEqualTo("DELETE FROM person WHERE person.id = $1"); + assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("Walter")); + } - @Test // gh-365 - void shouldInsertVersioned() { + @Test + // gh-365 + void shouldInsertVersioned() { - MockRowMetadata metadata = MockRowMetadata.builder().build(); - MockResult result = MockResult.builder().rowMetadata(metadata).rowsUpdated(1).build(); + MockRowMetadata metadata = MockRowMetadata.builder().build(); + MockResult result = MockResult.builder().rowMetadata(metadata).rowsUpdated(1).build(); - recorder.addStubbing(s -> s.startsWith("INSERT"), result); + recorder.addStubbing(s -> s.startsWith("INSERT"), result); - entityTemplate.insert(new VersionedPerson("id", 0, "bar")).as(StepVerifier::create) // - .assertNext(actual -> { - assertThat(actual.getVersion()).isEqualTo(1); - }) // - .verifyComplete(); + entityTemplate.insert(new VersionedPerson("id", 0, "bar")).as(StepVerifier::create) // + .assertNext(actual -> { + assertThat(actual.getVersion()).isEqualTo(1); + }) // + .verifyComplete(); - StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("INSERT")); + StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("INSERT")); - assertThat(statement.getSql()).isEqualTo("INSERT INTO versioned_person (id, version, name) VALUES ($1, $2, $3)"); - assertThat(statement.getBindings()).hasSize(3).containsEntry(0, Parameter.from("id")).containsEntry(1, - Parameter.from(1L)); - } + assertThat(statement.getSql()).isEqualTo("INSERT INTO versioned_person (id, version, name) VALUES ($1, $2, $3)"); + assertThat(statement.getBindings()).hasSize(3).containsEntry(0, Parameter.from("id")).containsEntry(1, + Parameter.from(1L)); + } - @Test // gh-557, gh-402 - void shouldSkipDefaultIdValueOnInsert() { + @Test + // gh-557, gh-402 + void shouldSkipDefaultIdValueOnInsert() { - MockRowMetadata metadata = MockRowMetadata.builder().build(); - MockResult result = MockResult.builder().rowMetadata(metadata).rowsUpdated(1).build(); + MockRowMetadata metadata = MockRowMetadata.builder().build(); + MockResult result = MockResult.builder().rowMetadata(metadata).rowsUpdated(1).build(); - recorder.addStubbing(s -> s.startsWith("INSERT"), result); + recorder.addStubbing(s -> s.startsWith("INSERT"), result); - entityTemplate.insert(new PersonWithPrimitiveId(0, "bar")).as(StepVerifier::create) // - .expectNextCount(1) // - .verifyComplete(); + entityTemplate.insert(new PersonWithPrimitiveId(0, "bar")).as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); - StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("INSERT")); + StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("INSERT")); - assertThat(statement.getSql()).isEqualTo("INSERT INTO person_with_primitive_id (name) VALUES ($1)"); - assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("bar")); - } + assertThat(statement.getSql()).isEqualTo("INSERT INTO person_with_primitive_id (name) VALUES ($1)"); + assertThat(statement.getBindings()).hasSize(1).containsEntry(0, Parameter.from("bar")); + } - @Test // gh-557, gh-402 - void shouldSkipDefaultIdValueOnVersionedInsert() { + @Test + // gh-557, gh-402 + void shouldSkipDefaultIdValueOnVersionedInsert() { - MockRowMetadata metadata = MockRowMetadata.builder().build(); - MockResult result = MockResult.builder().rowMetadata(metadata).rowsUpdated(1).build(); + MockRowMetadata metadata = MockRowMetadata.builder().build(); + MockResult result = MockResult.builder().rowMetadata(metadata).rowsUpdated(1).build(); - recorder.addStubbing(s -> s.startsWith("INSERT"), result); + recorder.addStubbing(s -> s.startsWith("INSERT"), result); - entityTemplate.insert(new VersionedPersonWithPrimitiveId(0, 0, "bar")).as(StepVerifier::create) // - .assertNext(actual -> { - assertThat(actual.getVersion()).isEqualTo(1); - }) // - .verifyComplete(); + entityTemplate.insert(new VersionedPersonWithPrimitiveId(0, 0, "bar")).as(StepVerifier::create) // + .assertNext(actual -> { + assertThat(actual.getVersion()).isEqualTo(1); + }) // + .verifyComplete(); - StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("INSERT")); + StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("INSERT")); - assertThat(statement.getSql()) - .isEqualTo("INSERT INTO versioned_person_with_primitive_id (version, name) VALUES ($1, $2)"); - assertThat(statement.getBindings()).hasSize(2).containsEntry(0, Parameter.from(1L)).containsEntry(1, - Parameter.from("bar")); - } + assertThat(statement.getSql()) + .isEqualTo("INSERT INTO versioned_person_with_primitive_id (version, name) VALUES ($1, $2)"); + assertThat(statement.getBindings()).hasSize(2).containsEntry(0, Parameter.from(1L)).containsEntry(1, + Parameter.from("bar")); + } - @Test // gh-451 - void shouldInsertCorrectlyVersionedAndAudited() { + @Test + // gh-451 + void shouldInsertCorrectlyVersionedAndAudited() { - MockRowMetadata metadata = MockRowMetadata.builder().build(); - MockResult result = MockResult.builder().rowMetadata(metadata).rowsUpdated(1).build(); + MockRowMetadata metadata = MockRowMetadata.builder().build(); + MockResult result = MockResult.builder().rowMetadata(metadata).rowsUpdated(1).build(); - recorder.addStubbing(s -> s.startsWith("INSERT"), result); + recorder.addStubbing(s -> s.startsWith("INSERT"), result); - ObjectFactory objectFactory = mock(ObjectFactory.class); - when(objectFactory.getObject()).thenReturn(new ReactiveIsNewAwareAuditingHandler( - PersistentEntities.of(entityTemplate.getConverter().getMappingContext()))); + ObjectFactory objectFactory = mock(ObjectFactory.class); + when(objectFactory.getObject()).thenReturn(new ReactiveIsNewAwareAuditingHandler( + PersistentEntities.of(entityTemplate.getConverter().getMappingContext()))); - entityTemplate - .setEntityCallbacks(ReactiveEntityCallbacks.create(new ReactiveAuditingEntityCallback(objectFactory))); - entityTemplate.insert(new WithAuditingAndOptimisticLocking(null, 0, "Walter", null, null)) // - .as(StepVerifier::create) // - .assertNext(actual -> { - assertThat(actual.getVersion()).isEqualTo(1); - assertThat(actual.getCreatedDate()).isNotNull(); - }) // - .verifyComplete(); + entityTemplate + .setEntityCallbacks(ReactiveEntityCallbacks.create(new ReactiveAuditingEntityCallback(objectFactory))); + entityTemplate.insert(new WithAuditingAndOptimisticLocking(null, 0, "Walter", null, null)) // + .as(StepVerifier::create) // + .assertNext(actual -> { + assertThat(actual.getVersion()).isEqualTo(1); + assertThat(actual.getCreatedDate()).isNotNull(); + }) // + .verifyComplete(); - StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("INSERT")); + StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("INSERT")); - assertThat(statement.getSql()).isEqualTo( - "INSERT INTO with_auditing_and_optimistic_locking (version, name, created_date, last_modified_date) VALUES ($1, $2, $3, $4)"); - } + assertThat(statement.getSql()).isEqualTo( + "INSERT INTO with_auditing_and_optimistic_locking (version, name, created_date, last_modified_date) VALUES ($1, $2, $3, $4)"); + } - @Test // gh-451 - void shouldUpdateCorrectlyVersionedAndAudited() { + @Test + // gh-451 + void shouldUpdateCorrectlyVersionedAndAudited() { - MockRowMetadata metadata = MockRowMetadata.builder().build(); - MockResult result = MockResult.builder().rowMetadata(metadata).rowsUpdated(1).build(); + MockRowMetadata metadata = MockRowMetadata.builder().build(); + MockResult result = MockResult.builder().rowMetadata(metadata).rowsUpdated(1).build(); - recorder.addStubbing(s -> s.startsWith("UPDATE"), result); + recorder.addStubbing(s -> s.startsWith("UPDATE"), result); - ObjectFactory objectFactory = mock(ObjectFactory.class); - when(objectFactory.getObject()).thenReturn(new ReactiveIsNewAwareAuditingHandler( - PersistentEntities.of(entityTemplate.getConverter().getMappingContext()))); + ObjectFactory objectFactory = mock(ObjectFactory.class); + when(objectFactory.getObject()).thenReturn(new ReactiveIsNewAwareAuditingHandler( + PersistentEntities.of(entityTemplate.getConverter().getMappingContext()))); - entityTemplate - .setEntityCallbacks(ReactiveEntityCallbacks.create(new ReactiveAuditingEntityCallback(objectFactory))); - entityTemplate.update(new WithAuditingAndOptimisticLocking(null, 2, "Walter", null, null)) // - .as(StepVerifier::create) // - .assertNext(actual -> { - assertThat(actual.getVersion()).isEqualTo(3); - assertThat(actual.getCreatedDate()).isNull(); - assertThat(actual.getLastModifiedDate()).isNotNull(); - }) // - .verifyComplete(); + entityTemplate + .setEntityCallbacks(ReactiveEntityCallbacks.create(new ReactiveAuditingEntityCallback(objectFactory))); + entityTemplate.update(new WithAuditingAndOptimisticLocking(null, 2, "Walter", null, null)) // + .as(StepVerifier::create) // + .assertNext(actual -> { + assertThat(actual.getVersion()).isEqualTo(3); + assertThat(actual.getCreatedDate()).isNull(); + assertThat(actual.getLastModifiedDate()).isNotNull(); + }) // + .verifyComplete(); - StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("UPDATE")); + StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("UPDATE")); - assertThat(statement.getSql()).startsWith( - "UPDATE with_auditing_and_optimistic_locking SET version = $1, name = $2, created_date = $3, last_modified_date = $4"); - } + assertThat(statement.getSql()).startsWith( + "UPDATE with_auditing_and_optimistic_locking SET version = $1, name = $2, created_date = $3, last_modified_date = $4"); + } - @Test // gh-215 - void insertShouldInvokeCallback() { + @Test + // gh-215 + void insertShouldInvokeCallback() { - MockRowMetadata metadata = MockRowMetadata.builder().build(); - MockResult result = MockResult.builder().rowMetadata(metadata).rowsUpdated(1).build(); + MockRowMetadata metadata = MockRowMetadata.builder().build(); + MockResult result = MockResult.builder().rowMetadata(metadata).rowsUpdated(1).build(); - recorder.addStubbing(s -> s.startsWith("INSERT"), result); + recorder.addStubbing(s -> s.startsWith("INSERT"), result); - ValueCapturingBeforeConvertCallback beforeConvert = new ValueCapturingBeforeConvertCallback(); - ValueCapturingBeforeSaveCallback beforeSave = new ValueCapturingBeforeSaveCallback(); - ValueCapturingAfterSaveCallback afterSave = new ValueCapturingAfterSaveCallback(); + ValueCapturingBeforeConvertCallback beforeConvert = new ValueCapturingBeforeConvertCallback(); + ValueCapturingBeforeSaveCallback beforeSave = new ValueCapturingBeforeSaveCallback(); + ValueCapturingAfterSaveCallback afterSave = new ValueCapturingAfterSaveCallback(); - entityTemplate.setEntityCallbacks(ReactiveEntityCallbacks.create(beforeConvert, beforeSave, afterSave)); - entityTemplate.insert(Person.empty()).as(StepVerifier::create) // - .assertNext(actual -> { - assertThat(actual.id).isEqualTo("after-save"); - assertThat(actual.name).isEqualTo("before-convert"); - assertThat(actual.description).isNull(); - }) // - .verifyComplete(); + entityTemplate.setEntityCallbacks(ReactiveEntityCallbacks.create(beforeConvert, beforeSave, afterSave)); + entityTemplate.insert(Person.empty()).as(StepVerifier::create) // + .assertNext(actual -> { + assertThat(actual.id).isEqualTo("after-save"); + assertThat(actual.name).isEqualTo("before-convert"); + assertThat(actual.description).isNull(); + }) // + .verifyComplete(); - StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("INSERT")); + StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("INSERT")); - assertThat(statement.getSql()).isEqualTo("INSERT INTO person (THE_NAME, description) VALUES ($1, $2)"); - assertThat(statement.getBindings()).hasSize(2).containsEntry(0, Parameter.from("before-convert")).containsEntry(1, - Parameter.from("before-save")); - } + assertThat(statement.getSql()).isEqualTo("INSERT INTO person (THE_NAME, description) VALUES ($1, $2)"); + assertThat(statement.getBindings()).hasSize(2).containsEntry(0, Parameter.from("before-convert")).containsEntry(1, + Parameter.from("before-save")); + } - @Test // gh-365 - void shouldUpdateVersioned() { + @Test + // gh-365 + void shouldUpdateVersioned() { - MockRowMetadata metadata = MockRowMetadata.builder().build(); - MockResult result = MockResult.builder().rowMetadata(metadata).rowsUpdated(1).build(); + MockRowMetadata metadata = MockRowMetadata.builder().build(); + MockResult result = MockResult.builder().rowMetadata(metadata).rowsUpdated(1).build(); - recorder.addStubbing(s -> s.startsWith("UPDATE"), result); + recorder.addStubbing(s -> s.startsWith("UPDATE"), result); - entityTemplate.update(new VersionedPerson("id", 1, "bar")).as(StepVerifier::create) // - .assertNext(actual -> { - assertThat(actual.getVersion()).isEqualTo(2); - }) // - .verifyComplete(); + entityTemplate.update(new VersionedPerson("id", 1, "bar")).as(StepVerifier::create) // + .assertNext(actual -> { + assertThat(actual.getVersion()).isEqualTo(2); + }) // + .verifyComplete(); - StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("UPDATE")); + StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("UPDATE")); - assertThat(statement.getSql()).isEqualTo( - "UPDATE versioned_person SET version = $1, name = $2 WHERE versioned_person.id = $3 AND (versioned_person.version = $4)"); - assertThat(statement.getBindings()).hasSize(4).containsEntry(0, Parameter.from(2L)).containsEntry(3, - Parameter.from(1L)); - } + assertThat(statement.getSql()).isEqualTo( + "UPDATE versioned_person SET version = $1, name = $2 WHERE versioned_person.id = $3 AND (versioned_person.version = $4)"); + assertThat(statement.getBindings()).hasSize(4).containsEntry(0, Parameter.from(2L)).containsEntry(3, + Parameter.from(1L)); + } - @Test // gh-215 - void updateShouldInvokeCallback() { + @Test + // gh-215 + void updateShouldInvokeCallback() { - MockRowMetadata metadata = MockRowMetadata.builder().build(); - MockResult result = MockResult.builder().rowMetadata(metadata).rowsUpdated(1).build(); + MockRowMetadata metadata = MockRowMetadata.builder().build(); + MockResult result = MockResult.builder().rowMetadata(metadata).rowsUpdated(1).build(); - recorder.addStubbing(s -> s.startsWith("UPDATE"), result); + recorder.addStubbing(s -> s.startsWith("UPDATE"), result); - ValueCapturingBeforeConvertCallback beforeConvert = new ValueCapturingBeforeConvertCallback(); - ValueCapturingBeforeSaveCallback beforeSave = new ValueCapturingBeforeSaveCallback(); - ValueCapturingAfterSaveCallback afterSave = new ValueCapturingAfterSaveCallback(); + ValueCapturingBeforeConvertCallback beforeConvert = new ValueCapturingBeforeConvertCallback(); + ValueCapturingBeforeSaveCallback beforeSave = new ValueCapturingBeforeSaveCallback(); + ValueCapturingAfterSaveCallback afterSave = new ValueCapturingAfterSaveCallback(); - Person person = Person.empty() // - .withId("the-id") // - .withName("name") // - .withDescription("description"); + Person person = Person.empty() // + .withId("the-id") // + .withName("name") // + .withDescription("description"); - entityTemplate.setEntityCallbacks(ReactiveEntityCallbacks.create(beforeConvert, beforeSave, afterSave)); - entityTemplate.update(person).as(StepVerifier::create) // - .assertNext(actual -> { - assertThat(actual.id).isEqualTo("after-save"); - assertThat(actual.name).isEqualTo("before-convert"); - assertThat(actual.description).isNull(); - }) // - .verifyComplete(); + entityTemplate.setEntityCallbacks(ReactiveEntityCallbacks.create(beforeConvert, beforeSave, afterSave)); + entityTemplate.update(person).as(StepVerifier::create) // + .assertNext(actual -> { + assertThat(actual.id).isEqualTo("after-save"); + assertThat(actual.name).isEqualTo("before-convert"); + assertThat(actual.description).isNull(); + }) // + .verifyComplete(); - StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("UPDATE")); + StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("UPDATE")); - assertThat(statement.getSql()).isEqualTo("UPDATE person SET THE_NAME = $1, description = $2 WHERE person.id = $3"); - assertThat(statement.getBindings()).hasSize(3).containsEntry(0, Parameter.from("before-convert")).containsEntry(1, - Parameter.from("before-save")); - } + assertThat(statement.getSql()).isEqualTo("UPDATE person SET THE_NAME = $1, description = $2 WHERE person.id = $3"); + assertThat(statement.getBindings()).hasSize(3).containsEntry(0, Parameter.from("before-convert")).containsEntry(1, + Parameter.from("before-save")); + } - @Value - static class WithoutId { + @Test + // gh-637 + void insertIncludesInsertOnlyColumns() { - String name; - } + MockRowMetadata metadata = MockRowMetadata.builder().build(); + MockResult result = MockResult.builder().rowMetadata(metadata).rowsUpdated(1).build(); - @Value - @With - static class Person { + recorder.addStubbing(s -> s.startsWith("INSERT"), result); - @Id String id; + entityTemplate.insert(new WithInsertOnly(null, "Alfred", "insert this")).as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); - @Column("THE_NAME") String name; + StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("INSERT")); - String description; + 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")); + } - public static Person empty() { - return new Person(null, null, null); - } - } + @Test + // gh-637 + void updateExcludesInsertOnlyColumns() { - @Value - @With - private static class VersionedPerson { + MockRowMetadata metadata = MockRowMetadata.builder().build(); + MockResult result = MockResult.builder().rowMetadata(metadata).rowsUpdated(1).build(); - @Id String id; + recorder.addStubbing(s -> s.startsWith("UPDATE"), result); - @Version long version; + entityTemplate.update(new WithInsertOnly(23L, "Alfred", "don't update this")).as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); - String name; - } + StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(s -> s.startsWith("UPDATE")); - @Value - @With - private static class PersonWithPrimitiveId { + 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)); + } - @Id int id; + @Value + static class WithoutId { - String name; - } + String name; + } - @Value - @With - private static class VersionedPersonWithPrimitiveId { + @Value + @With + static class Person { - @Id int id; + @Id + String id; - @Version long version; + @Column("THE_NAME") + String name; - String name; - } + String description; - @Value - @With - private static class WithAuditingAndOptimisticLocking { + public static Person empty() { + return new Person(null, null, null); + } + } - @Id String id; + @Value + @With + private static class VersionedPerson { - @Version long version; + @Id + String id; - String name; + @Version + long version; - @CreatedDate LocalDateTime createdDate; - @LastModifiedDate LocalDateTime lastModifiedDate; - } + String name; + } - static class ValueCapturingEntityCallback { + @Value + @With + private static class PersonWithPrimitiveId { - private final List values = new ArrayList<>(1); + @Id + int id; - void capture(T value) { - values.add(value); - } + String name; + } - public List getValues() { - return values; - } + @Value + @With + private static class VersionedPersonWithPrimitiveId { - @Nullable - public T getValue() { - return CollectionUtils.lastElement(values); - } - } + @Id + int id; - static class ValueCapturingBeforeConvertCallback extends ValueCapturingEntityCallback - implements BeforeConvertCallback { + @Version + long version; - @Override - public Mono onBeforeConvert(Person entity, SqlIdentifier table) { + String name; + } - capture(entity); - Person person = entity.withName("before-convert"); - return Mono.just(person); - } - } + @Value + @With + private static class WithAuditingAndOptimisticLocking { - static class ValueCapturingBeforeSaveCallback extends ValueCapturingEntityCallback - implements BeforeSaveCallback { + @Id + String id; - @Override - public Mono onBeforeSave(Person entity, OutboundRow outboundRow, SqlIdentifier table) { + @Version + long version; - capture(entity); - outboundRow.put(SqlIdentifier.unquoted("description"), Parameter.from("before-save")); - return Mono.just(entity); - } - } + String name; - static class ValueCapturingAfterSaveCallback extends ValueCapturingEntityCallback - implements AfterSaveCallback { + @CreatedDate + LocalDateTime createdDate; + @LastModifiedDate + LocalDateTime lastModifiedDate; + } - @Override - public Mono onAfterSave(Person entity, OutboundRow outboundRow, SqlIdentifier table) { + @Value + private static class WithInsertOnly { + @Id + Long id; - capture(entity); + String name; - Person person = Person.empty() // - .withId("after-save") // - .withName(entity.getName()); + @InsertOnlyProperty + String insertOnly; + } - return Mono.just(person); - } - } + static class ValueCapturingEntityCallback { - static class ValueCapturingAfterConvertCallback extends ValueCapturingEntityCallback - implements AfterConvertCallback { + private final List values = new ArrayList<>(1); - @Override - public Mono onAfterConvert(Person entity, SqlIdentifier table) { + void capture(T value) { + values.add(value); + } - capture(entity); - Person person = Person.empty() // - .withId("after-convert") // - .withName(entity.getName()); + public List getValues() { + return values; + } - return Mono.just(person); - } - } + @Nullable + public T getValue() { + return CollectionUtils.lastElement(values); + } + } + + static class ValueCapturingBeforeConvertCallback extends ValueCapturingEntityCallback + implements BeforeConvertCallback { + + @Override + public Mono onBeforeConvert(Person entity, SqlIdentifier table) { + + capture(entity); + Person person = entity.withName("before-convert"); + return Mono.just(person); + } + } + + static class ValueCapturingBeforeSaveCallback extends ValueCapturingEntityCallback + implements BeforeSaveCallback { + + @Override + public Mono onBeforeSave(Person entity, OutboundRow outboundRow, SqlIdentifier table) { + + capture(entity); + outboundRow.put(SqlIdentifier.unquoted("description"), Parameter.from("before-save")); + return Mono.just(entity); + } + } + + static class ValueCapturingAfterSaveCallback extends ValueCapturingEntityCallback + implements AfterSaveCallback { + + @Override + public Mono onAfterSave(Person entity, OutboundRow outboundRow, SqlIdentifier table) { + + capture(entity); + + Person person = Person.empty() // + .withId("after-save") // + .withName(entity.getName()); + + return Mono.just(person); + } + } + + static class ValueCapturingAfterConvertCallback extends ValueCapturingEntityCallback + implements AfterConvertCallback { + + @Override + public Mono onAfterConvert(Person entity, SqlIdentifier table) { + + capture(entity); + Person person = Person.empty() // + .withId("after-convert") // + .withName(entity.getName()); + + return Mono.just(person); + } + } } From 3be9494ae77ec0405e6b9558d55167c97436d331 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 20 Sep 2022 15:10:22 +0200 Subject: [PATCH 4/4] Fix sql scripts --- .../JdbcAggregateTemplateIntegrationTests-db2.sql | 8 ++++++++ .../JdbcAggregateTemplateIntegrationTests-h2.sql | 6 ++++++ .../JdbcAggregateTemplateIntegrationTests-mariadb.sql | 6 ++++++ .../JdbcAggregateTemplateIntegrationTests-mssql.sql | 8 ++++++++ .../JdbcAggregateTemplateIntegrationTests-mysql.sql | 6 ++++++ .../JdbcAggregateTemplateIntegrationTests-oracle.sql | 8 ++++++++ .../JdbcAggregateTemplateIntegrationTests-postgres.sql | 7 +++++++ 7 files changed, 49 insertions(+) diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql index 1d58605c7a..8ad4fda2dc 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql +++ b/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_INSERT_ONLY; + CREATE TABLE LEGO_SET ( "id1" BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, @@ -357,4 +359,10 @@ CREATE TABLE WITH_LOCAL_DATE_TIME 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) ); \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql index 73e85c0d8f..a0aff08ce8 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql @@ -326,4 +326,10 @@ CREATE TABLE WITH_LOCAL_DATE_TIME CREATE TABLE WITH_ID_ONLY ( ID SERIAL PRIMARY KEY +); + +CREATE TABLE WITH_INSERT_ONLY +( + ID SERIAL PRIMARY KEY, + INSERT_ONLY VARCHAR(100) ); \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql index 8d72641aa7..4dd82b9003 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql @@ -301,4 +301,10 @@ CREATE TABLE WITH_LOCAL_DATE_TIME 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) ); \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql index e6352bf257..880528cdbf 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql @@ -331,4 +331,12 @@ DROP TABLE IF EXISTS WITH_ID_ONLY; 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) ); \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql index 9720136459..6808c8a912 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql @@ -306,4 +306,10 @@ CREATE TABLE WITH_LOCAL_DATE_TIME 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) ); \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql index cf36153817..084e5db460 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql +++ b/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_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 ( @@ -338,4 +339,11 @@ CREATE TABLE WITH_LOCAL_DATE_TIME 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) ); \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql index 620c75f46b..ed1fb9662d 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql +++ b/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 WITH_READ_ONLY; DROP TABLE WITH_ID_ONLY; +DROP TABLE WITH_INSERT_ONLY; CREATE TABLE LEGO_SET ( @@ -341,4 +342,10 @@ CREATE TABLE WITH_LOCAL_DATE_TIME CREATE TABLE WITH_ID_ONLY ( ID SERIAL PRIMARY KEY +); + +CREATE TABLE WITH_INSERT_ONLY +( + ID SERIAL PRIMARY KEY, + INSERT_ONLY VARCHAR(100) ); \ No newline at end of file