From 1f2dc16d05798af8c337cedc9c8f9b0d49bc58ff Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 8 Aug 2017 11:20:57 +0200 Subject: [PATCH] DATAJDBC-120 - Changed events to support manipulation of DbChange. This change enables the manipulation of the DbChange instance before it gets interpreted and turned into SQL statements. Only for Aggregate Roots events get fired, since these are the abstraction the repositories work on. Insert and Update events got removed, since this distinction doesn't exist on the Aggregate Root level. It only exists on the level of entities and/or tables which is represented by DbActions. Improved some tests to properly check all the events triggered. --- .../jdbc/core/DefaultJdbcInterpreter.java | 3 +- .../data/jdbc/core/EntityRowMapper.java | 5 +- .../core/EventPublishingEntityRowMapper.java | 2 +- .../data/jdbc/core/JdbcEntityTemplate.java | 66 ++--- .../jdbc/mapping/event/AfterCreation.java | 6 +- .../data/jdbc/mapping/event/AfterDelete.java | 6 +- .../data/jdbc/mapping/event/AfterInsert.java | 37 --- .../data/jdbc/mapping/event/AfterSave.java | 6 +- .../data/jdbc/mapping/event/AfterUpdate.java | 37 --- .../data/jdbc/mapping/event/BeforeDelete.java | 6 +- .../data/jdbc/mapping/event/BeforeInsert.java | 36 --- .../data/jdbc/mapping/event/BeforeSave.java | 7 +- .../data/jdbc/mapping/event/BeforeUpdate.java | 51 ---- .../data/jdbc/mapping/event/Identifier.java | 4 + .../mapping/event/JdbcEventWithEntity.java | 6 +- .../jdbc/mapping/event/JdbcEventWithId.java | 5 +- .../event/JdbcEventWithIdAndEntity.java | 5 +- .../jdbc/mapping/event/SimpleJdbcEvent.java | 9 +- ...entPublishingEntityRowMapperUnitTests.java | 3 +- ...ryManipulateDbActionsIntegrationTests.java | 232 ++++++++++++++++++ ...oryPropertyConversionIntegrationTests.java | 4 +- .../SimpleJdbcRepositoryEventsUnitTests.java | 55 +++-- ...nipulateDbActionsIntegrationTests-hsql.sql | 2 + ...ipulateDbActionsIntegrationTests-mysql.sql | 2 + ...lateDbActionsIntegrationTests-postgres.sql | 4 + 25 files changed, 362 insertions(+), 237 deletions(-) delete mode 100644 src/main/java/org/springframework/data/jdbc/mapping/event/AfterInsert.java delete mode 100644 src/main/java/org/springframework/data/jdbc/mapping/event/AfterUpdate.java delete mode 100644 src/main/java/org/springframework/data/jdbc/mapping/event/BeforeInsert.java delete mode 100644 src/main/java/org/springframework/data/jdbc/mapping/event/BeforeUpdate.java rename src/test/java/org/springframework/data/jdbc/{repository => core}/EventPublishingEntityRowMapperUnitTests.java (94%) create mode 100644 src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryManipulateDbActionsIntegrationTests.java create mode 100644 src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryManipulateDbActionsIntegrationTests-hsql.sql create mode 100644 src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryManipulateDbActionsIntegrationTests-mysql.sql create mode 100644 src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryManipulateDbActionsIntegrationTests-postgres.sql diff --git a/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java b/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java index 41195a51b..e6aadeeb5 100644 --- a/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java +++ b/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java @@ -25,7 +25,6 @@ import org.springframework.data.jdbc.core.conversion.DbAction.DeleteAll; import org.springframework.data.jdbc.core.conversion.DbAction.Insert; import org.springframework.data.jdbc.core.conversion.DbAction.Update; import org.springframework.data.jdbc.core.conversion.Interpreter; -import org.springframework.data.jdbc.mapping.event.Identifier; import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; @@ -74,7 +73,7 @@ class DefaultJdbcInterpreter implements Interpreter { public void interpret(Delete delete) { if (delete.getPropertyPath() == null) { - template.doDelete(Identifier.of(delete.getRootId()), Optional.ofNullable(delete.getEntity()), + template.doDelete(delete.getRootId(), Optional.ofNullable(delete.getEntity()), delete.getEntityType()); } else { template.doDelete(delete.getRootId(), delete.getPropertyPath()); diff --git a/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java b/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java index 667827c90..fcfa92116 100644 --- a/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java +++ b/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java @@ -128,10 +128,11 @@ class EntityRowMapper implements RowMapper { return null; } + String column = prefix + name; try { - return conversionService.convert(resultSet.getObject(prefix + name), parameter.getType().getType()); + return conversionService.convert(resultSet.getObject(column), parameter.getType().getType()); } catch (SQLException o_O) { - throw new MappingException(String.format("Couldn't read column %s from ResultSet.", name), o_O); + throw new MappingException(String.format("Couldn't read column %s from ResultSet.", column), o_O); } } } diff --git a/src/main/java/org/springframework/data/jdbc/core/EventPublishingEntityRowMapper.java b/src/main/java/org/springframework/data/jdbc/core/EventPublishingEntityRowMapper.java index a543cd449..8b39ee144 100644 --- a/src/main/java/org/springframework/data/jdbc/core/EventPublishingEntityRowMapper.java +++ b/src/main/java/org/springframework/data/jdbc/core/EventPublishingEntityRowMapper.java @@ -50,7 +50,7 @@ public class EventPublishingEntityRowMapper implements RowMapper { T instance = delegate.mapRow(resultSet, i); - publisher.publishEvent(new AfterCreation(Identifier.of(entityInformation.getRequiredId(instance)), instance)); + publisher.publishEvent(new AfterCreation(Identifier.of(entityInformation.getRequiredId(instance)), instance, null)); return instance; } diff --git a/src/main/java/org/springframework/data/jdbc/core/JdbcEntityTemplate.java b/src/main/java/org/springframework/data/jdbc/core/JdbcEntityTemplate.java index e55754fbd..6e596c0b6 100644 --- a/src/main/java/org/springframework/data/jdbc/core/JdbcEntityTemplate.java +++ b/src/main/java/org/springframework/data/jdbc/core/JdbcEntityTemplate.java @@ -36,11 +36,9 @@ import org.springframework.data.jdbc.core.conversion.Interpreter; import org.springframework.data.jdbc.core.conversion.JdbcEntityDeleteWriter; import org.springframework.data.jdbc.core.conversion.JdbcEntityWriter; import org.springframework.data.jdbc.mapping.event.AfterDelete; -import org.springframework.data.jdbc.mapping.event.AfterInsert; -import org.springframework.data.jdbc.mapping.event.AfterUpdate; +import org.springframework.data.jdbc.mapping.event.AfterSave; import org.springframework.data.jdbc.mapping.event.BeforeDelete; -import org.springframework.data.jdbc.mapping.event.BeforeInsert; -import org.springframework.data.jdbc.mapping.event.BeforeUpdate; +import org.springframework.data.jdbc.mapping.event.BeforeSave; import org.springframework.data.jdbc.mapping.event.Identifier; import org.springframework.data.jdbc.mapping.event.Identifier.Specified; import org.springframework.data.jdbc.mapping.model.BasicJdbcPersistentEntityInformation; @@ -102,14 +100,29 @@ public class JdbcEntityTemplate implements JdbcEntityOperations { @Override public void save(T instance, Class domainType) { - createChange(instance).executeWith(interpreter); + + JdbcPersistentEntityInformation entityInformation = context.getRequiredPersistentEntityInformation(domainType); + + AggregateChange change = createChange(instance); + + publisher.publishEvent(new BeforeSave( // + Identifier.ofNullable(entityInformation.getId(instance)), // + instance, // + change // + )); + + change.executeWith(interpreter); + + publisher.publishEvent(new AfterSave( // + Identifier.of(entityInformation.getId(instance)), // + instance, // + change // + )); } @Override public void insert(T instance, Class domainType, Map additionalParameters) { - publisher.publishEvent(new BeforeInsert(instance)); - KeyHolder holder = new GeneratedKeyHolder(); JdbcPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); JdbcPersistentEntityInformation entityInformation = context @@ -132,23 +145,17 @@ public class JdbcEntityTemplate implements JdbcEntityOperations { throw new IllegalStateException(String.format(ENTITY_NEW_AFTER_INSERT, persistentEntity)); } - publisher.publishEvent(new AfterInsert(Identifier.of(entityInformation.getRequiredId(instance)), instance)); - } @Override public void update(S instance, Class domainType) { JdbcPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); - JdbcPersistentEntityInformation entityInformation = context - .getRequiredPersistentEntityInformation(domainType); - Specified specifiedId = Identifier.of(entityInformation.getRequiredId(instance)); - publisher.publishEvent(new BeforeUpdate(specifiedId, instance)); operations.update(sql(domainType).getUpdate(), getPropertyMap(instance, persistentEntity)); - publisher.publishEvent(new AfterUpdate(specifiedId, instance)); } + @SuppressWarnings("ConstantConditions") @Override public long count(Class domainType) { return operations.getJdbcOperations().queryForObject(sql(domainType).getCount(), Long.class); @@ -211,9 +218,21 @@ public class JdbcEntityTemplate implements JdbcEntityOperations { change.executeWith(interpreter); } - void doDelete(Object rootId, PropertyPath propertyPath) { + private void deleteTree(Object id, Object entity, Class domainType) { - JdbcPersistentEntity entityToDelete = context.getRequiredPersistentEntity(propertyPath.getTypeInformation()); + AggregateChange change = createDeletingChange(id, entity, domainType); + + Specified specifiedId = Identifier.of(id); + Optional optionalEntity = Optional.ofNullable(entity); + publisher.publishEvent(new BeforeDelete(specifiedId, optionalEntity, change)); + + change.executeWith(interpreter); + + publisher.publishEvent(new AfterDelete(specifiedId, optionalEntity, change)); + + } + + void doDelete(Object rootId, PropertyPath propertyPath) { JdbcPersistentEntity rootEntity = context.getRequiredPersistentEntity(propertyPath.getOwningType()); @@ -228,23 +247,12 @@ public class JdbcEntityTemplate implements JdbcEntityOperations { } - void doDelete(Specified specifiedId, Optional optionalEntity, Class domainType) { - - publisher.publishEvent(new BeforeDelete(specifiedId, optionalEntity)); + void doDelete(Object id, Optional optionalEntity, Class domainType) { String deleteByIdSql = sql(domainType).getDeleteById(); - MapSqlParameterSource parameter = createIdParameterSource(specifiedId.getValue(), domainType); + MapSqlParameterSource parameter = createIdParameterSource(id, domainType); operations.update(deleteByIdSql, parameter); - - publisher.publishEvent(new AfterDelete(specifiedId, optionalEntity)); - } - - private void deleteTree(Object id, Object entity, Class domainType) { - - AggregateChange change = createDeletingChange(id, entity, domainType); - - change.executeWith(interpreter); } private AggregateChange createChange(T instance) { diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/AfterCreation.java b/src/main/java/org/springframework/data/jdbc/mapping/event/AfterCreation.java index b0f2e8edd..6ceb9fa53 100644 --- a/src/main/java/org/springframework/data/jdbc/mapping/event/AfterCreation.java +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/AfterCreation.java @@ -15,6 +15,7 @@ */ package org.springframework.data.jdbc.mapping.event; +import org.springframework.data.jdbc.core.conversion.AggregateChange; import org.springframework.data.jdbc.mapping.event.Identifier.Specified; /** @@ -31,8 +32,9 @@ public class AfterCreation extends JdbcEventWithIdAndEntity { /** * @param id of the entity * @param entity the newly instantiated entity. + * @param change */ - public AfterCreation(Specified id, Object entity) { - super(id, entity); + public AfterCreation(Specified id, Object entity, AggregateChange change) { + super(id, entity, change); } } diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/AfterDelete.java b/src/main/java/org/springframework/data/jdbc/mapping/event/AfterDelete.java index f1f24d195..dd4650a56 100644 --- a/src/main/java/org/springframework/data/jdbc/mapping/event/AfterDelete.java +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/AfterDelete.java @@ -17,6 +17,7 @@ package org.springframework.data.jdbc.mapping.event; import java.util.Optional; +import org.springframework.data.jdbc.core.conversion.AggregateChange; import org.springframework.data.jdbc.mapping.event.Identifier.Specified; /** @@ -33,8 +34,9 @@ public class AfterDelete extends JdbcEventWithId { /** * @param id of the entity. * @param instance the deleted entity if it is available. + * @param change the {@link AggregateChange} encoding the planned actions to be performed on the database. */ - public AfterDelete(Specified id, Optional instance) { - super(id, instance); + public AfterDelete(Specified id, Optional instance, AggregateChange change) { + super(id, instance, change); } } diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/AfterInsert.java b/src/main/java/org/springframework/data/jdbc/mapping/event/AfterInsert.java deleted file mode 100644 index 5b82bd3c9..000000000 --- a/src/main/java/org/springframework/data/jdbc/mapping/event/AfterInsert.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2017 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 - * - * http://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.jdbc.mapping.event; - -import org.springframework.data.jdbc.mapping.event.Identifier.Specified; - -/** - * Gets published after an entity got inserted into the database. - * - * @author Jens Schauder - * @since 2.0 - */ -public class AfterInsert extends AfterSave { - - private static final long serialVersionUID = 1312645717283677063L; - - /** - * @param id identifier of the entity triggering the event. - * @param instance the newly inserted entity. - */ - public AfterInsert(Specified id, Object instance) { - super(id, instance); - } -} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/AfterSave.java b/src/main/java/org/springframework/data/jdbc/mapping/event/AfterSave.java index 9c79e2139..2ce327881 100644 --- a/src/main/java/org/springframework/data/jdbc/mapping/event/AfterSave.java +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/AfterSave.java @@ -15,6 +15,7 @@ */ package org.springframework.data.jdbc.mapping.event; +import org.springframework.data.jdbc.core.conversion.AggregateChange; import org.springframework.data.jdbc.mapping.event.Identifier.Specified; /** @@ -30,8 +31,9 @@ public class AfterSave extends JdbcEventWithIdAndEntity { /** * @param id identifier of * @param instance the newly saved entity. + * @param change the {@link AggregateChange} encoding the planned actions to be performed on the database. */ - AfterSave(Specified id, Object instance) { - super(id, instance); + public AfterSave(Specified id, Object instance, AggregateChange change) { + super(id, instance, change); } } diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/AfterUpdate.java b/src/main/java/org/springframework/data/jdbc/mapping/event/AfterUpdate.java deleted file mode 100644 index cda83304b..000000000 --- a/src/main/java/org/springframework/data/jdbc/mapping/event/AfterUpdate.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2017 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 - * - * http://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.jdbc.mapping.event; - -import org.springframework.data.jdbc.mapping.event.Identifier.Specified; - -/** - * Gets published after an entity was updated in the database. - * - * @author Jens Schauder - * @since 2.0 - */ -public class AfterUpdate extends AfterSave { - - private static final long serialVersionUID = -1765706904721563399L; - - /** - * @param id of the entity - * @param instance the updated entity. - */ - public AfterUpdate(Specified id, Object instance) { - super(id, instance); - } -} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeDelete.java b/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeDelete.java index ef7d1c0f1..7088060b3 100644 --- a/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeDelete.java +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeDelete.java @@ -17,6 +17,7 @@ package org.springframework.data.jdbc.mapping.event; import java.util.Optional; +import org.springframework.data.jdbc.core.conversion.AggregateChange; import org.springframework.data.jdbc.mapping.event.Identifier.Specified; /** @@ -32,8 +33,9 @@ public class BeforeDelete extends JdbcEventWithId { /** * @param id the id of the entity * @param entity the entity about to get deleted. Might be empty. + * @param change the {@link AggregateChange} encoding the planned actions to be performed on the database. */ - public BeforeDelete(Specified id, Optional entity) { - super(id, entity); + public BeforeDelete(Specified id, Optional entity, AggregateChange change) { + super(id, entity, change); } } diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeInsert.java b/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeInsert.java deleted file mode 100644 index 37d222703..000000000 --- a/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeInsert.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2017 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 - * - * http://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.jdbc.mapping.event; - -/** - * Gets published before an entity gets inserted into the database. When the id-property of the entity must get set - * manually, an event listener for this event may do so.
- * The {@link Identifier} is {@link Unset#UNSET} - * - * @author Jens Schauder - * @since 2.0 - */ -public class BeforeInsert extends BeforeSave { - - private static final long serialVersionUID = -5350552051337308870L; - - /** - * @param instance the entity about to get inserted. - */ - public BeforeInsert(Object instance) { - super(Unset.UNSET, instance); - } -} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeSave.java b/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeSave.java index ae100d39a..7db884ccd 100644 --- a/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeSave.java +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeSave.java @@ -15,6 +15,8 @@ */ package org.springframework.data.jdbc.mapping.event; +import org.springframework.data.jdbc.core.conversion.AggregateChange; + /** * Subclasses of this get published before an entity gets saved to the database. * @@ -28,8 +30,9 @@ public class BeforeSave extends JdbcEventWithEntity { /** * @param id of the entity to be saved. * @param instance the entity about to get saved. + * @param change */ - BeforeSave(Identifier id, Object instance) { - super(id, instance); + public BeforeSave(Identifier id, Object instance, AggregateChange change) { + super(id, instance, change); } } diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeUpdate.java b/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeUpdate.java deleted file mode 100644 index 57f7a330e..000000000 --- a/src/main/java/org/springframework/data/jdbc/mapping/event/BeforeUpdate.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2017 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 - * - * http://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.jdbc.mapping.event; - -import org.springframework.data.jdbc.mapping.event.Identifier.Specified; - -/** - * Gets published before an entity gets updated in the database. - * - * @author Jens Schauder - * @since 2.0 - */ -public class BeforeUpdate extends BeforeSave implements WithId { - - private static final long serialVersionUID = 794561215071613972L; - - private final Specified id; - - /** - * @param id of the entity about to get updated - * @param instance the entity about to get updated. - */ - public BeforeUpdate(Specified id, Object instance) { - - super(id, instance); - - this.id = id; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.jdbc.mapping.event.JdbcEvent#getId() - */ - @Override - public Specified getId() { - return id; - } -} diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/Identifier.java b/src/main/java/org/springframework/data/jdbc/mapping/event/Identifier.java index 0db466462..3c6cfe34d 100644 --- a/src/main/java/org/springframework/data/jdbc/mapping/event/Identifier.java +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/Identifier.java @@ -40,6 +40,10 @@ public interface Identifier { return SpecifiedIdentifier.of(identifier); } + static Identifier ofNullable(Object identifier) { + return identifier == null ? Unset.UNSET : of(identifier); + } + /** * Creates a new {@link Identifier} for the given optional source value. * diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEventWithEntity.java b/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEventWithEntity.java index 1d325f6bd..805dd7d4e 100644 --- a/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEventWithEntity.java +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEventWithEntity.java @@ -17,6 +17,8 @@ package org.springframework.data.jdbc.mapping.event; import java.util.Optional; +import org.springframework.data.jdbc.core.conversion.AggregateChange; + /** * A {@link SimpleJdbcEvent} which is guaranteed to have an entity. * @@ -27,7 +29,7 @@ public class JdbcEventWithEntity extends SimpleJdbcEvent implements WithEntity { private static final long serialVersionUID = 4891455396602090638L; - public JdbcEventWithEntity(Identifier id, Object entity) { - super(id, Optional.of(entity)); + public JdbcEventWithEntity(Identifier id, Object entity, AggregateChange change) { + super(id, Optional.of(entity), change); } } diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEventWithId.java b/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEventWithId.java index d5c1ded08..f5711e628 100644 --- a/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEventWithId.java +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEventWithId.java @@ -17,6 +17,7 @@ package org.springframework.data.jdbc.mapping.event; import java.util.Optional; +import org.springframework.data.jdbc.core.conversion.AggregateChange; import org.springframework.data.jdbc.mapping.event.Identifier.Specified; /** @@ -31,9 +32,9 @@ public class JdbcEventWithId extends SimpleJdbcEvent implements WithId { private final Specified id; - public JdbcEventWithId(Specified id, Optional entity) { + public JdbcEventWithId(Specified id, Optional entity, AggregateChange change) { - super(id, entity); + super(id, entity, change); this.id = id; } diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEventWithIdAndEntity.java b/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEventWithIdAndEntity.java index e9268bf4b..621cc9d42 100644 --- a/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEventWithIdAndEntity.java +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/JdbcEventWithIdAndEntity.java @@ -19,6 +19,7 @@ import lombok.Getter; import java.util.Optional; +import org.springframework.data.jdbc.core.conversion.AggregateChange; import org.springframework.data.jdbc.mapping.event.Identifier.Specified; /** @@ -32,7 +33,7 @@ public class JdbcEventWithIdAndEntity extends JdbcEventWithId implements WithEnt private static final long serialVersionUID = -3194462549552515519L; - public JdbcEventWithIdAndEntity(Specified id, Object entity) { - super(id, Optional.of(entity)); + public JdbcEventWithIdAndEntity(Specified id, Object entity, AggregateChange change) { + super(id, Optional.of(entity), change); } } diff --git a/src/main/java/org/springframework/data/jdbc/mapping/event/SimpleJdbcEvent.java b/src/main/java/org/springframework/data/jdbc/mapping/event/SimpleJdbcEvent.java index bf5df81af..321a27abb 100644 --- a/src/main/java/org/springframework/data/jdbc/mapping/event/SimpleJdbcEvent.java +++ b/src/main/java/org/springframework/data/jdbc/mapping/event/SimpleJdbcEvent.java @@ -18,6 +18,7 @@ package org.springframework.data.jdbc.mapping.event; import java.util.Optional; import org.springframework.context.ApplicationEvent; +import org.springframework.data.jdbc.core.conversion.AggregateChange; /** * The common superclass for all events published by JDBC repositories. {@link #getSource} contains the @@ -32,12 +33,14 @@ class SimpleJdbcEvent extends ApplicationEvent implements JdbcEvent { private static final long serialVersionUID = -1798807778668751659L; private final Object entity; + private final AggregateChange change; - SimpleJdbcEvent(Identifier id, Optional entity) { + SimpleJdbcEvent(Identifier id, Optional entity, AggregateChange change) { super(id); this.entity = entity.orElse(null); + this.change = change; } /* @@ -57,4 +60,8 @@ class SimpleJdbcEvent extends ApplicationEvent implements JdbcEvent { public Optional getOptionalEntity() { return Optional.ofNullable(entity); } + + public AggregateChange getChange() { + return change; + } } diff --git a/src/test/java/org/springframework/data/jdbc/repository/EventPublishingEntityRowMapperUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/EventPublishingEntityRowMapperUnitTests.java similarity index 94% rename from src/test/java/org/springframework/data/jdbc/repository/EventPublishingEntityRowMapperUnitTests.java rename to src/test/java/org/springframework/data/jdbc/core/EventPublishingEntityRowMapperUnitTests.java index 0c9b54b03..d84ec2c8f 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/EventPublishingEntityRowMapperUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/EventPublishingEntityRowMapperUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository; +package org.springframework.data.jdbc.core; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -31,7 +31,6 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.annotation.Id; -import org.springframework.data.jdbc.core.EventPublishingEntityRowMapper; import org.springframework.data.jdbc.mapping.event.AfterCreation; import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntityInformation; import org.springframework.jdbc.core.RowMapper; diff --git a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryManipulateDbActionsIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryManipulateDbActionsIntegrationTests.java new file mode 100644 index 000000000..1615c05c3 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryManipulateDbActionsIntegrationTests.java @@ -0,0 +1,232 @@ +/* + * Copyright 2017 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 + * + * http://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.jdbc.repository; + +import static java.util.Arrays.*; +import static org.assertj.core.api.Assertions.*; + +import junit.framework.AssertionFailedError; +import lombok.Data; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +import java.util.List; +import java.util.Random; + +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.jdbc.core.conversion.DbAction; +import org.springframework.data.jdbc.mapping.event.BeforeDelete; +import org.springframework.data.jdbc.mapping.event.BeforeSave; +import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories; +import org.springframework.data.jdbc.testing.TestConfiguration; +import org.springframework.data.repository.CrudRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.rules.SpringClassRule; +import org.springframework.test.context.junit4.rules.SpringMethodRule; + +/** + * Tests that the event infrastructure of Spring Data JDBC is sufficient to manipulate the {@link DbAction}s to be + * executed against the database. + * + * @author Jens Schauder + */ +@ContextConfiguration +public class JdbcRepositoryManipulateDbActionsIntegrationTests { + + @ClassRule public static final SpringClassRule classRule = new SpringClassRule(); + @Rule public SpringMethodRule methodRule = new SpringMethodRule(); + + @Autowired DummyEntityRepository repository; + @Autowired LogRepository logRepository; + + @Test // DATAJDBC-120 + public void softDelete() { + + // given a persistent entity + DummyEntity entity = new DummyEntity(null, "Hello"); + repository.save(entity); + assertThat(entity.id).isNotNull(); + + // when I delete the entity + repository.delete(entity); + + // it is still in the repository, but marked as deleted + assertThat(repository.findById(entity.id)) // + .contains(new DummyEntity( // + entity.id, // + entity.name, // + true) // + ); + + } + + @Test // DATAJDBC-120 + public void softDeleteMany() { + + // given persistent entities + DummyEntity one = new DummyEntity(null, "One"); + DummyEntity two = new DummyEntity(null, "Two"); + repository.saveAll(asList(one, two)); + + assertThat(one.id).isNotNull(); + + // when I delete the entities + repository.deleteAll(asList(one, two)); + + // they are still in the repository, but marked as deleted + assertThat(repository.findById(one.id)) // + .contains(new DummyEntity( // + one.id, // + one.name, // + true) // + ); + + assertThat(repository.findById(two.id)) // + .contains(new DummyEntity( // + two.id, // + two.name, // + true) // + ); + } + + @Test // DATAJDBC-120 + public void loggingOnSave() { + + // given a new entity + DummyEntity one = new DummyEntity(null, "one"); + + repository.save(one); + assertThat(one.id).isNotNull(); + + // they are still in the repository, but marked as deleted + assertThat(logRepository.findById(Config.lastLogId)) // + .isNotEmpty() // + .map(Log::getText) // + .contains("one saved"); + } + + @Test // DATAJDBC-120 + public void loggingOnSaveMany() { + + // given a new entity + DummyEntity one = new DummyEntity(null, "one"); + DummyEntity two = new DummyEntity(null, "two"); + + repository.saveAll(asList(one, two)); + assertThat(one.id).isNotNull(); + + // they are still in the repository, but marked as deleted + assertThat(logRepository.findById(Config.lastLogId)) // + .isNotEmpty() // + .map(Log::getText) // + .contains("two saved"); + } + + @Data + private static class DummyEntity { + + final @Id Long id; + String name; + boolean deleted; + + DummyEntity(Long id, String name) { + + this.id = id; + this.name = name; + this.deleted = false; + } + + @PersistenceConstructor + DummyEntity(Long id, String name, boolean deleted) { + + this.id = id; + this.name = name; + this.deleted = deleted; + } + } + + private interface DummyEntityRepository extends CrudRepository {} + + @Getter + @Setter + @RequiredArgsConstructor + private static class Log { + + @Id final Long id; + DummyEntity entity; + String text; + } + + private interface LogRepository extends CrudRepository {} + + @Configuration + @Import(TestConfiguration.class) + @EnableJdbcRepositories(considerNestedRepositories = true) + static class Config { + + static long lastLogId; + + @Bean + Class testClass() { + return JdbcRepositoryManipulateDbActionsIntegrationTests.class; + } + + @Bean + ApplicationListener softDeleteListener() { + + return event -> { + + DummyEntity entity = (DummyEntity) event.getOptionalEntity().orElseThrow(AssertionFailedError::new); + entity.deleted = true; + + List actions = event.getChange().getActions(); + actions.clear(); + actions.add(DbAction.update(entity, null)); + }; + } + + @Bean + ApplicationListener logOnSaveListener() { + + // this would actually be easier to implement with an AfterSave listener, but we want to test AggregateChange + // manipulation. + return event -> { + + DummyEntity entity = (DummyEntity) event.getOptionalEntity().orElseThrow(AssertionFailedError::new); + lastLogId = new Random().nextLong(); + Log log = new Log(lastLogId); + log.entity = entity; + log.text = entity.name + " saved"; + + + List actions = event.getChange().getActions(); + actions.add(DbAction.insert(log, null)); + }; + } + + } + +} diff --git a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryPropertyConversionIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryPropertyConversionIntegrationTests.java index 8f485778a..66cb61821 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryPropertyConversionIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryPropertyConversionIntegrationTests.java @@ -40,7 +40,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.data.annotation.Id; -import org.springframework.data.jdbc.mapping.event.BeforeInsert; +import org.springframework.data.jdbc.mapping.event.BeforeSave; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; import org.springframework.data.jdbc.testing.TestConfiguration; import org.springframework.data.repository.CrudRepository; @@ -77,7 +77,7 @@ public class JdbcRepositoryPropertyConversionIntegrationTests { @Bean ApplicationListener applicationListener() { - return (ApplicationListener) beforeInsert -> ((EntityWithColumnsRequiringConversions) beforeInsert + return (ApplicationListener) beforeInsert -> ((EntityWithColumnsRequiringConversions) beforeInsert .getEntity()).setIdTimestamp(getNow()); } diff --git a/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java b/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java index af0a6e5e2..fe3b04ed1 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java @@ -2,26 +2,27 @@ package org.springframework.data.jdbc.repository; import static java.util.Arrays.*; import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; +import junit.framework.AssertionFailedError; import lombok.Data; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import org.assertj.core.groups.Tuple; import org.junit.Before; import org.junit.Test; import org.mockito.stubbing.Answer; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.mapping.event.AfterDelete; -import org.springframework.data.jdbc.mapping.event.AfterInsert; -import org.springframework.data.jdbc.mapping.event.AfterUpdate; +import org.springframework.data.jdbc.mapping.event.AfterSave; import org.springframework.data.jdbc.mapping.event.BeforeDelete; -import org.springframework.data.jdbc.mapping.event.BeforeInsert; -import org.springframework.data.jdbc.mapping.event.BeforeUpdate; +import org.springframework.data.jdbc.mapping.event.BeforeSave; import org.springframework.data.jdbc.mapping.event.Identifier; import org.springframework.data.jdbc.mapping.event.JdbcEvent; import org.springframework.data.jdbc.mapping.model.DefaultNamingStrategy; @@ -55,8 +56,12 @@ public class SimpleJdbcRepositoryEventsUnitTests { repository.save(entity); - assertThat(publisher.events.get(0)).isInstanceOf(BeforeUpdate.class); - assertThat(publisher.events.get(1)).isInstanceOf(AfterUpdate.class); + assertThat(publisher.events) // + .extracting(e -> (Class) e.getClass()) // + .containsExactly( // + BeforeSave.class, // + AfterSave.class // + ); } @Test // DATAJDBC-99 @@ -67,10 +72,14 @@ public class SimpleJdbcRepositoryEventsUnitTests { repository.saveAll(asList(entity1, entity2)); - assertThat(publisher.events.get(0)).isInstanceOf(BeforeInsert.class); - assertThat(publisher.events.get(1)).isInstanceOf(AfterInsert.class); - assertThat(publisher.events.get(2)).isInstanceOf(BeforeUpdate.class); - assertThat(publisher.events.get(3)).isInstanceOf(AfterUpdate.class); + assertThat(publisher.events) // + .extracting(e -> (Class) e.getClass()) // + .containsExactly( // + BeforeSave.class, // + AfterSave.class, // + BeforeSave.class, // + AfterSave.class // + ); } @Test // DATAJDBC-99 @@ -80,14 +89,14 @@ public class SimpleJdbcRepositoryEventsUnitTests { repository.delete(entity); - assertThat(publisher.events.get(0)).isInstanceOf(BeforeDelete.class); - assertThat(publisher.events.get(1)).isInstanceOf(AfterDelete.class); - - assertThat(publisher.events.get(0).getOptionalEntity()).hasValue(entity); - assertThat(publisher.events.get(1).getOptionalEntity()).hasValue(entity); - - assertThat(publisher.events.get(0).getId()).isEqualTo(Identifier.of(23L)); - assertThat(publisher.events.get(1).getId()).isEqualTo(Identifier.of(23L)); + assertThat(publisher.events).extracting( // + e -> (Class) e.getClass(), // + e -> e.getOptionalEntity().orElseGet(AssertionFailedError::new), // + JdbcEvent::getId // + ).containsExactly( // + Tuple.tuple(BeforeDelete.class, entity, Identifier.of(23L)), // + Tuple.tuple(AfterDelete.class, entity, Identifier.of(23L)) // + ); } @Test // DATAJDBC-99 @@ -95,8 +104,12 @@ public class SimpleJdbcRepositoryEventsUnitTests { repository.deleteById(23L); - assertThat(publisher.events.get(0)).isInstanceOf(BeforeDelete.class); - assertThat(publisher.events.get(1)).isInstanceOf(AfterDelete.class); + assertThat(publisher.events) // + .extracting(e -> (Class) e.getClass()) // + .containsExactly( // + BeforeDelete.class, // + AfterDelete.class // + ); } private static NamedParameterJdbcOperations createIdGeneratingOperations() { diff --git a/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryManipulateDbActionsIntegrationTests-hsql.sql b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryManipulateDbActionsIntegrationTests-hsql.sql new file mode 100644 index 000000000..27f8287ca --- /dev/null +++ b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryManipulateDbActionsIntegrationTests-hsql.sql @@ -0,0 +1,2 @@ +CREATE TABLE dummyentity ( id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, NAME VARCHAR(100), DELETED CHAR(1), log BIGINT); +CREATE TABLE log ( id BIGINT, TEXT VARCHAR(100)); diff --git a/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryManipulateDbActionsIntegrationTests-mysql.sql b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryManipulateDbActionsIntegrationTests-mysql.sql new file mode 100644 index 000000000..9b52d44da --- /dev/null +++ b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryManipulateDbActionsIntegrationTests-mysql.sql @@ -0,0 +1,2 @@ +CREATE TABLE dummyentity ( id BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100), DELETED CHAR(1), log BIGINT); +CREATE TABLE log ( id BIGINT, TEXT VARCHAR(100)); diff --git a/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryManipulateDbActionsIntegrationTests-postgres.sql b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryManipulateDbActionsIntegrationTests-postgres.sql new file mode 100644 index 000000000..703cd57fb --- /dev/null +++ b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryManipulateDbActionsIntegrationTests-postgres.sql @@ -0,0 +1,4 @@ +DROP TABLE dummyentity; +DROP TABLE log; +CREATE TABLE dummyentity ( id SERIAL PRIMARY KEY, NAME VARCHAR(100), DELETED CHAR(5), log BIGINT); +CREATE TABLE log ( id BIGINT, TEXT VARCHAR(100));