DATAJDBC-113 - Added support for Set-valued references.
When loading the referenced entities get loaded by a separate select statement. Changes to a collection get handled just like 1-1 relationships by deleting all and reinserting them again. Introduced a separate method in JdbcPersistentProperty instead of just unsing the table name, although it currently still is the table name. Now providing SqlTypes in SqlParameterSource, since MySql has a problem with null parameters when it doesn't know the type. Fixed an error where a SQL-statement generated by the SqlGenerator was obviously wrong.
This commit is contained in:
committed by
Greg Turnquist
parent
2067fb6952
commit
9b7b1267bb
@@ -17,7 +17,6 @@ package org.springframework.data.jdbc.core;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jdbc.core.conversion.DbAction;
|
||||
import org.springframework.data.jdbc.core.conversion.DbAction.Delete;
|
||||
@@ -73,8 +72,7 @@ class DefaultJdbcInterpreter implements Interpreter {
|
||||
public <T> void interpret(Delete<T> delete) {
|
||||
|
||||
if (delete.getPropertyPath() == null) {
|
||||
template.doDelete(delete.getRootId(), Optional.ofNullable(delete.getEntity()),
|
||||
delete.getEntityType());
|
||||
template.doDelete(delete.getRootId(), delete.getEntityType());
|
||||
} else {
|
||||
template.doDelete(delete.getRootId(), delete.getPropertyPath());
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
package org.springframework.data.jdbc.core;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.data.convert.ClassGeneratingEntityInstantiator;
|
||||
@@ -42,13 +42,26 @@ import org.springframework.jdbc.core.RowMapper;
|
||||
* @author Oliver Gierke
|
||||
* @since 2.0
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
class EntityRowMapper<T> implements RowMapper<T> {
|
||||
|
||||
private final JdbcPersistentEntity<T> entity;
|
||||
private final EntityInstantiator instantiator = new ClassGeneratingEntityInstantiator();
|
||||
private final ConversionService conversions;
|
||||
private final JdbcMappingContext context;
|
||||
private final JdbcEntityOperations template;
|
||||
private final JdbcPersistentProperty idProperty;
|
||||
|
||||
@java.beans.ConstructorProperties({ "entity", "conversions", "context", "template" })
|
||||
public EntityRowMapper(JdbcPersistentEntity<T> entity, ConversionService conversions, JdbcMappingContext context,
|
||||
JdbcEntityOperations template) {
|
||||
|
||||
this.entity = entity;
|
||||
this.conversions = conversions;
|
||||
this.context = context;
|
||||
this.template = template;
|
||||
|
||||
idProperty = entity.getRequiredIdProperty();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
@@ -58,11 +71,19 @@ class EntityRowMapper<T> implements RowMapper<T> {
|
||||
public T mapRow(ResultSet resultSet, int rowNumber) throws SQLException {
|
||||
|
||||
T result = createInstance(resultSet);
|
||||
PersistentPropertyAccessor accessor = entity.getPropertyAccessor(result);
|
||||
ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor(accessor, conversions);
|
||||
|
||||
ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor(entity.getPropertyAccessor(result),
|
||||
conversions);
|
||||
|
||||
Object id = readFrom(resultSet, idProperty, "");
|
||||
|
||||
for (JdbcPersistentProperty property : entity) {
|
||||
propertyAccessor.setProperty(property, readFrom(resultSet, property, ""));
|
||||
|
||||
if (Set.class.isAssignableFrom(property.getType())) {
|
||||
propertyAccessor.setProperty(property, template.findAllByProperty(id, property));
|
||||
} else {
|
||||
propertyAccessor.setProperty(property, readFrom(resultSet, property, ""));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -91,7 +112,8 @@ class EntityRowMapper<T> implements RowMapper<T> {
|
||||
String prefix = property.getName() + "_";
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
JdbcPersistentEntity<S> entity = (JdbcPersistentEntity<S>) context.getRequiredPersistentEntity(property.getType());
|
||||
JdbcPersistentEntity<S> entity = (JdbcPersistentEntity<S>) context
|
||||
.getRequiredPersistentEntity(property.getActualType());
|
||||
|
||||
if (readFrom(rs, entity.getRequiredIdProperty(), prefix) == null) {
|
||||
return null;
|
||||
@@ -109,13 +131,25 @@ class EntityRowMapper<T> implements RowMapper<T> {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor(staticName = "of")
|
||||
private static class ResultSetParameterValueProvider implements ParameterValueProvider<JdbcPersistentProperty> {
|
||||
|
||||
@NonNull private final ResultSet resultSet;
|
||||
@NonNull private final ConversionService conversionService;
|
||||
@NonNull private final String prefix;
|
||||
|
||||
@java.beans.ConstructorProperties({ "resultSet", "conversionService", "prefix" })
|
||||
private ResultSetParameterValueProvider(ResultSet resultSet, ConversionService conversionService, String prefix) {
|
||||
|
||||
this.resultSet = resultSet;
|
||||
this.conversionService = conversionService;
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
public static ResultSetParameterValueProvider of(ResultSet resultSet, ConversionService conversionService,
|
||||
String prefix) {
|
||||
return new ResultSetParameterValueProvider(resultSet, conversionService, prefix);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mapping.model.ParameterValueProvider#getParameterValue(org.springframework.data.mapping.PreferredConstructor.Parameter)
|
||||
|
||||
@@ -17,6 +17,8 @@ package org.springframework.data.jdbc.core;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty;
|
||||
|
||||
/**
|
||||
* Specifies a operations one can perform on a database, based on an <em>Domain Type</em>.
|
||||
*
|
||||
@@ -44,5 +46,8 @@ public interface JdbcEntityOperations {
|
||||
|
||||
<T> Iterable<T> findAll(Class<T> domainType);
|
||||
|
||||
<T> Iterable<T> findAllByProperty(Object id, JdbcPersistentProperty property);
|
||||
|
||||
<T> boolean existsById(Object id, Class<T> domainType);
|
||||
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
*/
|
||||
package org.springframework.data.jdbc.core;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.sql.Types;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -46,10 +44,10 @@ import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
|
||||
import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity;
|
||||
import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntityInformation;
|
||||
import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty;
|
||||
import org.springframework.data.jdbc.support.JdbcUtil;
|
||||
import org.springframework.data.mapping.PropertyHandler;
|
||||
import org.springframework.data.mapping.PropertyPath;
|
||||
import org.springframework.data.repository.core.EntityInformation;
|
||||
import org.springframework.jdbc.core.SqlParameterValue;
|
||||
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
|
||||
import org.springframework.jdbc.support.GeneratedKeyHolder;
|
||||
@@ -101,7 +99,8 @@ public class JdbcEntityTemplate implements JdbcEntityOperations {
|
||||
@Override
|
||||
public <T> void save(T instance, Class<T> domainType) {
|
||||
|
||||
JdbcPersistentEntityInformation<T, ?> entityInformation = context.getRequiredPersistentEntityInformation(domainType);
|
||||
JdbcPersistentEntityInformation<T, ?> entityInformation = context
|
||||
.getRequiredPersistentEntityInformation(domainType);
|
||||
|
||||
AggregateChange change = createChange(instance);
|
||||
|
||||
@@ -128,16 +127,17 @@ public class JdbcEntityTemplate implements JdbcEntityOperations {
|
||||
JdbcPersistentEntityInformation<T, ?> entityInformation = context
|
||||
.getRequiredPersistentEntityInformation(domainType);
|
||||
|
||||
Map<String, Object> propertyMap = getPropertyMap(instance, persistentEntity);
|
||||
MapSqlParameterSource parameterSource = getPropertyMap(instance, persistentEntity);
|
||||
|
||||
Object idValue = getIdValueOrNull(instance, persistentEntity);
|
||||
JdbcPersistentProperty idProperty = persistentEntity.getRequiredIdProperty();
|
||||
propertyMap.put(idProperty.getColumnName(), convert(idValue, idProperty.getColumnType()));
|
||||
parameterSource.addValue(idProperty.getColumnName(), convert(idValue, idProperty.getColumnType()),
|
||||
JdbcUtil.sqlTypeFor(idProperty.getColumnType()));
|
||||
|
||||
propertyMap.putAll(additionalParameters);
|
||||
additionalParameters.forEach(parameterSource::addValue);
|
||||
|
||||
operations.update(sql(domainType).getInsert(idValue == null, additionalParameters.keySet()),
|
||||
new MapSqlParameterSource(propertyMap), holder);
|
||||
operations.update(sql(domainType).getInsert(idValue == null, additionalParameters.keySet()), parameterSource,
|
||||
holder);
|
||||
|
||||
setIdFromJdbc(instance, holder, persistentEntity);
|
||||
|
||||
@@ -198,6 +198,17 @@ public class JdbcEntityTemplate implements JdbcEntityOperations {
|
||||
return operations.query(findAllInListSql, parameter, getEntityRowMapper(domainType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Iterable<T> findAllByProperty(Object id, JdbcPersistentProperty property) {
|
||||
|
||||
Class<?> actualType = property.getActualType();
|
||||
String findAllByProperty = sql(actualType).getFindAllByProperty(property.getReverseColumnName());
|
||||
|
||||
MapSqlParameterSource parameter = new MapSqlParameterSource(property.getReverseColumnName(), id);
|
||||
|
||||
return (Iterable<T>) operations.query(findAllByProperty, parameter, getEntityRowMapper(actualType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> void delete(S entity, Class<S> domainType) {
|
||||
|
||||
@@ -247,7 +258,7 @@ public class JdbcEntityTemplate implements JdbcEntityOperations {
|
||||
|
||||
}
|
||||
|
||||
void doDelete(Object id, Optional<Object> optionalEntity, Class<?> domainType) {
|
||||
void doDelete(Object id, Class<?> domainType) {
|
||||
|
||||
String deleteByIdSql = sql(domainType).getDeleteById();
|
||||
MapSqlParameterSource parameter = createIdParameterSource(id, domainType);
|
||||
@@ -281,20 +292,16 @@ public class JdbcEntityTemplate implements JdbcEntityOperations {
|
||||
convert(id, getRequiredPersistentEntity(domainType).getRequiredIdProperty().getColumnType()));
|
||||
}
|
||||
|
||||
private <S> Map<String, Object> getPropertyMap(final S instance, JdbcPersistentEntity<S> persistentEntity) {
|
||||
private <S> MapSqlParameterSource getPropertyMap(final S instance, JdbcPersistentEntity<S> persistentEntity) {
|
||||
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
MapSqlParameterSource parameters = new MapSqlParameterSource();
|
||||
|
||||
persistentEntity.doWithProperties((PropertyHandler<JdbcPersistentProperty>) property -> {
|
||||
if (!property.isEntity()) {
|
||||
Object value = persistentEntity.getPropertyAccessor(instance).getProperty(property);
|
||||
|
||||
Object convertedValue = convert(value, property.getColumnType());
|
||||
if (convertedValue instanceof BigInteger) {
|
||||
parameters.put(property.getColumnName(), new SqlParameterValue(Types.BIGINT, convertedValue));
|
||||
} else {
|
||||
parameters.put(property.getColumnName(), convertedValue);
|
||||
}
|
||||
parameters.addValue(property.getColumnName(), convertedValue, JdbcUtil.sqlTypeFor(property.getColumnType()));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -372,7 +379,7 @@ public class JdbcEntityTemplate implements JdbcEntityOperations {
|
||||
}
|
||||
|
||||
private <T> EntityRowMapper<T> getEntityRowMapper(Class<T> domainType) {
|
||||
return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), conversions, context);
|
||||
return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), conversions, context, this);
|
||||
}
|
||||
|
||||
<T> void doDeleteAll(Class<T> domainType, PropertyPath propertyPath) {
|
||||
@@ -381,4 +388,8 @@ public class JdbcEntityTemplate implements JdbcEntityOperations {
|
||||
.update(sql(propertyPath == null ? domainType : propertyPath.getOwningType().getType())
|
||||
.createDeleteAllSql(propertyPath));
|
||||
}
|
||||
|
||||
public NamedParameterJdbcOperations getOperations() {
|
||||
return operations;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.springframework.data.jdbc.core;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -58,8 +59,7 @@ class SqlGenerator {
|
||||
private final Lazy<String> deleteByListSql = Lazy.of(this::createDeleteByListSql);
|
||||
private final SqlGeneratorSource sqlGeneratorSource;
|
||||
|
||||
SqlGenerator(JdbcMappingContext context, JdbcPersistentEntity<?> entity,
|
||||
SqlGeneratorSource sqlGeneratorSource) {
|
||||
SqlGenerator(JdbcMappingContext context, JdbcPersistentEntity<?> entity, SqlGeneratorSource sqlGeneratorSource) {
|
||||
|
||||
this.context = context;
|
||||
this.entity = entity;
|
||||
@@ -88,6 +88,10 @@ class SqlGenerator {
|
||||
return findAllSql.get();
|
||||
}
|
||||
|
||||
String getFindAllByProperty(String columnName) {
|
||||
return String.format("%s WHERE %s = :%s", findAllSql.get(), columnName, columnName);
|
||||
}
|
||||
|
||||
String getExists() {
|
||||
return existsSql.get();
|
||||
}
|
||||
@@ -96,7 +100,7 @@ class SqlGenerator {
|
||||
return findOneSql.get();
|
||||
}
|
||||
|
||||
String getInsert(boolean excludeId, Set additionalColumns) {
|
||||
String getInsert(boolean excludeId, Set<String> additionalColumns) {
|
||||
return createInsertSql(excludeId, additionalColumns);
|
||||
}
|
||||
|
||||
@@ -124,39 +128,51 @@ class SqlGenerator {
|
||||
}
|
||||
|
||||
private SelectBuilder createSelectBuilder() {
|
||||
|
||||
SelectBuilder builder = new SelectBuilder(entity.getTableName());
|
||||
|
||||
for (JdbcPersistentProperty property : entity) {
|
||||
if (!property.isEntity()) {
|
||||
|
||||
builder.column(cb -> cb //
|
||||
.tableAlias(entity.getTableName()) //
|
||||
.column(property.getColumnName()) //
|
||||
.as(property.getColumnName()));
|
||||
}
|
||||
}
|
||||
|
||||
for (JdbcPersistentProperty property : entity) {
|
||||
if (property.isEntity()) {
|
||||
|
||||
JdbcPersistentEntity<?> refEntity = context.getRequiredPersistentEntity(property.getType());
|
||||
String joinAlias = property.getName();
|
||||
builder.join(jb -> jb.leftOuter().table(refEntity.getTableName()).as(joinAlias) //
|
||||
.where(entity.getTableName()).eq().column(entity.getTableName(), entity.getIdColumn()));
|
||||
|
||||
for (JdbcPersistentProperty refProperty : refEntity) {
|
||||
builder.column( //
|
||||
cb -> cb.tableAlias(joinAlias) //
|
||||
.column(refProperty.getColumnName()) //
|
||||
.as(joinAlias + "_" + refProperty.getColumnName()) //
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
addColumnsForSimpleProperties(builder);
|
||||
addColumnsAndJoinsForOneToOneReferences(builder);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private void addColumnsAndJoinsForOneToOneReferences(SelectBuilder builder) {
|
||||
|
||||
for (JdbcPersistentProperty property : entity) {
|
||||
if (!property.isEntity() || Collection.class.isAssignableFrom(property.getType())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
JdbcPersistentEntity<?> refEntity = context.getRequiredPersistentEntity(property.getActualType());
|
||||
String joinAlias = property.getName();
|
||||
builder.join(jb -> jb.leftOuter().table(refEntity.getTableName()).as(joinAlias) //
|
||||
.where(property.getReverseColumnName()).eq().column(entity.getTableName(), entity.getIdColumn()));
|
||||
|
||||
for (JdbcPersistentProperty refProperty : refEntity) {
|
||||
builder.column( //
|
||||
cb -> cb.tableAlias(joinAlias) //
|
||||
.column(refProperty.getColumnName()) //
|
||||
.as(joinAlias + "_" + refProperty.getColumnName()) //
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addColumnsForSimpleProperties(SelectBuilder builder) {
|
||||
|
||||
for (JdbcPersistentProperty property : entity) {
|
||||
|
||||
if (property.isEntity()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.column(cb -> cb //
|
||||
.tableAlias(entity.getTableName()) //
|
||||
.column(property.getColumnName()) //
|
||||
.as(property.getColumnName()));
|
||||
}
|
||||
}
|
||||
|
||||
private Stream<String> getColumnNameStream(String prefix) {
|
||||
|
||||
return StreamUtils.createStreamFromIterator(entity.iterator()) //
|
||||
@@ -191,11 +207,13 @@ class SqlGenerator {
|
||||
return String.format("select count(*) from %s", entity.getTableName());
|
||||
}
|
||||
|
||||
private String createInsertSql(boolean excludeId, Set additionalColumns) {
|
||||
private String createInsertSql(boolean excludeId, Set<String> additionalColumns) {
|
||||
|
||||
String insertTemplate = "insert into %s (%s) values (%s)";
|
||||
|
||||
List<String> propertyNamesForInsert = new ArrayList<>(excludeId ? nonIdPropertyNames : propertyNames);
|
||||
propertyNamesForInsert.addAll(additionalColumns);
|
||||
|
||||
String tableColumns = String.join(", ", propertyNamesForInsert);
|
||||
String parameterNames = propertyNamesForInsert.stream().collect(Collectors.joining(", :", ":", ""));
|
||||
|
||||
@@ -225,29 +243,31 @@ class SqlGenerator {
|
||||
|
||||
JdbcPersistentEntity<?> entityToDelete = context.getRequiredPersistentEntity(PropertyPaths.getLeafType(path));
|
||||
|
||||
String innerMostCondition = String.format("%s IS NOT NULL", entity.getTableName(), entity.getTableName(),
|
||||
entity.getIdColumn());
|
||||
JdbcPersistentEntity<?> owningEntity = context.getRequiredPersistentEntity(path.getOwningType());
|
||||
JdbcPersistentProperty property = owningEntity.getRequiredPersistentProperty(path.getSegment());
|
||||
|
||||
String innerMostCondition = String.format("%s IS NOT NULL", property.getReverseColumnName());
|
||||
|
||||
String condition = cascadeConditions(innerMostCondition, path.next());
|
||||
|
||||
return String.format("DELETE FROM %s WHERE %s", entityToDelete.getTableName(), condition, condition);
|
||||
|
||||
return String.format("DELETE FROM %s WHERE %s", entityToDelete.getTableName(), condition);
|
||||
}
|
||||
|
||||
private String createDeleteByListSql() {
|
||||
return String.format("doDelete from %s where %s in (:ids)", entity.getTableName(), entity.getIdColumn());
|
||||
return String.format("DELETE FROM %s WHERE %s IN (:ids)", entity.getTableName(), entity.getIdColumn());
|
||||
}
|
||||
|
||||
String createDeleteByPath(PropertyPath path) {
|
||||
|
||||
JdbcPersistentEntity<?> entityToDelete = context.getRequiredPersistentEntity(PropertyPaths.getLeafType(path));
|
||||
JdbcPersistentEntity<?> owningEntity = context.getRequiredPersistentEntity(path.getOwningType());
|
||||
JdbcPersistentProperty property = owningEntity.getRequiredPersistentProperty(path.getSegment());
|
||||
|
||||
String innerMostCondition = String.format("%s = :rootId", entity.getTableName());
|
||||
String innerMostCondition = String.format("%s = :rootId", property.getReverseColumnName());
|
||||
|
||||
String condition = cascadeConditions(innerMostCondition, path.next());
|
||||
|
||||
return String.format("DELETE FROM %s WHERE %s", entityToDelete.getTableName(), condition);
|
||||
|
||||
}
|
||||
|
||||
private String cascadeConditions(String innerCondition, PropertyPath path) {
|
||||
@@ -261,10 +281,10 @@ class SqlGenerator {
|
||||
|
||||
Assert.notNull(property, "could not find property for path " + path.getSegment() + " in " + entity);
|
||||
|
||||
String tableName = entity.getTableName();
|
||||
String idColumn = entity.getIdColumn();
|
||||
|
||||
return String.format("%s IN (SELECT %s FROM %s WHERE %s)", tableName, idColumn, tableName, innerCondition);
|
||||
|
||||
return String.format("%s IN (SELECT %s FROM %s WHERE %s)", //
|
||||
property.getReverseColumnName(), //
|
||||
entity.getIdColumn(), //
|
||||
entity.getTableName(), innerCondition //
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.jdbc.core.conversion;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.data.jdbc.core.conversion.DbAction.Insert;
|
||||
@@ -26,6 +27,7 @@ import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.util.StreamUtils;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Converts an entity that is about to be saved into {@link DbAction}s inside a {@link AggregateChange} that need to be
|
||||
@@ -46,8 +48,6 @@ public class JdbcEntityWriter extends JdbcEntityWriterSupport {
|
||||
|
||||
private void write(Object o, AggregateChange aggregateChange, DbAction dependingOn) {
|
||||
|
||||
JdbcPersistentEntity<?> persistentEntity = context.getRequiredPersistentEntity(aggregateChange.getEntityType());
|
||||
|
||||
JdbcPersistentEntityInformation<Object, ?> entityInformation = context
|
||||
.getRequiredPersistentEntityInformation((Class<Object>) o.getClass());
|
||||
|
||||
@@ -70,22 +70,34 @@ public class JdbcEntityWriter extends JdbcEntityWriterSupport {
|
||||
|
||||
private void saveReferencedEntities(Object o, AggregateChange aggregateChange, DbAction dependingOn) {
|
||||
|
||||
DbAction action = saveAction(o, dependingOn);
|
||||
aggregateChange.addAction(action);
|
||||
saveActions(o, dependingOn).forEach(a -> {
|
||||
|
||||
aggregateChange.addAction(a);
|
||||
referencedEntities(o).forEach(e -> saveReferencedEntities(e, aggregateChange, a));
|
||||
});
|
||||
|
||||
referencedEntities(o).forEach(e -> saveReferencedEntities(e, aggregateChange, action));
|
||||
}
|
||||
|
||||
private <T> DbAction saveAction(T t, DbAction dependingOn) {
|
||||
private <T> Stream<DbAction> saveActions(T t, DbAction dependingOn) {
|
||||
|
||||
if (Collection.class.isAssignableFrom(ClassUtils.getUserClass(t))) {
|
||||
return collectionSaveAction((Collection) t, dependingOn);
|
||||
}
|
||||
|
||||
return Stream.of(singleSaveAction(t, dependingOn));
|
||||
}
|
||||
|
||||
private Stream<DbAction> collectionSaveAction(Collection collection, DbAction dependingOn) {
|
||||
|
||||
return collection.stream().map(e -> singleSaveAction(e, dependingOn));
|
||||
}
|
||||
|
||||
private <T> DbAction singleSaveAction(T t, DbAction dependingOn) {
|
||||
|
||||
JdbcPersistentEntityInformation<T, ?> entityInformation = context
|
||||
.getRequiredPersistentEntityInformation((Class<T>) t.getClass());
|
||||
.getRequiredPersistentEntityInformation((Class<T>) ClassUtils.getUserClass(t));
|
||||
|
||||
if (entityInformation.isNew(t)) {
|
||||
return DbAction.insert(t, dependingOn);
|
||||
} else {
|
||||
return DbAction.update(t, dependingOn);
|
||||
}
|
||||
return entityInformation.isNew(t) ? DbAction.insert(t, dependingOn) : DbAction.update(t, dependingOn);
|
||||
}
|
||||
|
||||
private void insertReferencedEntities(Object o, AggregateChange aggregateChange, DbAction dependingOn) {
|
||||
@@ -105,14 +117,34 @@ public class JdbcEntityWriter extends JdbcEntityWriterSupport {
|
||||
|
||||
private Stream<?> referencedEntity(JdbcPersistentProperty p, PersistentPropertyAccessor propertyAccessor) {
|
||||
|
||||
Class<?> type = p.getActualType();
|
||||
Class<?> actualType = p.getActualType();
|
||||
JdbcPersistentEntity<?> persistentEntity = context //
|
||||
.getPersistentEntity(type);
|
||||
.getPersistentEntity(actualType);
|
||||
|
||||
if (persistentEntity == null) {
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
Class<?> type = p.getType();
|
||||
if (Collection.class.isAssignableFrom(type))
|
||||
return collectionPropertyAsStream(p, propertyAccessor);
|
||||
|
||||
return singlePropertyAsStream(p, propertyAccessor);
|
||||
}
|
||||
|
||||
private Stream<Object> collectionPropertyAsStream(JdbcPersistentProperty p,
|
||||
PersistentPropertyAccessor propertyAccessor) {
|
||||
|
||||
Object property = propertyAccessor.getProperty(p);
|
||||
if (property == null) {
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
return ((Collection<Object>) property).stream();
|
||||
}
|
||||
|
||||
private Stream<Object> singlePropertyAsStream(JdbcPersistentProperty p, PersistentPropertyAccessor propertyAccessor) {
|
||||
|
||||
Object property = propertyAccessor.getProperty(p);
|
||||
if (property == null) {
|
||||
return Stream.empty();
|
||||
|
||||
@@ -92,9 +92,19 @@ public class BasicJdbcPersistentProperty extends AnnotationBasedPersistentProper
|
||||
@Override
|
||||
public Class getColumnType() {
|
||||
|
||||
Class columnType = columnTypeIfEntity(getType());
|
||||
Class columnType = columnTypeIfEntity(getActualType());
|
||||
|
||||
return columnType == null ? columnTypeForNonEntity(getType()) : columnType;
|
||||
return columnType == null ? columnTypeForNonEntity(getActualType()) : columnType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JdbcPersistentEntity<?> getOwner() {
|
||||
return (JdbcPersistentEntity<?>) super.getOwner();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getReverseColumnName() {
|
||||
return getOwner().getTableName();
|
||||
}
|
||||
|
||||
private Class columnTypeIfEntity(Class type) {
|
||||
|
||||
@@ -39,4 +39,9 @@ public interface JdbcPersistentProperty extends PersistentProperty<JdbcPersisten
|
||||
* @return a {@link Class} that is suitable for usage with JDBC drivers
|
||||
*/
|
||||
Class<?> getColumnType();
|
||||
|
||||
@Override
|
||||
JdbcPersistentEntity<?> getOwner();
|
||||
|
||||
String getReverseColumnName();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.support;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.sql.Date;
|
||||
import java.sql.Time;
|
||||
import java.sql.Timestamp;
|
||||
import java.sql.Types;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.jdbc.support.JdbcUtils;
|
||||
|
||||
/**
|
||||
* Contains methods dealing with the quirks of JDBC, independent of any Entity, Aggregate or Repository abstraction.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
@UtilityClass
|
||||
public class JdbcUtil {
|
||||
|
||||
private static final Map<Class, Integer> sqlTypeMappings = new HashMap<>();
|
||||
|
||||
static {
|
||||
|
||||
sqlTypeMappings.put(String.class, Types.VARCHAR);
|
||||
sqlTypeMappings.put(BigInteger.class, Types.BIGINT);
|
||||
sqlTypeMappings.put(BigDecimal.class, Types.NUMERIC);
|
||||
sqlTypeMappings.put(Byte.class, Types.TINYINT);
|
||||
sqlTypeMappings.put(byte.class, Types.TINYINT);
|
||||
sqlTypeMappings.put(Short.class, Types.SMALLINT);
|
||||
sqlTypeMappings.put(short.class, Types.SMALLINT);
|
||||
sqlTypeMappings.put(Integer.class, Types.INTEGER);
|
||||
sqlTypeMappings.put(int.class, Types.INTEGER);
|
||||
sqlTypeMappings.put(Long.class, Types.BIGINT);
|
||||
sqlTypeMappings.put(long.class, Types.BIGINT);
|
||||
sqlTypeMappings.put(Double.class, Types.DOUBLE);
|
||||
sqlTypeMappings.put(double.class, Types.DOUBLE);
|
||||
sqlTypeMappings.put(Float.class, Types.REAL);
|
||||
sqlTypeMappings.put(float.class, Types.REAL);
|
||||
sqlTypeMappings.put(Boolean.class, Types.BIT);
|
||||
sqlTypeMappings.put(boolean.class, Types.BIT);
|
||||
sqlTypeMappings.put(byte[].class, Types.VARBINARY);
|
||||
sqlTypeMappings.put(Date.class, Types.DATE);
|
||||
sqlTypeMappings.put(Time.class, Types.TIME);
|
||||
sqlTypeMappings.put(Timestamp.class, Types.TIMESTAMP);
|
||||
}
|
||||
|
||||
public static int sqlTypeFor(Class type) {
|
||||
return sqlTypeMappings.keySet().stream() //
|
||||
.filter(k -> k.isAssignableFrom(type)) //
|
||||
.findFirst() //
|
||||
.map(sqlTypeMappings::get) //
|
||||
.orElse(JdbcUtils.TYPE_UNKNOWN);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* 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.core;
|
||||
|
||||
import static java.util.Arrays.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.naming.OperationNotSupportedException;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.jdbc.mapping.model.DefaultNamingStrategy;
|
||||
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
|
||||
import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity;
|
||||
import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Tests the extraction of entities from a {@link ResultSet} by the {@link EntityRowMapper}.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
public class EntityRowMapperUnitTests {
|
||||
|
||||
@Test // DATAJDBC-113
|
||||
public void simpleEntitiesGetProperlyExtracted() throws SQLException {
|
||||
|
||||
ResultSet rs = mockResultSet(asList("id", "name"), //
|
||||
23L, "alpha");
|
||||
rs.next();
|
||||
|
||||
Trivial extracted = createRowMapper(Trivial.class).mapRow(rs, 1);
|
||||
|
||||
assertThat(extracted) //
|
||||
.isNotNull() //
|
||||
.extracting(e -> e.id, e -> e.name) //
|
||||
.containsExactly(23L, "alpha");
|
||||
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-113
|
||||
public void simpleOneToOneGetsProperlyExtracted() throws SQLException {
|
||||
|
||||
ResultSet rs = mockResultSet(asList("id", "name", "child_id", "child_name"), //
|
||||
23L, "alpha", 42L, "beta");
|
||||
rs.next();
|
||||
|
||||
OneToOne extracted = createRowMapper(OneToOne.class).mapRow(rs, 1);
|
||||
|
||||
assertThat(extracted) //
|
||||
.isNotNull() //
|
||||
.extracting(e -> e.id, e -> e.name, e -> e.child.id, e -> e.child.name) //
|
||||
.containsExactly(23L, "alpha", 42L, "beta");
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-113
|
||||
public void collectionReferenceGetsLoadedWithAdditionalSelect() throws SQLException {
|
||||
|
||||
ResultSet rs = mockResultSet(asList("id", "name"), //
|
||||
23L, "alpha");
|
||||
rs.next();
|
||||
|
||||
OneToSet extracted = createRowMapper(OneToSet.class).mapRow(rs, 1);
|
||||
|
||||
assertThat(extracted) //
|
||||
.isNotNull() //
|
||||
.extracting(e -> e.id, e -> e.name, e -> e.children.size()) //
|
||||
.containsExactly(23L, "alpha", 2);
|
||||
}
|
||||
|
||||
private <T> EntityRowMapper<T> createRowMapper(Class<T> type) {
|
||||
|
||||
JdbcMappingContext context = new JdbcMappingContext(new DefaultNamingStrategy());
|
||||
JdbcEntityOperations template = mock(JdbcEntityOperations.class);
|
||||
|
||||
doReturn(new HashSet<>(asList(new Trivial(), new Trivial()))).when(template).findAllByProperty(eq(23L),
|
||||
any(JdbcPersistentProperty.class));
|
||||
|
||||
return new EntityRowMapper<>((JdbcPersistentEntity<T>) context.getRequiredPersistentEntity(type),
|
||||
new DefaultConversionService(), context, template);
|
||||
}
|
||||
|
||||
private static ResultSet mockResultSet(List<String> columns, Object... values) {
|
||||
|
||||
Assert.isTrue( //
|
||||
values.length % columns.size() == 0, //
|
||||
String //
|
||||
.format( //
|
||||
"Number of values [%d] must be a multiple of the number of columns [%d]", //
|
||||
values.length, //
|
||||
columns.size() //
|
||||
) //
|
||||
);
|
||||
|
||||
List<Map<String, Object>> result = convertValues(columns, values);
|
||||
|
||||
return mock(ResultSet.class, new ResultSetAnswer(result));
|
||||
}
|
||||
|
||||
private static List<Map<String, Object>> convertValues(List<String> columns, Object[] values) {
|
||||
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
|
||||
int index = 0;
|
||||
while (index < values.length) {
|
||||
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
result.add(row);
|
||||
for (String column : columns) {
|
||||
|
||||
row.put(column, values[index]);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static class ResultSetAnswer implements Answer {
|
||||
|
||||
private final List<Map<String, Object>> values;
|
||||
private int index = -1;
|
||||
|
||||
public ResultSetAnswer(List<Map<String, Object>> values) {
|
||||
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
|
||||
switch (invocation.getMethod().getName()) {
|
||||
case "next":
|
||||
}
|
||||
|
||||
if (invocation.getMethod().getName().equals("next"))
|
||||
return next();
|
||||
|
||||
if (invocation.getMethod().getName().equals("getObject"))
|
||||
return getObject(invocation.getArgument(0));
|
||||
|
||||
if (invocation.getMethod().getName().equals("isAfterLast"))
|
||||
return isAfterLast();
|
||||
|
||||
if (invocation.getMethod().getName().equals("isBeforeFirst"))
|
||||
return isBeforeFirst();
|
||||
|
||||
if (invocation.getMethod().getName().equals("getRow"))
|
||||
return isAfterLast() || isBeforeFirst() ? 0 : index + 1;
|
||||
|
||||
if (invocation.getMethod().getName().equals("toString"))
|
||||
return this.toString();
|
||||
|
||||
throw new OperationNotSupportedException(invocation.getMethod().getName());
|
||||
}
|
||||
|
||||
private boolean isAfterLast() {
|
||||
return index >= values.size() && !values.isEmpty();
|
||||
}
|
||||
|
||||
private boolean isBeforeFirst() {
|
||||
return index < 0 && !values.isEmpty();
|
||||
}
|
||||
|
||||
private Object getObject(String column) {
|
||||
|
||||
return values.get(index).get(column);
|
||||
}
|
||||
|
||||
private boolean next() {
|
||||
|
||||
index++;
|
||||
return index < values.size();
|
||||
}
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static class Trivial {
|
||||
|
||||
@Id Long id;
|
||||
String name;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static class OneToOne {
|
||||
|
||||
@Id Long id;
|
||||
String name;
|
||||
Trivial child;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static class OneToSet {
|
||||
|
||||
@Id Long id;
|
||||
String name;
|
||||
Set<Trivial> children;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -162,6 +162,17 @@ public class SqlGeneratorFixedNamingStrategyUnitTests {
|
||||
+ "WHERE FixedCustomSchema.FixedCustomTablePrefix_DummyEntity IS NOT NULL)");
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-113
|
||||
public void deleteByList() {
|
||||
|
||||
SqlGenerator sqlGenerator = configureSqlGenerator(fixedCustomTablePrefixStrategy);
|
||||
|
||||
String sql = sqlGenerator.getDeleteByList();
|
||||
|
||||
assertThat(sql).isEqualTo("DELETE FROM FixedCustomSchema.FixedCustomTablePrefix_DummyEntity WHERE FixedCustomPropertyPrefix_id IN (:ids)");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Plug in a custom {@link NamingStrategy} for this test case.
|
||||
*
|
||||
|
||||
@@ -17,6 +17,8 @@ package org.springframework.data.jdbc.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.assertj.core.api.SoftAssertions;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -57,7 +59,9 @@ public class SqlGeneratorUnitTests {
|
||||
.contains("DummyEntity.id AS id,") //
|
||||
.contains("DummyEntity.name AS name,") //
|
||||
.contains("ref.l1id AS ref_l1id") //
|
||||
.contains("ref.content AS ref_content").contains(" FROM DummyEntity");
|
||||
.contains("ref.content AS ref_content").contains(" FROM DummyEntity") //
|
||||
// 1-N relationships do not get loaded via join
|
||||
.doesNotContain("Element AS elements");
|
||||
softAssertions.assertAll();
|
||||
}
|
||||
|
||||
@@ -109,6 +113,7 @@ public class SqlGeneratorUnitTests {
|
||||
@Id Long id;
|
||||
String name;
|
||||
ReferencedEntity ref;
|
||||
Set<Element> elements;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@@ -125,4 +130,9 @@ public class SqlGeneratorUnitTests {
|
||||
@Id Long l2id;
|
||||
String something;
|
||||
}
|
||||
|
||||
static class Element {
|
||||
@Id Long id;
|
||||
String content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,10 @@ package org.springframework.data.jdbc.core.conversion;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -43,61 +46,159 @@ public class JdbcEntityWriterUnitTests {
|
||||
@Test // DATAJDBC-112
|
||||
public void newEntityGetsConvertedToOneInsert() {
|
||||
|
||||
SomeEntity entity = new SomeEntity(null);
|
||||
AggregateChange<SomeEntity> aggregateChange = new AggregateChange(Kind.SAVE, SomeEntity.class, entity);
|
||||
SingleReferenceEntity entity = new SingleReferenceEntity(null);
|
||||
AggregateChange<SingleReferenceEntity> aggregateChange = //
|
||||
new AggregateChange(Kind.SAVE, SingleReferenceEntity.class, entity);
|
||||
|
||||
converter.write(entity, aggregateChange);
|
||||
|
||||
assertThat(aggregateChange.getActions()).extracting(DbAction::getClass, DbAction::getEntityType) //
|
||||
assertThat(aggregateChange.getActions()) //
|
||||
.extracting(DbAction::getClass, DbAction::getEntityType) //
|
||||
.containsExactly( //
|
||||
tuple(Insert.class, SomeEntity.class) //
|
||||
tuple(Insert.class, SingleReferenceEntity.class) //
|
||||
);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-112
|
||||
public void existingEntityGetsConvertedToUpdate() {
|
||||
|
||||
SomeEntity entity = new SomeEntity(23L);
|
||||
|
||||
AggregateChange<SomeEntity> aggregateChange = new AggregateChange(Kind.SAVE, SomeEntity.class, entity);
|
||||
SingleReferenceEntity entity = new SingleReferenceEntity(23L);
|
||||
AggregateChange<SingleReferenceEntity> aggregateChange = //
|
||||
new AggregateChange(Kind.SAVE, SingleReferenceEntity.class, entity);
|
||||
|
||||
converter.write(entity, aggregateChange);
|
||||
|
||||
assertThat(aggregateChange.getActions()).extracting(DbAction::getClass, DbAction::getEntityType) //
|
||||
assertThat(aggregateChange.getActions()) //
|
||||
.extracting(DbAction::getClass, DbAction::getEntityType) //
|
||||
.containsExactly( //
|
||||
tuple(Delete.class, OtherEntity.class), //
|
||||
tuple(Update.class, SomeEntity.class) //
|
||||
tuple(Delete.class, Element.class), //
|
||||
tuple(Update.class, SingleReferenceEntity.class) //
|
||||
);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-112
|
||||
public void referenceTriggersDeletePlusInsert() {
|
||||
|
||||
SomeEntity entity = new SomeEntity(23L);
|
||||
entity.setOther(new OtherEntity(null));
|
||||
SingleReferenceEntity entity = new SingleReferenceEntity(23L);
|
||||
entity.other = new Element(null);
|
||||
|
||||
AggregateChange<SomeEntity> aggregateChange = new AggregateChange(Kind.SAVE, SomeEntity.class, entity);
|
||||
AggregateChange<SingleReferenceEntity> aggregateChange = new AggregateChange(Kind.SAVE, SingleReferenceEntity.class,
|
||||
entity);
|
||||
|
||||
converter.write(entity, aggregateChange);
|
||||
|
||||
assertThat(aggregateChange.getActions()) //
|
||||
.extracting(DbAction::getClass, DbAction::getEntityType) //
|
||||
.containsExactly( //
|
||||
tuple(Delete.class, Element.class), //
|
||||
tuple(Update.class, SingleReferenceEntity.class), //
|
||||
tuple(Insert.class, Element.class) //
|
||||
);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-113
|
||||
public void newEntityWithEmptySetResultsInSingleInsert() {
|
||||
|
||||
SetContainer entity = new SetContainer(null);
|
||||
AggregateChange<SingleReferenceEntity> aggregateChange = new AggregateChange(Kind.SAVE, SetContainer.class, entity);
|
||||
|
||||
converter.write(entity, aggregateChange);
|
||||
|
||||
assertThat(aggregateChange.getActions()).extracting(DbAction::getClass, DbAction::getEntityType) //
|
||||
.containsExactly( //
|
||||
tuple(Delete.class, OtherEntity.class), //
|
||||
tuple(Update.class, SomeEntity.class), //
|
||||
tuple(Insert.class, OtherEntity.class) //
|
||||
tuple(Insert.class, SetContainer.class));
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-113
|
||||
public void newEntityWithSetResultsInAdditionalInsertPerElement() {
|
||||
|
||||
SetContainer entity = new SetContainer(null);
|
||||
entity.elements.add(new Element(null));
|
||||
entity.elements.add(new Element(null));
|
||||
|
||||
AggregateChange<SingleReferenceEntity> aggregateChange = new AggregateChange(Kind.SAVE, SetContainer.class, entity);
|
||||
converter.write(entity, aggregateChange);
|
||||
|
||||
assertThat(aggregateChange.getActions()).extracting(DbAction::getClass, DbAction::getEntityType) //
|
||||
.containsExactly( //
|
||||
tuple(Insert.class, SetContainer.class), //
|
||||
tuple(Insert.class, Element.class), //
|
||||
tuple(Insert.class, Element.class) //
|
||||
);
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class SomeEntity {
|
||||
@Test // DATAJDBC-113
|
||||
public void cascadingReferencesTriggerCascadingActions() {
|
||||
|
||||
CascadingReferenceEntity entity = new CascadingReferenceEntity(null);
|
||||
|
||||
entity.other.add(createMiddleElement( //
|
||||
new Element(null), //
|
||||
new Element(null)) //
|
||||
);
|
||||
|
||||
entity.other.add(createMiddleElement( //
|
||||
new Element(null), //
|
||||
new Element(null)) //
|
||||
);
|
||||
|
||||
AggregateChange<SingleReferenceEntity> aggregateChange = new AggregateChange(Kind.SAVE, SetContainer.class, entity);
|
||||
|
||||
converter.write(entity, aggregateChange);
|
||||
|
||||
assertThat(aggregateChange.getActions()).extracting(DbAction::getClass, DbAction::getEntityType) //
|
||||
.containsExactly( //
|
||||
tuple(Insert.class, CascadingReferenceEntity.class), //
|
||||
tuple(Insert.class, CascadingReferenceMiddleElement.class), //
|
||||
tuple(Insert.class, Element.class), //
|
||||
tuple(Insert.class, Element.class), //
|
||||
tuple(Insert.class, CascadingReferenceMiddleElement.class), //
|
||||
tuple(Insert.class, Element.class), //
|
||||
tuple(Insert.class, Element.class) //
|
||||
);
|
||||
}
|
||||
|
||||
private CascadingReferenceMiddleElement createMiddleElement(Element first, Element second) {
|
||||
|
||||
CascadingReferenceMiddleElement middleElement1 = new CascadingReferenceMiddleElement(null);
|
||||
middleElement1.element.add(first);
|
||||
middleElement1.element.add(second);
|
||||
return middleElement1;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static class SingleReferenceEntity {
|
||||
|
||||
@Id final Long id;
|
||||
OtherEntity other;
|
||||
Element other;
|
||||
// should not trigger own Dbaction
|
||||
String name;
|
||||
}
|
||||
|
||||
@Data
|
||||
private class OtherEntity {
|
||||
@RequiredArgsConstructor
|
||||
private static class CascadingReferenceMiddleElement {
|
||||
|
||||
@Id final Long id;
|
||||
final Set<Element> element = new HashSet<>();
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
private static class CascadingReferenceEntity {
|
||||
|
||||
@Id final Long id;
|
||||
final Set<CascadingReferenceMiddleElement> other = new HashSet<>();
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
private static class SetContainer {
|
||||
|
||||
@Id final Long id;
|
||||
Set<Element> elements = new HashSet<>();
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
private static class Element {
|
||||
@Id final Long id;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* 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 org.assertj.core.api.Assertions.*;
|
||||
|
||||
import junit.framework.AssertionFailedError;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.repository.support.JdbcRepositoryFactory;
|
||||
import org.springframework.data.jdbc.testing.TestConfiguration;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.rules.SpringClassRule;
|
||||
import org.springframework.test.context.junit4.rules.SpringMethodRule;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Very simple use cases for creation and usage of JdbcRepositories.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
@ContextConfiguration
|
||||
@Transactional
|
||||
public class JdbcRepositoryWithCollectionsIntegrationTests {
|
||||
|
||||
@Configuration
|
||||
@Import(TestConfiguration.class)
|
||||
static class Config {
|
||||
|
||||
@Autowired JdbcRepositoryFactory factory;
|
||||
|
||||
@Bean
|
||||
Class<?> testClass() {
|
||||
return JdbcRepositoryWithCollectionsIntegrationTests.class;
|
||||
}
|
||||
|
||||
@Bean
|
||||
DummyEntityRepository dummyEntityRepository() {
|
||||
return factory.getRepository(DummyEntityRepository.class);
|
||||
}
|
||||
}
|
||||
|
||||
@ClassRule public static final SpringClassRule classRule = new SpringClassRule();
|
||||
@Rule public SpringMethodRule methodRule = new SpringMethodRule();
|
||||
|
||||
@Autowired NamedParameterJdbcTemplate template;
|
||||
@Autowired DummyEntityRepository repository;
|
||||
|
||||
@Test // DATAJDBC-113
|
||||
public void saveAndLoadEmptySet() {
|
||||
|
||||
DummyEntity entity = repository.save(createDummyEntity());
|
||||
|
||||
assertThat(entity.id).isNotNull();
|
||||
|
||||
DummyEntity reloaded = repository.findById(entity.id).orElseThrow(AssertionFailedError::new);
|
||||
|
||||
assertThat(reloaded.content) //
|
||||
.isNotNull() //
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-113
|
||||
public void saveAndLoadNonEmptySet() {
|
||||
|
||||
Element element1 = new Element();
|
||||
Element element2 = new Element();
|
||||
|
||||
DummyEntity entity = createDummyEntity();
|
||||
entity.content.add(element1);
|
||||
entity.content.add(element2);
|
||||
|
||||
entity = repository.save(entity);
|
||||
|
||||
assertThat(entity.id).isNotNull();
|
||||
assertThat(entity.content).allMatch(element -> element.id != null);
|
||||
|
||||
DummyEntity reloaded = repository.findById(entity.id).orElseThrow(AssertionFailedError::new);
|
||||
|
||||
assertThat(reloaded.content) //
|
||||
.isNotNull() //
|
||||
.extracting(e -> e.id) //
|
||||
.containsExactlyInAnyOrder(element1.id, element2.id);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-113
|
||||
public void findAllLoadsCollection() {
|
||||
|
||||
Element element1 = new Element();
|
||||
Element element2 = new Element();
|
||||
|
||||
DummyEntity entity = createDummyEntity();
|
||||
entity.content.add(element1);
|
||||
entity.content.add(element2);
|
||||
|
||||
entity = repository.save(entity);
|
||||
|
||||
assertThat(entity.id).isNotNull();
|
||||
assertThat(entity.content).allMatch(element -> element.id != null);
|
||||
|
||||
Iterable<DummyEntity> reloaded = repository.findAll();
|
||||
|
||||
assertThat(reloaded) //
|
||||
.extracting(e -> e.id, e -> e.content.size()) //
|
||||
.containsExactly(tuple(entity.id, entity.content.size()));
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-113
|
||||
public void updateSet() {
|
||||
|
||||
Element element1 = createElement("one");
|
||||
Element element2 = createElement("two");
|
||||
Element element3 = createElement("three");
|
||||
|
||||
DummyEntity entity = createDummyEntity();
|
||||
entity.content.add(element1);
|
||||
entity.content.add(element2);
|
||||
|
||||
entity = repository.save(entity);
|
||||
|
||||
entity.content.remove(element1);
|
||||
element2.content = "two changed";
|
||||
entity.content.add(element3);
|
||||
|
||||
entity = repository.save(entity);
|
||||
|
||||
assertThat(entity.id).isNotNull();
|
||||
assertThat(entity.content).allMatch(element -> element.id != null);
|
||||
|
||||
DummyEntity reloaded = repository.findById(entity.id).orElseThrow(AssertionFailedError::new);
|
||||
|
||||
// the elements got properly updated and reloaded
|
||||
assertThat(reloaded.content) //
|
||||
.isNotNull() //
|
||||
.extracting(e -> e.id, e -> e.content) //
|
||||
.containsExactlyInAnyOrder( //
|
||||
tuple(element2.id, "two changed"), //
|
||||
tuple(element3.id, "three") //
|
||||
);
|
||||
|
||||
Long count = template.queryForObject("select count(1) from Element", new HashMap<>(), Long.class);
|
||||
assertThat(count).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-113
|
||||
public void deletingWithSet() {
|
||||
|
||||
Element element1 = createElement("one");
|
||||
Element element2 = createElement("two");
|
||||
|
||||
DummyEntity entity = createDummyEntity();
|
||||
entity.content.add(element1);
|
||||
entity.content.add(element2);
|
||||
|
||||
entity = repository.save(entity);
|
||||
|
||||
repository.deleteById(entity.id);
|
||||
|
||||
assertThat(repository.findById(entity.id)).isEmpty();
|
||||
|
||||
Long count = template.queryForObject("select count(1) from Element", new HashMap<>(), Long.class);
|
||||
assertThat(count).isEqualTo(0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private Element createElement(String content) {
|
||||
|
||||
Element element = new Element();
|
||||
element.content = content;
|
||||
return element;
|
||||
}
|
||||
|
||||
private static DummyEntity createDummyEntity() {
|
||||
|
||||
DummyEntity entity = new DummyEntity();
|
||||
entity.setName("Entity Name");
|
||||
return entity;
|
||||
}
|
||||
|
||||
interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {}
|
||||
|
||||
@Data
|
||||
static class DummyEntity {
|
||||
|
||||
@Id private Long id;
|
||||
String name;
|
||||
Set<Element> content = new HashSet<>();
|
||||
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static class Element {
|
||||
|
||||
@Id private Long id;
|
||||
String content;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE TABLE dummyentity ( id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, NAME VARCHAR(100));
|
||||
CREATE TABLE element (id BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, content VARCHAR(100), dummyentity BIGINT);
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE TABLE dummyentity ( id BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100));
|
||||
CREATE TABLE element (id BIGINT AUTO_INCREMENT PRIMARY KEY, content VARCHAR(100), dummyentity BIGINT);
|
||||
@@ -0,0 +1,4 @@
|
||||
DROP TABLE element;
|
||||
DROP TABLE dummyentity;
|
||||
CREATE TABLE dummyentity ( id SERIAL PRIMARY KEY, NAME VARCHAR(100));
|
||||
CREATE TABLE element (id SERIAL PRIMARY KEY, content VARCHAR(100), dummyentity BIGINT);
|
||||
Reference in New Issue
Block a user