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 ff7329a11..4a82a7f7d 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 @@ -60,1046 +60,1052 @@ import org.springframework.util.Assert; */ class SqlGenerator { - static final SqlIdentifier VERSION_SQL_PARAMETER = SqlIdentifier.unquoted("___oldOptimisticLockingVersion"); - static final SqlIdentifier ID_SQL_PARAMETER = SqlIdentifier.unquoted("id"); - static final SqlIdentifier IDS_SQL_PARAMETER = SqlIdentifier.unquoted("ids"); - static final SqlIdentifier ROOT_ID_PARAMETER = SqlIdentifier.unquoted("rootId"); - - private static final Pattern parameterPattern = Pattern.compile("\\W"); - private final RelationalPersistentEntity entity; - private final MappingContext, RelationalPersistentProperty> mappingContext; - private final RenderContext renderContext; - - private final SqlContext sqlContext; - private final SqlRenderer sqlRenderer; - private final Columns columns; - - private final Lazy findOneSql = Lazy.of(this::createFindOneSql); - private final Lazy findAllSql = Lazy.of(this::createFindAllSql); - private final Lazy findAllInListSql = Lazy.of(this::createFindAllInListSql); - - private final Lazy existsSql = Lazy.of(this::createExistsSql); - private final Lazy countSql = Lazy.of(this::createCountSql); - - private final Lazy updateSql = Lazy.of(this::createUpdateSql); - private final Lazy updateWithVersionSql = Lazy.of(this::createUpdateWithVersionSql); - - private final Lazy deleteByIdSql = Lazy.of(this::createDeleteByIdSql); - private final Lazy deleteByIdInSql = Lazy.of(this::createDeleteByIdInSql); - private final Lazy deleteByIdAndVersionSql = Lazy.of(this::createDeleteByIdAndVersionSql); - private final Lazy deleteByListSql = Lazy.of(this::createDeleteByListSql); - private final QueryMapper queryMapper; - private final Dialect dialect; - - /** - * Create a new {@link SqlGenerator} given {@link RelationalMappingContext} and {@link RelationalPersistentEntity}. - * - * @param mappingContext must not be {@literal null}. - * @param converter must not be {@literal null}. - * @param entity must not be {@literal null}. - * @param dialect must not be {@literal null}. - */ - SqlGenerator(RelationalMappingContext mappingContext, JdbcConverter converter, RelationalPersistentEntity entity, - Dialect dialect) { - - this.mappingContext = mappingContext; - this.entity = entity; - this.sqlContext = new SqlContext(entity); - this.renderContext = new RenderContextFactory(dialect).createRenderContext(); - this.sqlRenderer = SqlRenderer.create(renderContext); - this.columns = new Columns(entity, mappingContext, converter); - this.queryMapper = new QueryMapper(dialect, converter); - this.dialect = dialect; - } - - /** - * Construct an IN-condition based on a {@link Select Sub-Select} which selects the ids (or stand-ins for ids) of the - * given {@literal path} to those that reference the root entities specified by the {@literal rootCondition}. - * - * @param path specifies the table and id to select - * @param rootCondition the condition on the root of the path determining what to select - * @param filterColumn the column to apply the IN-condition to. - * @return the IN condition - */ - private Condition getSubselectCondition(PersistentPropertyPathExtension path, - Function rootCondition, Column filterColumn) { - - PersistentPropertyPathExtension parentPath = path.getParentPath(); - - if (!parentPath.hasIdProperty()) { - if (parentPath.getLength() > 1) { - return getSubselectCondition(parentPath, rootCondition, filterColumn); - } - return rootCondition.apply(filterColumn); - } - - Table subSelectTable = Table.create(parentPath.getTableName()); - Column idColumn = subSelectTable.column(parentPath.getIdColumnName()); - Column selectFilterColumn = subSelectTable.column(parentPath.getEffectiveIdColumnName()); - - Condition innerCondition; - - if (parentPath.getLength() == 1) { // if the parent is the root of the path - - // apply the rootCondition - innerCondition = rootCondition.apply(selectFilterColumn); - } else { - - // otherwise, we need another layer of subselect - innerCondition = getSubselectCondition(parentPath, rootCondition, selectFilterColumn); - } - - Select select = Select.builder() // - .select(idColumn) // - .from(subSelectTable) // - .where(innerCondition).build(); - - return filterColumn.in(select); - } - - private BindMarker getBindMarker(SqlIdentifier columnName) { - return SQL.bindMarker(":" + parameterPattern.matcher(renderReference(columnName)).replaceAll("")); - } - - /** - * Returns a query for selecting all simple properties of an entity, including those for one-to-one relationships. - * Results are filtered using an {@code IN}-clause on the id column. - * - * @return a SQL statement. Guaranteed to be not {@code null}. - */ - String getFindAllInList() { - return findAllInListSql.get(); - } - - /** - * Returns a query for selecting all simple properties of an entity, including those for one-to-one relationships. - * - * @return a SQL statement. Guaranteed to be not {@code null}. - */ - String getFindAll() { - return findAllSql.get(); - } - - /** - * Returns a query for selecting all simple properties of an entity, including those for one-to-one relationships, - * sorted by the given parameter. - * - * @return a SQL statement. Guaranteed to be not {@code null}. - */ - String getFindAll(Sort sort) { - return render(selectBuilder(Collections.emptyList(), sort, Pageable.unpaged()).build()); - } - - /** - * Returns a query for selecting all simple properties of an entity, including those for one-to-one relationships, - * paged and sorted by the given parameter. - * - * @return a SQL statement. Guaranteed to be not {@code null}. - */ - String getFindAll(Pageable pageable) { - return render(selectBuilder(Collections.emptyList(), pageable.getSort(), pageable).build()); - } - - /** - * Returns a query for selecting all simple properties of an entity, including those for one-to-one relationships. - * Results are limited to those rows referencing some other entity using the column specified by - * {@literal columnName}. This is used to select values for a complex property ({@link Set}, {@link Map} ...) based on - * a referencing entity. - * - * @param parentIdentifier name of the column of the FK back to the referencing entity. - * @param keyColumn if the property is of type {@link Map} this column contains the map key. - * @param ordered whether the SQL statement should include an ORDER BY for the keyColumn. If this is {@code true}, the - * keyColumn must not be {@code null}. - * @return a SQL String. - */ - String getFindAllByProperty(Identifier parentIdentifier, @Nullable SqlIdentifier keyColumn, boolean ordered) { - - Assert.isTrue(keyColumn != null || !ordered, - "If the SQL statement should be ordered a keyColumn to order by must be provided"); - - Table table = getTable(); - - SelectBuilder.SelectWhere builder = selectBuilder( // - keyColumn == null // - ? Collections.emptyList() // - : Collections.singleton(keyColumn) // - ); - - Condition condition = buildConditionForBackReference(parentIdentifier, table); - SelectBuilder.SelectWhereAndOr withWhereClause = builder.where(condition); - - Select select = ordered // - ? withWhereClause.orderBy(table.column(keyColumn).as(keyColumn)).build() // - : withWhereClause.build(); - - return render(select); - } - - private Condition buildConditionForBackReference(Identifier parentIdentifier, Table table) { - - Condition condition = null; - for (SqlIdentifier backReferenceColumn : parentIdentifier.toMap().keySet()) { - - Condition newCondition = table.column(backReferenceColumn).isEqualTo(getBindMarker(backReferenceColumn)); - condition = condition == null ? newCondition : condition.and(newCondition); - } - - Assert.state(condition != null, "We need at least one condition"); - - return condition; - } - - /** - * Create a {@code SELECT COUNT(id) FROM … WHERE :id = …} statement. - * - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getExists() { - return existsSql.get(); - } - - /** - * Create a {@code SELECT … FROM … WHERE :id = …} statement. - * - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getFindOne() { - return findOneSql.get(); - } - - /** - * Create a {@code SELECT count(id) FROM … WHERE :id = … (LOCK CLAUSE)} statement. - * - * @param lockMode Lock clause mode. - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getAcquireLockById(LockMode lockMode) { - return this.createAcquireLockById(lockMode); - } - - /** - * Create a {@code SELECT count(id) FROM … (LOCK CLAUSE)} statement. - * - * @param lockMode Lock clause mode. - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getAcquireLockAll(LockMode lockMode) { - return this.createAcquireLockAll(lockMode); - } - - /** - * Create a {@code INSERT INTO … (…) VALUES(…)} statement. - * - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getInsert(Set additionalColumns) { - return createInsertSql(additionalColumns); - } - - /** - * Create a {@code UPDATE … SET …} statement. - * - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getUpdate() { - return updateSql.get(); - } - - /** - * Create a {@code UPDATE … SET … WHERE ID = :id and VERSION_COLUMN = :___oldOptimisticLockingVersion } statement. - * - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getUpdateWithVersion() { - return updateWithVersionSql.get(); - } - - /** - * Create a {@code SELECT COUNT(*) FROM …} statement. - * - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getCount() { - return countSql.get(); - } - - /** - * Create a {@code DELETE FROM … WHERE :id = …} statement. - * - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getDeleteById() { - return deleteByIdSql.get(); - } - - /** - * Create a {@code DELETE FROM … WHERE :id IN …} statement. - * - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getDeleteByIdIn() { - return deleteByIdInSql.get(); - } - - /** - * Create a {@code DELETE FROM … WHERE :id = … and :___oldOptimisticLockingVersion = ...} statement. - * - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getDeleteByIdAndVersion() { - return deleteByIdAndVersionSql.get(); - } - - /** - * Create a {@code DELETE FROM … WHERE :ids in (…)} statement. - * - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String getDeleteByList() { - return deleteByListSql.get(); - } - - /** - * Create a {@code DELETE} query and optionally filter by {@link PersistentPropertyPath}. - * - * @param path can be {@literal null}. - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String createDeleteAllSql(@Nullable PersistentPropertyPath path) { - - Table table = getTable(); - - DeleteBuilder.DeleteWhere deleteAll = Delete.builder().from(table); - - if (path == null) { - return render(deleteAll.build()); - } - - return createDeleteByPathAndCriteria(new PersistentPropertyPathExtension(mappingContext, path), Column::isNotNull); - } - - /** - * Create a {@code DELETE} query and filter by {@link PersistentPropertyPath} using {@code WHERE} with the {@code =} - * operator. - * - * @param path must not be {@literal null}. - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String createDeleteByPath(PersistentPropertyPath path) { - return createDeleteByPathAndCriteria(new PersistentPropertyPathExtension(mappingContext, path), - filterColumn -> filterColumn.isEqualTo(getBindMarker(ROOT_ID_PARAMETER))); - } - - /** - * Create a {@code DELETE} query and filter by {@link PersistentPropertyPath} using {@code WHERE} with the {@code IN} - * operator. - * - * @param path must not be {@literal null}. - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - String createDeleteInByPath(PersistentPropertyPath path) { - - return createDeleteByPathAndCriteria(new PersistentPropertyPathExtension(mappingContext, path), - filterColumn -> filterColumn.in(getBindMarker(IDS_SQL_PARAMETER))); - } - - private String createFindOneSql() { - - Select select = selectBuilder().where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // - .build(); - - return render(select); - } - - private String createAcquireLockById(LockMode lockMode) { - - Table table = this.getTable(); - - Select select = StatementBuilder // - .select(getIdColumn()) // - .from(table) // - .where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // - .lock(lockMode) // - .build(); - - return render(select); - } - - private String createAcquireLockAll(LockMode lockMode) { - - Table table = this.getTable(); - - Select select = StatementBuilder // - .select(getIdColumn()) // - .from(table) // - .lock(lockMode) // - .build(); - - return render(select); - } - - private String createFindAllSql() { - return render(selectBuilder().build()); - } - - private SelectBuilder.SelectWhere selectBuilder() { - return selectBuilder(Collections.emptyList()); - } - - private SelectBuilder.SelectWhere selectBuilder(Collection keyColumns) { + static final SqlIdentifier VERSION_SQL_PARAMETER = SqlIdentifier.unquoted("___oldOptimisticLockingVersion"); + static final SqlIdentifier ID_SQL_PARAMETER = SqlIdentifier.unquoted("id"); + static final SqlIdentifier IDS_SQL_PARAMETER = SqlIdentifier.unquoted("ids"); + static final SqlIdentifier ROOT_ID_PARAMETER = SqlIdentifier.unquoted("rootId"); + + private static final Pattern parameterPattern = Pattern.compile("\\W"); + private final RelationalPersistentEntity entity; + private final MappingContext, RelationalPersistentProperty> mappingContext; + private final RenderContext renderContext; + + private final SqlContext sqlContext; + private final SqlRenderer sqlRenderer; + private final Columns columns; + + private final Lazy findOneSql = Lazy.of(this::createFindOneSql); + private final Lazy findAllSql = Lazy.of(this::createFindAllSql); + private final Lazy findAllInListSql = Lazy.of(this::createFindAllInListSql); + + private final Lazy existsSql = Lazy.of(this::createExistsSql); + private final Lazy countSql = Lazy.of(this::createCountSql); + + private final Lazy updateSql = Lazy.of(this::createUpdateSql); + private final Lazy updateWithVersionSql = Lazy.of(this::createUpdateWithVersionSql); + + private final Lazy deleteByIdSql = Lazy.of(this::createDeleteByIdSql); + private final Lazy deleteByIdInSql = Lazy.of(this::createDeleteByIdInSql); + private final Lazy deleteByIdAndVersionSql = Lazy.of(this::createDeleteByIdAndVersionSql); + private final Lazy deleteByListSql = Lazy.of(this::createDeleteByListSql); + private final QueryMapper queryMapper; + private final Dialect dialect; + + /** + * Create a new {@link SqlGenerator} given {@link RelationalMappingContext} and {@link RelationalPersistentEntity}. + * + * @param mappingContext must not be {@literal null}. + * @param converter must not be {@literal null}. + * @param entity must not be {@literal null}. + * @param dialect must not be {@literal null}. + */ + SqlGenerator(RelationalMappingContext mappingContext, JdbcConverter converter, RelationalPersistentEntity entity, + Dialect dialect) { + + this.mappingContext = mappingContext; + this.entity = entity; + this.sqlContext = new SqlContext(entity); + this.renderContext = new RenderContextFactory(dialect).createRenderContext(); + this.sqlRenderer = SqlRenderer.create(renderContext); + this.columns = new Columns(entity, mappingContext, converter); + this.queryMapper = new QueryMapper(dialect, converter); + this.dialect = dialect; + } + + /** + * Construct an IN-condition based on a {@link Select Sub-Select} which selects the ids (or stand-ins for ids) of the + * given {@literal path} to those that reference the root entities specified by the {@literal rootCondition}. + * + * @param path specifies the table and id to select + * @param rootCondition the condition on the root of the path determining what to select + * @param filterColumn the column to apply the IN-condition to. + * @return the IN condition + */ + private Condition getSubselectCondition(PersistentPropertyPathExtension path, + Function rootCondition, Column filterColumn) { + + PersistentPropertyPathExtension parentPath = path.getParentPath(); + + if (!parentPath.hasIdProperty()) { + if (parentPath.getLength() > 1) { + return getSubselectCondition(parentPath, rootCondition, filterColumn); + } + return rootCondition.apply(filterColumn); + } + + Table subSelectTable = Table.create(parentPath.getTableName()); + Column idColumn = subSelectTable.column(parentPath.getIdColumnName()); + Column selectFilterColumn = subSelectTable.column(parentPath.getEffectiveIdColumnName()); + + Condition innerCondition; + + if (parentPath.getLength() == 1) { // if the parent is the root of the path + + // apply the rootCondition + innerCondition = rootCondition.apply(selectFilterColumn); + } else { + + // otherwise, we need another layer of subselect + innerCondition = getSubselectCondition(parentPath, rootCondition, selectFilterColumn); + } + + Select select = Select.builder() // + .select(idColumn) // + .from(subSelectTable) // + .where(innerCondition).build(); + + return filterColumn.in(select); + } + + private BindMarker getBindMarker(SqlIdentifier columnName) { + return SQL.bindMarker(":" + parameterPattern.matcher(renderReference(columnName)).replaceAll("")); + } + + /** + * Returns a query for selecting all simple properties of an entity, including those for one-to-one relationships. + * Results are filtered using an {@code IN}-clause on the id column. + * + * @return a SQL statement. Guaranteed to be not {@code null}. + */ + String getFindAllInList() { + return findAllInListSql.get(); + } + + /** + * Returns a query for selecting all simple properties of an entity, including those for one-to-one relationships. + * + * @return a SQL statement. Guaranteed to be not {@code null}. + */ + String getFindAll() { + return findAllSql.get(); + } + + /** + * Returns a query for selecting all simple properties of an entity, including those for one-to-one relationships, + * sorted by the given parameter. + * + * @return a SQL statement. Guaranteed to be not {@code null}. + */ + String getFindAll(Sort sort) { + return render(selectBuilder(Collections.emptyList(), sort, Pageable.unpaged()).build()); + } + + /** + * Returns a query for selecting all simple properties of an entity, including those for one-to-one relationships, + * paged and sorted by the given parameter. + * + * @return a SQL statement. Guaranteed to be not {@code null}. + */ + String getFindAll(Pageable pageable) { + return render(selectBuilder(Collections.emptyList(), pageable.getSort(), pageable).build()); + } + + /** + * Returns a query for selecting all simple properties of an entity, including those for one-to-one relationships. + * Results are limited to those rows referencing some other entity using the column specified by + * {@literal columnName}. This is used to select values for a complex property ({@link Set}, {@link Map} ...) based on + * a referencing entity. + * + * @param parentIdentifier name of the column of the FK back to the referencing entity. + * @param keyColumn if the property is of type {@link Map} this column contains the map key. + * @param ordered whether the SQL statement should include an ORDER BY for the keyColumn. If this is {@code true}, the + * keyColumn must not be {@code null}. + * @return a SQL String. + */ + String getFindAllByProperty(Identifier parentIdentifier, @Nullable SqlIdentifier keyColumn, boolean ordered) { + + Assert.isTrue(keyColumn != null || !ordered, + "If the SQL statement should be ordered a keyColumn to order by must be provided"); + + Table table = getTable(); + + SelectBuilder.SelectWhere builder = selectBuilder( // + keyColumn == null // + ? Collections.emptyList() // + : Collections.singleton(keyColumn) // + ); + + Condition condition = buildConditionForBackReference(parentIdentifier, table); + SelectBuilder.SelectWhereAndOr withWhereClause = builder.where(condition); + + Select select = ordered // + ? withWhereClause.orderBy(table.column(keyColumn).as(keyColumn)).build() // + : withWhereClause.build(); + + return render(select); + } + + private Condition buildConditionForBackReference(Identifier parentIdentifier, Table table) { + + Condition condition = null; + for (SqlIdentifier backReferenceColumn : parentIdentifier.toMap().keySet()) { + + Condition newCondition = table.column(backReferenceColumn).isEqualTo(getBindMarker(backReferenceColumn)); + condition = condition == null ? newCondition : condition.and(newCondition); + } + + Assert.state(condition != null, "We need at least one condition"); + + return condition; + } + + /** + * Create a {@code SELECT COUNT(id) FROM … WHERE :id = …} statement. + * + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getExists() { + return existsSql.get(); + } + + /** + * Create a {@code SELECT … FROM … WHERE :id = …} statement. + * + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getFindOne() { + return findOneSql.get(); + } + + /** + * Create a {@code SELECT count(id) FROM … WHERE :id = … (LOCK CLAUSE)} statement. + * + * @param lockMode Lock clause mode. + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getAcquireLockById(LockMode lockMode) { + return this.createAcquireLockById(lockMode); + } + + /** + * Create a {@code SELECT count(id) FROM … (LOCK CLAUSE)} statement. + * + * @param lockMode Lock clause mode. + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getAcquireLockAll(LockMode lockMode) { + return this.createAcquireLockAll(lockMode); + } + + /** + * Create a {@code INSERT INTO … (…) VALUES(…)} statement. + * + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getInsert(Set additionalColumns) { + return createInsertSql(additionalColumns); + } + + /** + * Create a {@code UPDATE … SET …} statement. + * + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getUpdate() { + return updateSql.get(); + } + + /** + * Create a {@code UPDATE … SET … WHERE ID = :id and VERSION_COLUMN = :___oldOptimisticLockingVersion } statement. + * + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getUpdateWithVersion() { + return updateWithVersionSql.get(); + } + + /** + * Create a {@code SELECT COUNT(*) FROM …} statement. + * + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getCount() { + return countSql.get(); + } + + /** + * Create a {@code DELETE FROM … WHERE :id = …} statement. + * + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getDeleteById() { + return deleteByIdSql.get(); + } + + /** + * Create a {@code DELETE FROM … WHERE :id IN …} statement. + * + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getDeleteByIdIn() { + return deleteByIdInSql.get(); + } + + /** + * Create a {@code DELETE FROM … WHERE :id = … and :___oldOptimisticLockingVersion = ...} statement. + * + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getDeleteByIdAndVersion() { + return deleteByIdAndVersionSql.get(); + } + + /** + * Create a {@code DELETE FROM … WHERE :ids in (…)} statement. + * + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String getDeleteByList() { + return deleteByListSql.get(); + } + + /** + * Create a {@code DELETE} query and optionally filter by {@link PersistentPropertyPath}. + * + * @param path can be {@literal null}. + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String createDeleteAllSql(@Nullable PersistentPropertyPath path) { + + Table table = getTable(); + + DeleteBuilder.DeleteWhere deleteAll = Delete.builder().from(table); + + if (path == null) { + return render(deleteAll.build()); + } + + return createDeleteByPathAndCriteria(new PersistentPropertyPathExtension(mappingContext, path), Column::isNotNull); + } + + /** + * Create a {@code DELETE} query and filter by {@link PersistentPropertyPath} using {@code WHERE} with the {@code =} + * operator. + * + * @param path must not be {@literal null}. + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String createDeleteByPath(PersistentPropertyPath path) { + return createDeleteByPathAndCriteria(new PersistentPropertyPathExtension(mappingContext, path), + filterColumn -> filterColumn.isEqualTo(getBindMarker(ROOT_ID_PARAMETER))); + } + + /** + * Create a {@code DELETE} query and filter by {@link PersistentPropertyPath} using {@code WHERE} with the {@code IN} + * operator. + * + * @param path must not be {@literal null}. + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + String createDeleteInByPath(PersistentPropertyPath path) { + + return createDeleteByPathAndCriteria(new PersistentPropertyPathExtension(mappingContext, path), + filterColumn -> filterColumn.in(getBindMarker(IDS_SQL_PARAMETER))); + } + + private String createFindOneSql() { + + Select select = selectBuilder().where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // + .build(); + + return render(select); + } + + private String createAcquireLockById(LockMode lockMode) { + + Table table = this.getTable(); + + Select select = StatementBuilder // + .select(getIdColumn()) // + .from(table) // + .where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // + .lock(lockMode) // + .build(); + + return render(select); + } + + private String createAcquireLockAll(LockMode lockMode) { + + Table table = this.getTable(); + + Select select = StatementBuilder // + .select(getIdColumn()) // + .from(table) // + .lock(lockMode) // + .build(); + + return render(select); + } + + private String createFindAllSql() { + return render(selectBuilder().build()); + } + + private SelectBuilder.SelectWhere selectBuilder() { + return selectBuilder(Collections.emptyList()); + } + + private SelectBuilder.SelectWhere selectBuilder(Collection keyColumns) { - Table table = getTable(); + Table table = getTable(); - List columnExpressions = new ArrayList<>(); + List columnExpressions = new ArrayList<>(); - List joinTables = new ArrayList<>(); - for (PersistentPropertyPath path : mappingContext - .findPersistentPropertyPaths(entity.getType(), p -> true)) { + List joinTables = new ArrayList<>(); + for (PersistentPropertyPath path : mappingContext + .findPersistentPropertyPaths(entity.getType(), p -> true)) { - PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(mappingContext, path); + PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(mappingContext, path); - // add a join if necessary - Join join = getJoin(extPath); - if (join != null) { - joinTables.add(join); - } + // add a join if necessary + Join join = getJoin(extPath); + if (join != null) { + joinTables.add(join); + } - Column column = getColumn(extPath); - if (column != null) { - columnExpressions.add(column); - } - } + Column column = getColumn(extPath); + if (column != null) { + columnExpressions.add(column); + } + } - for (SqlIdentifier keyColumn : keyColumns) { - columnExpressions.add(table.column(keyColumn).as(keyColumn)); - } + for (SqlIdentifier keyColumn : keyColumns) { + columnExpressions.add(table.column(keyColumn).as(keyColumn)); + } - SelectBuilder.SelectAndFrom selectBuilder = StatementBuilder.select(columnExpressions); - SelectBuilder.SelectJoin baseSelect = selectBuilder.from(table); + SelectBuilder.SelectAndFrom selectBuilder = StatementBuilder.select(columnExpressions); + SelectBuilder.SelectJoin baseSelect = selectBuilder.from(table); - for (Join join : joinTables) { - baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); - } + for (Join join : joinTables) { + baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); + } - return (SelectBuilder.SelectWhere) baseSelect; - } + return (SelectBuilder.SelectWhere) baseSelect; + } - private SelectBuilder.SelectOrdered selectBuilder(Collection keyColumns, Sort sort, - Pageable pageable) { + private SelectBuilder.SelectOrdered selectBuilder(Collection keyColumns, Sort sort, + Pageable pageable) { - SelectBuilder.SelectOrdered sortable = this.selectBuilder(keyColumns); - sortable = applyPagination(pageable, sortable); - return sortable.orderBy(extractOrderByFields(sort)); + SelectBuilder.SelectOrdered sortable = this.selectBuilder(keyColumns); + sortable = applyPagination(pageable, sortable); + return sortable.orderBy(extractOrderByFields(sort)); - } + } - private SelectBuilder.SelectOrdered applyPagination(Pageable pageable, SelectBuilder.SelectOrdered select) { + private SelectBuilder.SelectOrdered applyPagination(Pageable pageable, SelectBuilder.SelectOrdered select) { - if (!pageable.isPaged()) { - return select; - } + if (!pageable.isPaged()) { + return select; + } - Assert.isTrue(select instanceof SelectBuilder.SelectLimitOffset, - () -> String.format("Can't apply limit clause to statement of type %s", select.getClass())); + Assert.isTrue(select instanceof SelectBuilder.SelectLimitOffset, + () -> String.format("Can't apply limit clause to statement of type %s", select.getClass())); - SelectBuilder.SelectLimitOffset limitable = (SelectBuilder.SelectLimitOffset) select; - SelectBuilder.SelectLimitOffset limitResult = limitable.limitOffset(pageable.getPageSize(), pageable.getOffset()); + SelectBuilder.SelectLimitOffset limitable = (SelectBuilder.SelectLimitOffset) select; + SelectBuilder.SelectLimitOffset limitResult = limitable.limitOffset(pageable.getPageSize(), pageable.getOffset()); - Assert.state(limitResult instanceof SelectBuilder.SelectOrdered, String.format( - "The result of applying the limit-clause must be of type SelectOrdered in order to apply the order-by-clause but is of type %s", - select.getClass())); + Assert.state(limitResult instanceof SelectBuilder.SelectOrdered, String.format( + "The result of applying the limit-clause must be of type SelectOrdered in order to apply the order-by-clause but is of type %s", + select.getClass())); - return (SelectBuilder.SelectOrdered) limitResult; - } + return (SelectBuilder.SelectOrdered) limitResult; + } - /** - * Create a {@link Column} for {@link PersistentPropertyPathExtension}. - * - * @param path the path to the column in question. - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - @Nullable - Column getColumn(PersistentPropertyPathExtension path) { + /** + * Create a {@link Column} for {@link PersistentPropertyPathExtension}. + * + * @param path the path to the column in question. + * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. + */ + @Nullable + Column getColumn(PersistentPropertyPathExtension path) { - // an embedded itself doesn't give a column, its members will though. - // if there is a collection or map on the path it won't get selected at all, but it will get loaded with a separate - // select - // only the parent path is considered in order to handle arrays that get stored as BINARY properly - if (path.isEmbedded() || path.getParentPath().isMultiValued()) { - return null; - } + // an embedded itself doesn't give a column, its members will though. + // if there is a collection or map on the path it won't get selected at all, but it will get loaded with a separate + // select + // only the parent path is considered in order to handle arrays that get stored as BINARY properly + if (path.isEmbedded() || path.getParentPath().isMultiValued()) { + return null; + } - if (path.isEntity()) { + if (path.isEntity()) { - // Simple entities without id include there backreference as a synthetic id in order to distinguish null entities - // from entities with only null values. + // Simple entities without id include there backreference as a synthetic id in order to distinguish null entities + // from entities with only null values. - if (path.isQualified() // - || path.isCollectionLike() // - || path.hasIdProperty() // - ) { - return null; - } + if (path.isQualified() // + || path.isCollectionLike() // + || path.hasIdProperty() // + ) { + return null; + } - return sqlContext.getReverseColumn(path); - } + return sqlContext.getReverseColumn(path); + } - return sqlContext.getColumn(path); - } + return sqlContext.getColumn(path); + } - @Nullable - Join getJoin(PersistentPropertyPathExtension path) { + @Nullable + Join getJoin(PersistentPropertyPathExtension path) { - if (!path.isEntity() || path.isEmbedded() || path.isMultiValued()) { - return null; - } + if (!path.isEntity() || path.isEmbedded() || path.isMultiValued()) { + return null; + } - Table currentTable = sqlContext.getTable(path); + Table currentTable = sqlContext.getTable(path); - PersistentPropertyPathExtension idDefiningParentPath = path.getIdDefiningParentPath(); - Table parentTable = sqlContext.getTable(idDefiningParentPath); + PersistentPropertyPathExtension idDefiningParentPath = path.getIdDefiningParentPath(); + Table parentTable = sqlContext.getTable(idDefiningParentPath); - return new Join( // - currentTable, // - currentTable.column(path.getReverseColumnName()), // - parentTable.column(idDefiningParentPath.getIdColumnName()) // - ); - } + return new Join( // + currentTable, // + currentTable.column(path.getReverseColumnName()), // + parentTable.column(idDefiningParentPath.getIdColumnName()) // + ); + } - private String createFindAllInListSql() { + private String createFindAllInListSql() { - Select select = selectBuilder().where(getIdColumn().in(getBindMarker(IDS_SQL_PARAMETER))).build(); + Select select = selectBuilder().where(getIdColumn().in(getBindMarker(IDS_SQL_PARAMETER))).build(); - return render(select); - } + return render(select); + } - private String createExistsSql() { + private String createExistsSql() { - Table table = getTable(); + Table table = getTable(); - Select select = StatementBuilder // - .select(Functions.count(getIdColumn())) // - .from(table) // - .where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // - .build(); + Select select = StatementBuilder // + .select(Functions.count(getIdColumn())) // + .from(table) // + .where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // + .build(); - return render(select); - } + return render(select); + } - private String createCountSql() { + private String createCountSql() { - Table table = getTable(); + Table table = getTable(); - Select select = StatementBuilder // - .select(Functions.count(Expressions.asterisk())) // - .from(table) // - .build(); + Select select = StatementBuilder // + .select(Functions.count(Expressions.asterisk())) // + .from(table) // + .build(); - return render(select); - } + return render(select); + } - private String createInsertSql(Set additionalColumns) { + private String createInsertSql(Set additionalColumns) { - Table table = getTable(); + Table table = getTable(); - Set columnNamesForInsert = new TreeSet<>(Comparator.comparing(SqlIdentifier::getReference)); - columnNamesForInsert.addAll(columns.getInsertableColumns()); - columnNamesForInsert.addAll(additionalColumns); + Set columnNamesForInsert = new TreeSet<>(Comparator.comparing(SqlIdentifier::getReference)); + columnNamesForInsert.addAll(columns.getInsertableColumns()); + columnNamesForInsert.addAll(additionalColumns); - InsertBuilder.InsertIntoColumnsAndValuesWithBuild insert = Insert.builder().into(table); + InsertBuilder.InsertIntoColumnsAndValuesWithBuild insert = Insert.builder().into(table); - for (SqlIdentifier cn : columnNamesForInsert) { - insert = insert.column(table.column(cn)); - } + for (SqlIdentifier cn : columnNamesForInsert) { + insert = insert.column(table.column(cn)); + } - if (columnNamesForInsert.isEmpty()) { - return render(insert.build()); - } + if (columnNamesForInsert.isEmpty()) { + return render(insert.build()); + } - InsertBuilder.InsertValuesWithBuild insertWithValues = null; - for (SqlIdentifier cn : columnNamesForInsert) { - insertWithValues = (insertWithValues == null ? insert : insertWithValues).values(getBindMarker(cn)); - } + InsertBuilder.InsertValuesWithBuild insertWithValues = null; + for (SqlIdentifier cn : columnNamesForInsert) { + insertWithValues = (insertWithValues == null ? insert : insertWithValues).values(getBindMarker(cn)); + } - return render(insertWithValues.build()); - } + return render(insertWithValues.build()); + } - private String createUpdateSql() { - return render(createBaseUpdate().build()); - } + private String createUpdateSql() { + return render(createBaseUpdate().build()); + } - private String createUpdateWithVersionSql() { + private String createUpdateWithVersionSql() { - Update update = createBaseUpdate() // - .and(getVersionColumn().isEqualTo(SQL.bindMarker(":" + renderReference(VERSION_SQL_PARAMETER)))) // - .build(); + Update update = createBaseUpdate() // + .and(getVersionColumn().isEqualTo(SQL.bindMarker(":" + renderReference(VERSION_SQL_PARAMETER)))) // + .build(); - return render(update); - } + return render(update); + } - private UpdateBuilder.UpdateWhereAndOr createBaseUpdate() { + private UpdateBuilder.UpdateWhereAndOr createBaseUpdate() { - Table table = getTable(); + Table table = getTable(); - List assignments = columns.getUpdatableColumns() // - .stream() // - .map(columnName -> Assignments.value( // - table.column(columnName), // - getBindMarker(columnName))) // - .collect(Collectors.toList()); + List assignments = columns.getUpdatableColumns() // + .stream() // + .map(columnName -> Assignments.value( // + table.column(columnName), // + getBindMarker(columnName))) // + .collect(Collectors.toList()); - return Update.builder() // - .table(table) // - .set(assignments) // - .where(getIdColumn().isEqualTo(getBindMarker(entity.getIdColumn()))); - } + return Update.builder() // + .table(table) // + .set(assignments) // + .where(getIdColumn().isEqualTo(getBindMarker(entity.getIdColumn()))); + } - private String createDeleteByIdSql() { - return render(createBaseDeleteById(getTable()).build()); - } + private String createDeleteByIdSql() { + return render(createBaseDeleteById(getTable()).build()); + } - private String createDeleteByIdInSql() { - return render(createBaseDeleteByIdIn(getTable()).build()); - } + private String createDeleteByIdInSql() { + return render(createBaseDeleteByIdIn(getTable()).build()); + } - private String createDeleteByIdAndVersionSql() { + private String createDeleteByIdAndVersionSql() { - Delete delete = createBaseDeleteById(getTable()) // - .and(getVersionColumn().isEqualTo(SQL.bindMarker(":" + renderReference(VERSION_SQL_PARAMETER)))) // - .build(); + Delete delete = createBaseDeleteById(getTable()) // + .and(getVersionColumn().isEqualTo(SQL.bindMarker(":" + renderReference(VERSION_SQL_PARAMETER)))) // + .build(); - return render(delete); - } + return render(delete); + } - private DeleteBuilder.DeleteWhereAndOr createBaseDeleteById(Table table) { + private DeleteBuilder.DeleteWhereAndOr createBaseDeleteById(Table table) { - return Delete.builder().from(table) - .where(getIdColumn().isEqualTo(SQL.bindMarker(":" + renderReference(ID_SQL_PARAMETER)))); - } + return Delete.builder().from(table) + .where(getIdColumn().isEqualTo(SQL.bindMarker(":" + renderReference(ID_SQL_PARAMETER)))); + } - private DeleteBuilder.DeleteWhereAndOr createBaseDeleteByIdIn(Table table) { + private DeleteBuilder.DeleteWhereAndOr createBaseDeleteByIdIn(Table table) { - return Delete.builder().from(table) - .where(getIdColumn().in(SQL.bindMarker(":" + renderReference(IDS_SQL_PARAMETER)))); - } + return Delete.builder().from(table) + .where(getIdColumn().in(SQL.bindMarker(":" + renderReference(IDS_SQL_PARAMETER)))); + } - private String createDeleteByPathAndCriteria(PersistentPropertyPathExtension path, - Function rootCondition) { + private String createDeleteByPathAndCriteria(PersistentPropertyPathExtension path, + Function rootCondition) { - Table table = Table.create(path.getTableName()); + Table table = Table.create(path.getTableName()); - DeleteBuilder.DeleteWhere builder = Delete.builder() // - .from(table); - Delete delete; + DeleteBuilder.DeleteWhere builder = Delete.builder() // + .from(table); + Delete delete; - Column filterColumn = table.column(path.getReverseColumnName()); + Column filterColumn = table.column(path.getReverseColumnName()); - if (path.getLength() == 1) { + if (path.getLength() == 1) { - delete = builder // - .where(rootCondition.apply(filterColumn)) // - .build(); - } else { + delete = builder // + .where(rootCondition.apply(filterColumn)) // + .build(); + } else { - Condition condition = getSubselectCondition(path, rootCondition, filterColumn); - delete = builder.where(condition).build(); - } + Condition condition = getSubselectCondition(path, rootCondition, filterColumn); + delete = builder.where(condition).build(); + } - return render(delete); - } + return render(delete); + } - private String createDeleteByListSql() { + private String createDeleteByListSql() { - Table table = getTable(); + Table table = getTable(); - Delete delete = Delete.builder() // - .from(table) // - .where(getIdColumn().in(getBindMarker(IDS_SQL_PARAMETER))) // - .build(); + Delete delete = Delete.builder() // + .from(table) // + .where(getIdColumn().in(getBindMarker(IDS_SQL_PARAMETER))) // + .build(); - return render(delete); - } + return render(delete); + } - private String render(Select select) { - return this.sqlRenderer.render(select); - } + private String render(Select select) { + return this.sqlRenderer.render(select); + } - private String render(Insert insert) { - return this.sqlRenderer.render(insert); - } + private String render(Insert insert) { + return this.sqlRenderer.render(insert); + } - private String render(Update update) { - return this.sqlRenderer.render(update); - } + private String render(Update update) { + return this.sqlRenderer.render(update); + } - private String render(Delete delete) { - return this.sqlRenderer.render(delete); - } + private String render(Delete delete) { + return this.sqlRenderer.render(delete); + } - private Table getTable() { - return sqlContext.getTable(); - } + private Table getTable() { + return sqlContext.getTable(); + } - private Column getIdColumn() { - return sqlContext.getIdColumn(); - } + private Column getIdColumn() { + return sqlContext.getIdColumn(); + } - private Column getVersionColumn() { - return sqlContext.getVersionColumn(); - } + private Column getVersionColumn() { + return sqlContext.getVersionColumn(); + } - private String renderReference(SqlIdentifier identifier) { - return identifier.getReference(renderContext.getIdentifierProcessing()); - } + private String renderReference(SqlIdentifier identifier) { + return identifier.getReference(renderContext.getIdentifierProcessing()); + } - private List extractOrderByFields(Sort sort) { + private List extractOrderByFields(Sort sort) { - return sort.stream() // - .map(this::orderToOrderByField) // - .collect(Collectors.toList()); - } + return sort.stream() // + .map(this::orderToOrderByField) // + .collect(Collectors.toList()); + } - private OrderByField orderToOrderByField(Sort.Order order) { + private OrderByField orderToOrderByField(Sort.Order order) { - SqlIdentifier columnName = this.entity.getRequiredPersistentProperty(order.getProperty()).getColumnName(); - Column column = Column.create(columnName, this.getTable()); - return OrderByField.from(column, order.getDirection()).withNullHandling(order.getNullHandling()); - } + SqlIdentifier columnName = this.entity.getRequiredPersistentProperty(order.getProperty()).getColumnName(); + Column column = Column.create(columnName, this.getTable()); + return OrderByField.from(column, order.getDirection()).withNullHandling(order.getNullHandling()); + } - /** - * Constructs a single sql query that performs select based on the provided query. Additional the bindings for the - * where clause are stored after execution into the parameterSource - * - * @param query the query to base the select on. Must not be null - * @param parameterSource the source for holding the bindings - * @return a non null query string. - */ - public String selectByQuery(Query query, MapSqlParameterSource parameterSource) { + /** + * Constructs a single sql query that performs select based on the provided query. Additional the bindings for the + * where clause are stored after execution into the parameterSource + * + * @param query the query to base the select on. Must not be null + * @param parameterSource the source for holding the bindings + * @return a non null query string. + */ + public String selectByQuery(Query query, MapSqlParameterSource parameterSource) { - Assert.notNull(parameterSource, "parameterSource must not be null"); + Assert.notNull(parameterSource, "parameterSource must not be null"); - SelectBuilder.SelectWhere selectBuilder = selectBuilder(); - - Select select = applyQueryOnSelect(query, parameterSource, selectBuilder) // - .build(); - - return render(select); - } - - /** - * Constructs a single sql query that performs select based on the provided query and pagination information. - * Additional the bindings for the where clause are stored after execution into the parameterSource - * - * @param query the query to base the select on. Must not be null. - * @param pageable the pageable to perform on the select. - * @param parameterSource the source for holding the bindings. - * @return a non null query string. - */ - public String selectByQuery(Query query, MapSqlParameterSource parameterSource, Pageable pageable) { - - Assert.notNull(parameterSource, "parameterSource must not be null"); - - SelectBuilder.SelectWhere selectBuilder = selectBuilder(); - - // first apply query and then pagination. This means possible query sorting and limiting might be overwritten by the - // pagination. This is desired. - SelectBuilder.SelectOrdered selectOrdered = applyQueryOnSelect(query, parameterSource, selectBuilder); - selectOrdered = applyPagination(pageable, selectOrdered); - selectOrdered = selectOrdered.orderBy(extractOrderByFields(pageable.getSort())); - - Select select = selectOrdered.build(); - return render(select); - } - - /** - * Constructs a single sql query that performs select count based on the provided query for checking existence. - * Additional the bindings for the where clause are stored after execution into the parameterSource - * - * @param query the query to base the select on. Must not be null - * @param parameterSource the source for holding the bindings - * @return a non null query string. - */ - public String existsByQuery(Query query, MapSqlParameterSource parameterSource) { - - SelectBuilder.SelectJoin baseSelect = getExistsSelect(); - - Select select = applyQueryOnSelect(query, parameterSource, (SelectBuilder.SelectWhere) baseSelect) // - .build(); - - return render(select); - } - - /** - * Constructs a single sql query that performs select count based on the provided query. Additional the bindings for - * the where clause are stored after execution into the parameterSource - * - * @param query the query to base the select on. Must not be null - * @param parameterSource the source for holding the bindings - * @return a non null query string. - */ - public String countByQuery(Query query, MapSqlParameterSource parameterSource) { - - Expression countExpression = Expressions.just("1"); - SelectBuilder.SelectJoin baseSelect = getSelectCountWithExpression(countExpression); - - Select select = applyQueryOnSelect(query, parameterSource, (SelectBuilder.SelectWhere) baseSelect) // - .build(); - - return render(select); - } - - /** - * Generates a {@link org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin} with a - * COUNT(...) where the countExpressions are the parameters of the count. - * - * @return a non-null {@link org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin} that joins all the - * columns and has only a count in the projection of the select. - */ - private SelectBuilder.SelectJoin getExistsSelect() { - - Table table = getTable(); - - SelectBuilder.SelectJoin baseSelect = StatementBuilder // - .select(dialect.getExistsFunction()) // - .from(table); - - // add possible joins - for (PersistentPropertyPath path : mappingContext - .findPersistentPropertyPaths(entity.getType(), p -> true)) { - - PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(mappingContext, path); - - // add a join if necessary - Join join = getJoin(extPath); - if (join != null) { - baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); - } - } - return baseSelect; - } - - /** - * Generates a {@link org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin} with a - * COUNT(...) where the countExpressions are the parameters of the count. - * - * @param countExpressions the expression to use as count parameter. - * @return a non-null {@link org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin} that joins all the - * columns and has only a count in the projection of the select. - */ - private SelectBuilder.SelectJoin getSelectCountWithExpression(Expression... countExpressions) { - - Assert.notNull(countExpressions, "countExpressions must not be null"); - Assert.state(countExpressions.length >= 1, "countExpressions must contain at least one expression"); - - Table table = getTable(); - - SelectBuilder.SelectJoin baseSelect = StatementBuilder // - .select(Functions.count(countExpressions)) // - .from(table); - - // add possible joins - for (PersistentPropertyPath path : mappingContext - .findPersistentPropertyPaths(entity.getType(), p -> true)) { - - PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(mappingContext, path); - - // add a join if necessary - Join join = getJoin(extPath); - if (join != null) { - baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); - } - } - return baseSelect; - } - - private SelectBuilder.SelectOrdered applyQueryOnSelect(Query query, MapSqlParameterSource parameterSource, - SelectBuilder.SelectWhere selectBuilder) { - - Table table = Table.create(this.entity.getTableName()); - - SelectBuilder.SelectOrdered selectOrdered = query // - .getCriteria() // - .map(item -> this.applyCriteria(item, selectBuilder, parameterSource, table)) // - .orElse(selectBuilder); - - if (query.isSorted()) { - List sort = this.queryMapper.getMappedSort(table, query.getSort(), entity); - selectOrdered = selectBuilder.orderBy(sort); - } - - SelectBuilder.SelectLimitOffset limitable = (SelectBuilder.SelectLimitOffset) selectOrdered; - - if (query.getLimit() > 0) { - limitable = limitable.limit(query.getLimit()); - } - - if (query.getOffset() > 0) { - limitable = limitable.offset(query.getOffset()); - } - return (SelectBuilder.SelectOrdered) limitable; - } - - SelectBuilder.SelectOrdered applyCriteria(@Nullable CriteriaDefinition criteria, - SelectBuilder.SelectWhere whereBuilder, MapSqlParameterSource parameterSource, Table table) { - - return criteria == null || criteria.isEmpty() // Check for null and empty criteria - ? whereBuilder // - : whereBuilder.where(queryMapper.getMappedObject(parameterSource, criteria, table, entity)); - } - - /** - * Value object representing a {@code JOIN} association. - */ - static final class Join { - - private final Table joinTable; - private final Column joinColumn; - private final Column parentId; - - Join(Table joinTable, Column joinColumn, Column parentId) { - - Assert.notNull(joinTable, "JoinTable must not be null"); - Assert.notNull(joinColumn, "JoinColumn must not be null"); - Assert.notNull(parentId, "ParentId must not be null"); - - this.joinTable = joinTable; - this.joinColumn = joinColumn; - this.parentId = parentId; - } - - Table getJoinTable() { - return this.joinTable; - } - - Column getJoinColumn() { - return this.joinColumn; - } - - Column getParentId() { - return this.parentId; - } - - @Override - public boolean equals(Object o) { + SelectBuilder.SelectWhere selectBuilder = selectBuilder(); + + Select select = applyQueryOnSelect(query, parameterSource, selectBuilder) // + .build(); + + return render(select); + } + + /** + * Constructs a single sql query that performs select based on the provided query and pagination information. + * Additional the bindings for the where clause are stored after execution into the parameterSource + * + * @param query the query to base the select on. Must not be null. + * @param pageable the pageable to perform on the select. + * @param parameterSource the source for holding the bindings. + * @return a non null query string. + */ + public String selectByQuery(Query query, MapSqlParameterSource parameterSource, Pageable pageable) { + + Assert.notNull(parameterSource, "parameterSource must not be null"); + + SelectBuilder.SelectWhere selectBuilder = selectBuilder(); + + // first apply query and then pagination. This means possible query sorting and limiting might be overwritten by the + // pagination. This is desired. + SelectBuilder.SelectOrdered selectOrdered = applyQueryOnSelect(query, parameterSource, selectBuilder); + selectOrdered = applyPagination(pageable, selectOrdered); + selectOrdered = selectOrdered.orderBy(extractOrderByFields(pageable.getSort())); + + Select select = selectOrdered.build(); + return render(select); + } + + /** + * Constructs a single sql query that performs select count based on the provided query for checking existence. + * Additional the bindings for the where clause are stored after execution into the parameterSource + * + * @param query the query to base the select on. Must not be null + * @param parameterSource the source for holding the bindings + * @return a non null query string. + */ + public String existsByQuery(Query query, MapSqlParameterSource parameterSource) { + + SelectBuilder.SelectJoin baseSelect = getExistsSelect(); + + Select select = applyQueryOnSelect(query, parameterSource, (SelectBuilder.SelectWhere) baseSelect) // + .build(); + + return render(select); + } + + /** + * Constructs a single sql query that performs select count based on the provided query. Additional the bindings for + * the where clause are stored after execution into the parameterSource + * + * @param query the query to base the select on. Must not be null + * @param parameterSource the source for holding the bindings + * @return a non null query string. + */ + public String countByQuery(Query query, MapSqlParameterSource parameterSource) { + + Expression countExpression = Expressions.just("1"); + SelectBuilder.SelectJoin baseSelect = getSelectCountWithExpression(countExpression); + + Select select = applyQueryOnSelect(query, parameterSource, (SelectBuilder.SelectWhere) baseSelect) // + .build(); + + return render(select); + } + + /** + * Generates a {@link org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin} with a + * COUNT(...) where the countExpressions are the parameters of the count. + * + * @return a non-null {@link org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin} that joins all the + * columns and has only a count in the projection of the select. + */ + private SelectBuilder.SelectJoin getExistsSelect() { + + Table table = getTable(); + + SelectBuilder.SelectJoin baseSelect = StatementBuilder // + .select(dialect.getExistsFunction()) // + .from(table); + + // add possible joins + for (PersistentPropertyPath path : mappingContext + .findPersistentPropertyPaths(entity.getType(), p -> true)) { + + PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(mappingContext, path); + + // add a join if necessary + Join join = getJoin(extPath); + if (join != null) { + baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); + } + } + return baseSelect; + } + + /** + * Generates a {@link org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin} with a + * COUNT(...) where the countExpressions are the parameters of the count. + * + * @param countExpressions the expression to use as count parameter. + * @return a non-null {@link org.springframework.data.relational.core.sql.SelectBuilder.SelectJoin} that joins all the + * columns and has only a count in the projection of the select. + */ + private SelectBuilder.SelectJoin getSelectCountWithExpression(Expression... countExpressions) { + + Assert.notNull(countExpressions, "countExpressions must not be null"); + Assert.state(countExpressions.length >= 1, "countExpressions must contain at least one expression"); + + Table table = getTable(); + + SelectBuilder.SelectJoin baseSelect = StatementBuilder // + .select(Functions.count(countExpressions)) // + .from(table); + + // add possible joins + for (PersistentPropertyPath path : mappingContext + .findPersistentPropertyPaths(entity.getType(), p -> true)) { + + PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(mappingContext, path); + + // add a join if necessary + Join join = getJoin(extPath); + if (join != null) { + baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); + } + } + return baseSelect; + } + + private SelectBuilder.SelectOrdered applyQueryOnSelect(Query query, MapSqlParameterSource parameterSource, + SelectBuilder.SelectWhere selectBuilder) { + + Table table = Table.create(this.entity.getTableName()); + + SelectBuilder.SelectOrdered selectOrdered = query // + .getCriteria() // + .map(item -> this.applyCriteria(item, selectBuilder, parameterSource, table)) // + .orElse(selectBuilder); + + if (query.isSorted()) { + List sort = this.queryMapper.getMappedSort(table, query.getSort(), entity); + selectOrdered = selectBuilder.orderBy(sort); + } + + SelectBuilder.SelectLimitOffset limitable = (SelectBuilder.SelectLimitOffset) selectOrdered; + + if (query.getLimit() > 0) { + limitable = limitable.limit(query.getLimit()); + } + + if (query.getOffset() > 0) { + limitable = limitable.offset(query.getOffset()); + } + return (SelectBuilder.SelectOrdered) limitable; + } + + SelectBuilder.SelectOrdered applyCriteria(@Nullable CriteriaDefinition criteria, + SelectBuilder.SelectWhere whereBuilder, MapSqlParameterSource parameterSource, Table table) { + + return criteria == null || criteria.isEmpty() // Check for null and empty criteria + ? whereBuilder // + : whereBuilder.where(queryMapper.getMappedObject(parameterSource, criteria, table, entity)); + } + + /** + * Value object representing a {@code JOIN} association. + */ + static final class Join { + + private final Table joinTable; + private final Column joinColumn; + private final Column parentId; + + Join(Table joinTable, Column joinColumn, Column parentId) { + + Assert.notNull(joinTable, "JoinTable must not be null"); + Assert.notNull(joinColumn, "JoinColumn must not be null"); + Assert.notNull(parentId, "ParentId must not be null"); + + this.joinTable = joinTable; + this.joinColumn = joinColumn; + this.parentId = parentId; + } + + Table getJoinTable() { + return this.joinTable; + } + + Column getJoinColumn() { + return this.joinColumn; + } + + Column getParentId() { + return this.parentId; + } + + @Override + public boolean equals(Object o) { + + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Join join = (Join) o; + return joinTable.equals(join.joinTable) && joinColumn.equals(join.joinColumn) && parentId.equals(join.parentId); + } - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Join join = (Join) o; - return joinTable.equals(join.joinTable) && joinColumn.equals(join.joinColumn) && parentId.equals(join.parentId); - } + @Override + public int hashCode() { + return Objects.hash(joinTable, joinColumn, parentId); + } - @Override - public int hashCode() { - return Objects.hash(joinTable, joinColumn, parentId); - } + @Override + public String toString() { + + return "Join{" + // + "joinTable=" + joinTable + // + ", joinColumn=" + joinColumn + // + ", parentId=" + parentId + // + '}'; + } + } + + /** + * Value object encapsulating column name caches. + * + * @author Mark Paluch + * @author Jens Schauder + */ + static class Columns { + + private final MappingContext, RelationalPersistentProperty> mappingContext; + private final JdbcConverter converter; + + private final List columnNames = new ArrayList<>(); + 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; + + Columns(RelationalPersistentEntity entity, + MappingContext, RelationalPersistentProperty> mappingContext, + JdbcConverter converter) { + + this.mappingContext = mappingContext; + this.converter = converter; + + populateColumnNameCache(entity, ""); + + Set insertable = new LinkedHashSet<>(nonIdColumnNames); + insertable.removeAll(readOnlyColumnNames); + + this.insertableColumns = Collections.unmodifiableSet(insertable); + + Set updatable = new LinkedHashSet<>(columnNames); + + updatable.removeAll(idColumnNames); + updatable.removeAll(readOnlyColumnNames); + updatable.removeAll(insertOnlyColumnNames); + + this.updatableColumns = Collections.unmodifiableSet(updatable); + } + + private void populateColumnNameCache(RelationalPersistentEntity entity, String prefix) { + + entity.doWithAll(property -> { + + // the referencing column of referenced entity is expected to be on the other side of the relation + if (!property.isEntity()) { + initSimpleColumnName(property, prefix); + } else if (property.isEmbedded()) { + initEmbeddedColumnNames(property, prefix); + } + }); + } + + private void initSimpleColumnName(RelationalPersistentProperty property, String prefix) { + + SqlIdentifier columnName = property.getColumnName().transform(prefix::concat); + + columnNames.add(columnName); + + if (!property.getOwner().isIdProperty(property)) { + nonIdColumnNames.add(columnName); + } else { + idColumnNames.add(columnName); + } + + if (!property.isWritable()) { + readOnlyColumnNames.add(columnName); + } + + if (property.isInsertOnly()) { + insertOnlyColumnNames.add(columnName); + } + } + + private void initEmbeddedColumnNames(RelationalPersistentProperty property, String prefix) { + + String embeddedPrefix = property.getEmbeddedPrefix(); + + RelationalPersistentEntity embeddedEntity = mappingContext + .getRequiredPersistentEntity(converter.getColumnType(property)); - @Override - public String toString() { - - return "Join{" + // - "joinTable=" + joinTable + // - ", joinColumn=" + joinColumn + // - ", parentId=" + parentId + // - '}'; - } - } - - /** - * Value object encapsulating column name caches. - * - * @author Mark Paluch - * @author Jens Schauder - */ - static class Columns { - - private final MappingContext, RelationalPersistentProperty> mappingContext; - private final JdbcConverter converter; - - private final List columnNames = new ArrayList<>(); - private final List idColumnNames = new ArrayList<>(); - private final List nonIdColumnNames = new ArrayList<>(); - private final Set readOnlyColumnNames = new HashSet<>(); - private final Set insertableColumns; - private final Set updatableColumns; - - Columns(RelationalPersistentEntity entity, - MappingContext, RelationalPersistentProperty> mappingContext, - JdbcConverter converter) { - - this.mappingContext = mappingContext; - this.converter = converter; - - populateColumnNameCache(entity, ""); - - Set insertable = new LinkedHashSet<>(nonIdColumnNames); - insertable.removeAll(readOnlyColumnNames); - - this.insertableColumns = Collections.unmodifiableSet(insertable); - - Set updatable = new LinkedHashSet<>(columnNames); - - updatable.removeAll(idColumnNames); - updatable.removeAll(readOnlyColumnNames); - - this.updatableColumns = Collections.unmodifiableSet(updatable); - } - - private void populateColumnNameCache(RelationalPersistentEntity entity, String prefix) { - - entity.doWithAll(property -> { + populateColumnNameCache(embeddedEntity, prefix + embeddedPrefix); + } - // the referencing column of referenced entity is expected to be on the other side of the relation - if (!property.isEntity()) { - initSimpleColumnName(property, prefix); - } else if (property.isEmbedded()) { - initEmbeddedColumnNames(property, prefix); - } - }); - } - - private void initSimpleColumnName(RelationalPersistentProperty property, String prefix) { - - SqlIdentifier columnName = property.getColumnName().transform(prefix::concat); - - columnNames.add(columnName); - - if (!property.getOwner().isIdProperty(property)) { - nonIdColumnNames.add(columnName); - } else { - idColumnNames.add(columnName); - } - - if (!property.isWritable()) { - readOnlyColumnNames.add(columnName); - } - } - - private void initEmbeddedColumnNames(RelationalPersistentProperty property, String prefix) { - - String embeddedPrefix = property.getEmbeddedPrefix(); - - RelationalPersistentEntity embeddedEntity = mappingContext - .getRequiredPersistentEntity(converter.getColumnType(property)); - - populateColumnNameCache(embeddedEntity, prefix + embeddedPrefix); - } - - /** - * @return Column names that can be used for {@code INSERT}. - */ - Set getInsertableColumns() { - return insertableColumns; - } - - /** - * @return Column names that can be used for {@code UPDATE}. - */ - Set getUpdatableColumns() { - return updatableColumns; - } - } + /** + * @return Column names that can be used for {@code INSERT}. + */ + Set getInsertableColumns() { + return insertableColumns; + } + + /** + * @return Column names that can be used for {@code UPDATE}. + */ + Set getUpdatableColumns() { + return updatableColumns; + } + } } 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 b7f247f76..504aa33fe 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 @@ public class SqlParametersFactory { */ 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 7dab0003b..b3cbcf3b8 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.TestConfiguration; import org.springframework.data.jdbc.testing.TestDatabaseFeatures; import org.springframework.data.relational.core.conversion.DbActionExecutionException; import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.InsertOnlyProperty; import org.springframework.data.relational.core.mapping.MappedCollection; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.Table; @@ -1030,6 +1031,20 @@ class JdbcAggregateTemplateIntegrationTests { 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); @@ -1461,10 +1476,16 @@ class JdbcAggregateTemplateIntegrationTests { } @Table - class WithIdOnly { + static class WithIdOnly { @Id Long id; } + @Table + static class WithInsertOnly { + @Id Long id; + @InsertOnlyProperty + String insertOnly; + } @Configuration @Import(TestConfiguration.class) 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 1d58605c7..8ad4fda2d 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 73e85c0d8..a0aff08ce 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-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql index 6d04923f1..4dd1294ab 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-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 8d72641aa..4dd82b900 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 e6352bf25..880528cdb 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 972013645..6808c8a91 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 cf3615381..084e5db46 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 620c75f46..ed1fb9662 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 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 057f97261..5a72ab00c 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 @@ -31,7 +31,6 @@ import java.util.function.Function; import java.util.stream.Collectors; import org.reactivestreams.Publisher; - import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; @@ -594,6 +593,13 @@ public class R2dbcEntityTemplate implements R2dbcEntityOperations, BeanFactoryAw SqlIdentifier idColumn = persistentEntity.getRequiredIdProperty().getColumnName(); Parameter id = outboundRow.remove(idColumn); + + persistentEntity.forEach(p -> { + if (p.isInsertOnly()) { + outboundRow.remove(p.getColumnName()); + } + }); + Criteria criteria = Criteria.where(dataAccessStrategy.toSql(idColumn)).is(id); if (matchingVersionCriteria != null) { 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 b9c0a12a0..fc2402d93 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.MockRow; import io.r2dbc.spi.test.MockRowMetadata; import lombok.Value; import lombok.With; +import org.springframework.data.relational.core.mapping.InsertOnlyProperty; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -69,574 +70,659 @@ import org.springframework.util.CollectionUtils; * @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); + } + } } 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 6dbfc5d81..922b5b8a5 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 class BasicRelationalPersistentProperty extends AnnotationBasedPersistent return findAnnotation != null && OnEmpty.USE_EMPTY.equals(findAnnotation.onEmpty()); } + @Override + public boolean isInsertOnly() { + return findAnnotation(InsertOnlyProperty.class) != null; + } + private boolean isListLike() { return isCollectionLike() && !Set.class.isAssignableFrom(this.getType()); } 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 000000000..bd2ff04d2 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/InsertOnlyProperty.java @@ -0,0 +1,36 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.mapping; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A property with this annotation will only be written to the database during insert operations, not during updates. + * + * @author Jens Schauder + * @since 3.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Documented + +public @interface InsertOnlyProperty { +} 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 e8fe3b5d2..3f86d3d70 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 @@ public interface RelationalPersistentProperty extends PersistentProperty