diff --git a/src/main/java/org/springframework/data/jdbc/core/CascadingDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/CascadingDataAccessStrategy.java index fba3fd74..0a622b25 100644 --- a/src/main/java/org/springframework/data/jdbc/core/CascadingDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/jdbc/core/CascadingDataAccessStrategy.java @@ -43,8 +43,8 @@ public class CascadingDataAccessStrategy implements DataAccessStrategy { * @see org.springframework.data.jdbc.core.DataAccessStrategy#insert(java.lang.Object, java.lang.Class, java.util.Map) */ @Override - public void insert(T instance, Class domainType, Map additionalParameters) { - collectVoid(das -> das.insert(instance, domainType, additionalParameters)); + public T insert(T instance, Class domainType, Map additionalParameters) { + return collect(das -> das.insert(instance, domainType, additionalParameters)); } /* diff --git a/src/main/java/org/springframework/data/jdbc/core/DataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/DataAccessStrategy.java index 5530a0c3..56240425 100644 --- a/src/main/java/org/springframework/data/jdbc/core/DataAccessStrategy.java +++ b/src/main/java/org/springframework/data/jdbc/core/DataAccessStrategy.java @@ -39,7 +39,7 @@ public interface DataAccessStrategy { * to get referenced are contained in this map. Must not be {@code null}. * @param the type of the instance. */ - void insert(T instance, Class domainType, Map additionalParameters); + T insert(T instance, Class domainType, Map additionalParameters); /** * Updates the data of a single entity in the database. Referenced entities don't get handled. diff --git a/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java index 656030e5..0a04a514 100644 --- a/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java @@ -79,7 +79,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { } @Override - public void insert(T instance, Class domainType, Map additionalParameters) { + public T insert(T instance, Class domainType, Map additionalParameters) { KeyHolder holder = new GeneratedKeyHolder(); RelationalPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); @@ -106,7 +106,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { holder // ); - setIdFromJdbc(instance, holder, persistentEntity); + instance = setIdFromJdbc(instance, holder, persistentEntity); // if there is an id property and it was null before the save // The database should have created an id and provided it. @@ -114,6 +114,8 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { if (idProperty != null && idValue == null && persistentEntity.isNew(instance)) { throw new IllegalStateException(String.format(ENTITY_NEW_AFTER_INSERT, persistentEntity)); } + + return instance; } /* @@ -321,18 +323,22 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { || (idProperty.getType() == long.class && idValue.equals(0L)); } - private void setIdFromJdbc(S instance, KeyHolder holder, RelationalPersistentEntity persistentEntity) { + private S setIdFromJdbc(S instance, KeyHolder holder, RelationalPersistentEntity persistentEntity) { try { + PersistentPropertyAccessor accessor = converter.getPropertyAccessor(persistentEntity, instance); + getIdFromHolder(holder, persistentEntity).ifPresent(it -> { - PersistentPropertyAccessor accessor = converter.getPropertyAccessor(persistentEntity, instance); RelationalPersistentProperty idProperty = persistentEntity.getRequiredIdProperty(); accessor.setProperty(idProperty, it); + }); + return accessor.getBean(); + } catch (NonTransientDataAccessException e) { throw new UnableToSetId("Unable to set id of " + instance, e); } 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 f79eaf4d..1fac5284 100644 --- a/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java +++ b/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java @@ -57,7 +57,9 @@ class DefaultJdbcInterpreter implements Interpreter { */ @Override public void interpret(Insert insert) { - accessStrategy.insert(insert.getEntity(), insert.getEntityType(), createAdditionalColumnValues(insert)); + + T entity = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), createAdditionalColumnValues(insert)); + insert.setResultingEntity(entity); } /* @@ -66,7 +68,9 @@ class DefaultJdbcInterpreter implements Interpreter { */ @Override public void interpret(InsertRoot insert) { - accessStrategy.insert(insert.getEntity(), insert.getEntityType(), Collections.emptyMap()); + + T entity = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), Collections.emptyMap()); + insert.setResultingEntity(entity); } /* @@ -162,7 +166,14 @@ class DefaultJdbcInterpreter implements Interpreter { @Nullable private Object getIdFromEntityDependingOn(DbAction.WithEntity dependingOn, RelationalPersistentEntity persistentEntity) { - return persistentEntity.getIdentifierAccessor(dependingOn.getEntity()).getIdentifier(); + + Object entity = dependingOn.getEntity(); + + if (dependingOn instanceof DbAction.WithResultEntity) { + entity = ((DbAction.WithResultEntity) dependingOn).getResultingEntity(); + } + + return persistentEntity.getIdentifierAccessor(entity).getIdentifier(); } private String getColumnNameForReverseColumn(DbAction.WithPropertyPath action) { diff --git a/src/main/java/org/springframework/data/jdbc/core/DelegatingDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/DelegatingDataAccessStrategy.java index b6c6cbe7..6ffea6c6 100644 --- a/src/main/java/org/springframework/data/jdbc/core/DelegatingDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/jdbc/core/DelegatingDataAccessStrategy.java @@ -32,8 +32,8 @@ public class DelegatingDataAccessStrategy implements DataAccessStrategy { private DataAccessStrategy delegate; @Override - public void insert(T instance, Class domainType, Map additionalParameters) { - delegate.insert(instance, domainType, additionalParameters); + public T insert(T instance, Class domainType, Map additionalParameters) { + return delegate.insert(instance, domainType, additionalParameters); } @Override 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 679f1af4..1da0c3ba 100644 --- a/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java +++ b/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java @@ -142,14 +142,11 @@ public class EntityRowMapper implements RowMapper { return converter.createInstance(entity, parameter -> { String parameterName = parameter.getName(); - Assert.notNull(parameterName, "A constructor parameter name must not be null to be used with Spring Data JDBC"); - String column = prefix + entity.getRequiredPersistentProperty(parameterName).getColumnName(); - try { - return rs.getObject(column); - } catch (SQLException o_O) { - throw new MappingException(String.format("Couldn't read column %s from ResultSet.", column), o_O); - } + Assert.notNull(parameterName, "A constructor parameter name must not be null to be used with Spring Data JDBC"); + + RelationalPersistentProperty property = entity.getRequiredPersistentProperty(parameterName); + return readFrom(rs, property, prefix); }); } } diff --git a/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java b/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java index f3e46295..af3026b1 100644 --- a/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java +++ b/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java @@ -30,7 +30,7 @@ public interface JdbcAggregateOperations { * @param instance the aggregate root of the aggregate to be saved. Must not be {@code null}. * @param the type of the aggregate root. */ - void save(T instance); + T save(T instance); /** * Deletes a single Aggregate including all entities contained in that aggregate. diff --git a/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java b/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java index 6c1a760c..5ef66c5e 100644 --- a/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java +++ b/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java @@ -66,7 +66,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { } @Override - public void save(T instance) { + public T save(T instance) { Assert.notNull(instance, "Aggregate instance must not be null!"); @@ -83,15 +83,17 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { change.executeWith(interpreter); - Object identifier = identifierAccessor.getIdentifier(); + Object identifier = entity.getIdentifierAccessor(change.getEntity()).getIdentifier(); Assert.notNull(identifier, "After saving the identifier must not be null"); publisher.publishEvent(new AfterSaveEvent( // Identifier.of(identifier), // - instance, // + change.getEntity(), // change // )); + + return (T) change.getEntity(); } @Override diff --git a/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java index 5edca330..9dd23e61 100644 --- a/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java @@ -128,9 +128,12 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { * @see org.springframework.data.jdbc.core.DataAccessStrategy#insert(java.lang.Object, java.lang.Class, java.util.Map) */ @Override - public void insert(T instance, Class domainType, Map additionalParameters) { + public T insert(T instance, Class domainType, Map additionalParameters) { + sqlSession().insert(namespace(domainType) + ".insert", new MyBatisContext(null, instance, domainType, additionalParameters)); + + return instance; } /* diff --git a/src/main/java/org/springframework/data/relational/core/conversion/AggregateChange.java b/src/main/java/org/springframework/data/relational/core/conversion/AggregateChange.java index 2ed61c0b..3b66c3e7 100644 --- a/src/main/java/org/springframework/data/relational/core/conversion/AggregateChange.java +++ b/src/main/java/org/springframework/data/relational/core/conversion/AggregateChange.java @@ -27,7 +27,6 @@ import java.util.List; * @author Jens Schauder * @author Mark Paluch */ -@RequiredArgsConstructor @Getter public class AggregateChange { @@ -37,12 +36,28 @@ public class AggregateChange { private final Class entityType; /** Aggregate root, to which the change applies, if available */ - private final T entity; + private T entity; private final List> actions = new ArrayList<>(); + public AggregateChange(Kind kind, Class entityType, T entity) { + + this.kind = kind; + this.entityType = entityType; + this.entity = entity; + } + + @SuppressWarnings("unchecked") public void executeWith(Interpreter interpreter) { - actions.forEach(a -> a.executeWith(interpreter)); + + actions.forEach(a -> { + + a.executeWith(interpreter); + + if (a instanceof DbAction.InsertRoot && a.getEntityType().equals(entityType)) { + entity = (T) ((DbAction.InsertRoot) a).getResultingEntity(); + } + }); } public void addAction(DbAction action) { diff --git a/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java b/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java index 24caadef..5a897dab 100644 --- a/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java +++ b/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java @@ -15,7 +15,11 @@ */ package org.springframework.data.relational.core.conversion; +import lombok.Getter; import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; import lombok.Value; import java.util.HashMap; @@ -64,15 +68,20 @@ public interface DbAction { * * @param type of the entity for which this represents a database interaction. */ - @Value - class Insert implements WithDependingOn, WithEntity { + @Getter + @Setter + @ToString + @RequiredArgsConstructor + class Insert implements WithDependingOn, WithEntity, WithResultEntity { - @NonNull T entity; - @NonNull PersistentPropertyPath propertyPath; - @NonNull WithEntity dependingOn; + @NonNull private final T entity; + @NonNull private final PersistentPropertyPath propertyPath; + @NonNull private final WithEntity dependingOn; Map additionalValues = new HashMap<>(); + private T resultingEntity; + @Override public void doExecuteWith(Interpreter interpreter) { interpreter.interpret(this); @@ -89,10 +98,15 @@ public interface DbAction { * * @param type of the entity for which this represents a database interaction. */ - @Value - class InsertRoot implements WithEntity { + @Getter + @Setter + @ToString + @RequiredArgsConstructor + class InsertRoot implements WithEntity, WithResultEntity { - @NonNull T entity; + @NonNull private final T entity; + + private T resultingEntity; @Override public void doExecuteWith(Interpreter interpreter) { @@ -272,6 +286,26 @@ public interface DbAction { } } + /** + * A {@link DbAction} that may "update" its entity. In order to support immutable entities this requires at least + * potentially creating a new instance, which this interface makes available. + * + * @author Jens Schauder + */ + interface WithResultEntity extends WithEntity { + + /** + * @return the entity to persist. Guaranteed to be not {@code null}. + */ + T getResultingEntity(); + + @SuppressWarnings("unchecked") + @Override + default Class getEntityType() { + return (Class) getEntity().getClass(); + } + } + /** * A {@link DbAction} not operation on the root of an aggregate but on its contained entities. * diff --git a/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java index cf82043a..c636dcd4 100644 --- a/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java @@ -60,6 +60,8 @@ public class DefaultJdbcInterpreterUnitTests { Element element = new Element(); InsertRoot containerInsert = new InsertRoot<>(container); + containerInsert.setResultingEntity(container); + Insert insert = new Insert<>(element, PropertyPathUtils.toPath("element", Container.class, context), containerInsert); interpreter.interpret(insert); diff --git a/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java index ac77d881..47d5bbb4 100644 --- a/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java @@ -35,6 +35,7 @@ import java.util.Set; import javax.naming.OperationNotSupportedException; +import lombok.experimental.Wither; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -304,6 +305,7 @@ public class EntityRowMapperUnitTests { } @RequiredArgsConstructor + @Wither static class TrivialImmutable { @Id private final Long id; diff --git a/src/test/java/org/springframework/data/jdbc/core/JdbcEntityTemplateIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/core/JdbcEntityTemplateIntegrationTests.java index 8cc03a88..5d023d7e 100644 --- a/src/test/java/org/springframework/data/jdbc/core/JdbcEntityTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/JdbcEntityTemplateIntegrationTests.java @@ -145,7 +145,8 @@ public class JdbcEntityTemplateIntegrationTests { legoSet.setManual(null); template.save(legoSet); - Manual manual = new Manual(23L); + Manual manual = new Manual(); + manual.setId(23L); manual.setContent("Some content"); legoSet.setManual(manual); @@ -180,7 +181,7 @@ public class JdbcEntityTemplateIntegrationTests { template.save(legoSet); - Manual manual = new Manual(null); + Manual manual = new Manual(); manual.setContent("other content"); legoSet.setManual(manual); @@ -191,7 +192,7 @@ public class JdbcEntityTemplateIntegrationTests { SoftAssertions softly = new SoftAssertions(); softly.assertThat(reloadedLegoSet.manual.content).isEqualTo("other content"); - softly.assertThat(template.findAll(Manual.class)).describedAs("The should be only one manual").hasSize(1); + softly.assertThat(template.findAll(Manual.class)).describedAs("There should be only one manual").hasSize(1); softly.assertAll(); } @@ -215,7 +216,7 @@ public class JdbcEntityTemplateIntegrationTests { LegoSet entity = new LegoSet(); entity.setName("Star Destroyer"); - Manual manual = new Manual(null); + Manual manual = new Manual(); manual.setContent("Accelerates to 99% of light speed. Destroys almost everything. See https://what-if.xkcd.com/1/"); entity.setManual(manual); @@ -236,7 +237,7 @@ public class JdbcEntityTemplateIntegrationTests { @Data static class Manual { - @Id private final Long id; + @Id private Long id; private String content; } diff --git a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java index cdbeb62b..070bacc6 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java @@ -20,6 +20,7 @@ import static org.assertj.core.api.Assertions.*; import lombok.Data; import lombok.Value; +import lombok.experimental.FieldDefaults; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; @@ -84,7 +85,7 @@ public class JdbcRepositoryIdGenerationIntegrationTests { @Test // DATAJDBC-98 public void primitiveIdGetsSet() { - PrimitiveIdEntity entity = new PrimitiveIdEntity(0); + PrimitiveIdEntity entity = new PrimitiveIdEntity(); entity.setName("Entity Name"); PrimitiveIdEntity saved = primitiveIdRepository.save(entity); @@ -103,6 +104,7 @@ public class JdbcRepositoryIdGenerationIntegrationTests { public interface ReadOnlyIdEntityRepository extends CrudRepository {} @Value + @FieldDefaults(makeFinal = false) static class ReadOnlyIdEntity { @Id Long id; @@ -112,7 +114,7 @@ public class JdbcRepositoryIdGenerationIntegrationTests { @Data static class PrimitiveIdEntity { - @Id private final long id; + @Id private long id; String name; } diff --git a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryManipulateDbActionsIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryManipulateDbActionsIntegrationTests.java index eb9cd07c..90ba98f0 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryManipulateDbActionsIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryManipulateDbActionsIntegrationTests.java @@ -28,6 +28,7 @@ import java.util.List; import java.util.Random; import org.junit.ClassRule; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -149,7 +150,7 @@ public class JdbcRepositoryManipulateDbActionsIntegrationTests { @Data private static class DummyEntity { - final @Id Long id; + @Id Long id; String name; boolean deleted; @@ -176,7 +177,7 @@ public class JdbcRepositoryManipulateDbActionsIntegrationTests { @RequiredArgsConstructor private static class Log { - @Id final Long id; + @Id Long id; DummyEntity entity; String text; } @@ -218,7 +219,8 @@ public class JdbcRepositoryManipulateDbActionsIntegrationTests { DummyEntity entity = (DummyEntity) event.getOptionalEntity().orElseThrow(AssertionFailedError::new); lastLogId = new Random().nextLong(); - Log log = new Log(lastLogId); + Log log = new Log(); + log.setId(lastLogId); log.entity = entity; log.text = entity.name + " saved"; 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 deab95c3..9cf4fce3 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java @@ -30,6 +30,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import lombok.experimental.Wither; import org.assertj.core.groups.Tuple; import org.junit.Before; import org.junit.Test; @@ -230,6 +231,7 @@ public class SimpleJdbcRepositoryEventsUnitTests { interface DummyEntityRepository extends CrudRepository {} @Value + @Wither @RequiredArgsConstructor static class DummyEntity { @Id Long id;