DATAJDBC-233 - Changed API in order to support immutable entities.

Note: Immutable entities still aren't supported yet, these are just the API changes plus implementations that work for mutable entities.

The API of DbActions will need further evolution.

Original pull request: #80.
This commit is contained in:
Jens Schauder
2018-07-23 10:41:16 +02:00
committed by Mark Paluch
parent 962513627c
commit 1085918135
17 changed files with 124 additions and 45 deletions

View File

@@ -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 <T> void insert(T instance, Class<T> domainType, Map<String, Object> additionalParameters) {
collectVoid(das -> das.insert(instance, domainType, additionalParameters));
public <T> T insert(T instance, Class<T> domainType, Map<String, Object> additionalParameters) {
return collect(das -> das.insert(instance, domainType, additionalParameters));
}
/*

View File

@@ -39,7 +39,7 @@ public interface DataAccessStrategy {
* to get referenced are contained in this map. Must not be {@code null}.
* @param <T> the type of the instance.
*/
<T> void insert(T instance, Class<T> domainType, Map<String, Object> additionalParameters);
<T> T insert(T instance, Class<T> domainType, Map<String, Object> additionalParameters);
/**
* Updates the data of a single entity in the database. Referenced entities don't get handled.

View File

@@ -79,7 +79,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
}
@Override
public <T> void insert(T instance, Class<T> domainType, Map<String, Object> additionalParameters) {
public <T> T insert(T instance, Class<T> domainType, Map<String, Object> additionalParameters) {
KeyHolder holder = new GeneratedKeyHolder();
RelationalPersistentEntity<T> 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 <S> void setIdFromJdbc(S instance, KeyHolder holder, RelationalPersistentEntity<S> persistentEntity) {
private <S> S setIdFromJdbc(S instance, KeyHolder holder, RelationalPersistentEntity<S> persistentEntity) {
try {
PersistentPropertyAccessor<S> accessor = converter.getPropertyAccessor(persistentEntity, instance);
getIdFromHolder(holder, persistentEntity).ifPresent(it -> {
PersistentPropertyAccessor<S> 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);
}

View File

@@ -57,7 +57,9 @@ class DefaultJdbcInterpreter implements Interpreter {
*/
@Override
public <T> void interpret(Insert<T> 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 <T> void interpret(InsertRoot<T> 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) {

View File

@@ -32,8 +32,8 @@ public class DelegatingDataAccessStrategy implements DataAccessStrategy {
private DataAccessStrategy delegate;
@Override
public <T> void insert(T instance, Class<T> domainType, Map<String, Object> additionalParameters) {
delegate.insert(instance, domainType, additionalParameters);
public <T> T insert(T instance, Class<T> domainType, Map<String, Object> additionalParameters) {
return delegate.insert(instance, domainType, additionalParameters);
}
@Override

View File

@@ -142,14 +142,11 @@ public class EntityRowMapper<T> implements RowMapper<T> {
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);
});
}
}

View File

@@ -30,7 +30,7 @@ public interface JdbcAggregateOperations {
* @param instance the aggregate root of the aggregate to be saved. Must not be {@code null}.
* @param <T> the type of the aggregate root.
*/
<T> void save(T instance);
<T> T save(T instance);
/**
* Deletes a single Aggregate including all entities contained in that aggregate.

View File

@@ -66,7 +66,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
}
@Override
public <T> void save(T instance) {
public <T> 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

View File

@@ -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 <T> void insert(T instance, Class<T> domainType, Map<String, Object> additionalParameters) {
public <T> T insert(T instance, Class<T> domainType, Map<String, Object> additionalParameters) {
sqlSession().insert(namespace(domainType) + ".insert",
new MyBatisContext(null, instance, domainType, additionalParameters));
return instance;
}
/*

View File

@@ -27,7 +27,6 @@ import java.util.List;
* @author Jens Schauder
* @author Mark Paluch
*/
@RequiredArgsConstructor
@Getter
public class AggregateChange<T> {
@@ -37,12 +36,28 @@ public class AggregateChange<T> {
private final Class<T> entityType;
/** Aggregate root, to which the change applies, if available */
private final T entity;
private T entity;
private final List<DbAction<?>> actions = new ArrayList<>();
public AggregateChange(Kind kind, Class<T> 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) {

View File

@@ -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<T> {
*
* @param <T> type of the entity for which this represents a database interaction.
*/
@Value
class Insert<T> implements WithDependingOn<T>, WithEntity<T> {
@Getter
@Setter
@ToString
@RequiredArgsConstructor
class Insert<T> implements WithDependingOn<T>, WithEntity<T>, WithResultEntity<T> {
@NonNull T entity;
@NonNull PersistentPropertyPath<RelationalPersistentProperty> propertyPath;
@NonNull WithEntity<?> dependingOn;
@NonNull private final T entity;
@NonNull private final PersistentPropertyPath<RelationalPersistentProperty> propertyPath;
@NonNull private final WithEntity<?> dependingOn;
Map<String, Object> additionalValues = new HashMap<>();
private T resultingEntity;
@Override
public void doExecuteWith(Interpreter interpreter) {
interpreter.interpret(this);
@@ -89,10 +98,15 @@ public interface DbAction<T> {
*
* @param <T> type of the entity for which this represents a database interaction.
*/
@Value
class InsertRoot<T> implements WithEntity<T> {
@Getter
@Setter
@ToString
@RequiredArgsConstructor
class InsertRoot<T> implements WithEntity<T>, WithResultEntity<T> {
@NonNull T entity;
@NonNull private final T entity;
private T resultingEntity;
@Override
public void doExecuteWith(Interpreter interpreter) {
@@ -272,6 +286,26 @@ public interface DbAction<T> {
}
}
/**
* 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<T> extends WithEntity<T> {
/**
* @return the entity to persist. Guaranteed to be not {@code null}.
*/
T getResultingEntity();
@SuppressWarnings("unchecked")
@Override
default Class<T> getEntityType() {
return (Class<T>) getEntity().getClass();
}
}
/**
* A {@link DbAction} not operation on the root of an aggregate but on its contained entities.
*

View File

@@ -60,6 +60,8 @@ public class DefaultJdbcInterpreterUnitTests {
Element element = new Element();
InsertRoot<Container> containerInsert = new InsertRoot<>(container);
containerInsert.setResultingEntity(container);
Insert<?> insert = new Insert<>(element, PropertyPathUtils.toPath("element", Container.class, context), containerInsert);
interpreter.interpret(insert);

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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<ReadOnlyIdEntity, Long> {}
@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;
}

View File

@@ -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";

View File

@@ -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<DummyEntity, Long> {}
@Value
@Wither
@RequiredArgsConstructor
static class DummyEntity {
@Id Long id;