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 e6aadeeb..9c17d7d1 100644 --- a/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java +++ b/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java @@ -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 void interpret(Delete 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()); } 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 fcfa9211..b3082529 100644 --- a/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java +++ b/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java @@ -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 implements RowMapper { private final JdbcPersistentEntity 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 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 implements RowMapper { 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 implements RowMapper { String prefix = property.getName() + "_"; @SuppressWarnings("unchecked") - JdbcPersistentEntity entity = (JdbcPersistentEntity) context.getRequiredPersistentEntity(property.getType()); + JdbcPersistentEntity entity = (JdbcPersistentEntity) context + .getRequiredPersistentEntity(property.getActualType()); if (readFrom(rs, entity.getRequiredIdProperty(), prefix) == null) { return null; @@ -109,13 +131,25 @@ class EntityRowMapper implements RowMapper { return instance; } - @RequiredArgsConstructor(staticName = "of") private static class ResultSetParameterValueProvider implements ParameterValueProvider { @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) diff --git a/src/main/java/org/springframework/data/jdbc/core/JdbcEntityOperations.java b/src/main/java/org/springframework/data/jdbc/core/JdbcEntityOperations.java index 5c8dcda7..7d74f424 100644 --- a/src/main/java/org/springframework/data/jdbc/core/JdbcEntityOperations.java +++ b/src/main/java/org/springframework/data/jdbc/core/JdbcEntityOperations.java @@ -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 Domain Type. * @@ -44,5 +46,8 @@ public interface JdbcEntityOperations { Iterable findAll(Class domainType); + Iterable findAllByProperty(Object id, JdbcPersistentProperty property); + boolean existsById(Object id, Class domainType); + } 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 6e596c0b..b17f10ad 100644 --- a/src/main/java/org/springframework/data/jdbc/core/JdbcEntityTemplate.java +++ b/src/main/java/org/springframework/data/jdbc/core/JdbcEntityTemplate.java @@ -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 void save(T instance, Class domainType) { - JdbcPersistentEntityInformation entityInformation = context.getRequiredPersistentEntityInformation(domainType); + JdbcPersistentEntityInformation entityInformation = context + .getRequiredPersistentEntityInformation(domainType); AggregateChange change = createChange(instance); @@ -128,16 +127,17 @@ public class JdbcEntityTemplate implements JdbcEntityOperations { JdbcPersistentEntityInformation entityInformation = context .getRequiredPersistentEntityInformation(domainType); - Map 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 Iterable 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) operations.query(findAllByProperty, parameter, getEntityRowMapper(actualType)); + } + @Override public void delete(S entity, Class domainType) { @@ -247,7 +258,7 @@ public class JdbcEntityTemplate implements JdbcEntityOperations { } - void doDelete(Object id, Optional 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 Map getPropertyMap(final S instance, JdbcPersistentEntity persistentEntity) { + private MapSqlParameterSource getPropertyMap(final S instance, JdbcPersistentEntity persistentEntity) { - Map parameters = new HashMap<>(); + MapSqlParameterSource parameters = new MapSqlParameterSource(); persistentEntity.doWithProperties((PropertyHandler) 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 EntityRowMapper getEntityRowMapper(Class domainType) { - return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), conversions, context); + return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), conversions, context, this); } void doDeleteAll(Class 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; + } } diff --git a/src/main/java/org/springframework/data/jdbc/core/SqlGenerator.java b/src/main/java/org/springframework/data/jdbc/core/SqlGenerator.java index f02b0110..a275c223 100644 --- a/src/main/java/org/springframework/data/jdbc/core/SqlGenerator.java +++ b/src/main/java/org/springframework/data/jdbc/core/SqlGenerator.java @@ -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 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 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 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 additionalColumns) { String insertTemplate = "insert into %s (%s) values (%s)"; + List 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 // + ); } } diff --git a/src/main/java/org/springframework/data/jdbc/core/conversion/JdbcEntityWriter.java b/src/main/java/org/springframework/data/jdbc/core/conversion/JdbcEntityWriter.java index 93c62370..8f401d44 100644 --- a/src/main/java/org/springframework/data/jdbc/core/conversion/JdbcEntityWriter.java +++ b/src/main/java/org/springframework/data/jdbc/core/conversion/JdbcEntityWriter.java @@ -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 entityInformation = context .getRequiredPersistentEntityInformation((Class) 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 DbAction saveAction(T t, DbAction dependingOn) { + private Stream 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 collectionSaveAction(Collection collection, DbAction dependingOn) { + + return collection.stream().map(e -> singleSaveAction(e, dependingOn)); + } + + private DbAction singleSaveAction(T t, DbAction dependingOn) { JdbcPersistentEntityInformation entityInformation = context - .getRequiredPersistentEntityInformation((Class) t.getClass()); + .getRequiredPersistentEntityInformation((Class) 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 collectionPropertyAsStream(JdbcPersistentProperty p, + PersistentPropertyAccessor propertyAccessor) { + + Object property = propertyAccessor.getProperty(p); + if (property == null) { + return Stream.empty(); + } + + return ((Collection) property).stream(); + } + + private Stream singlePropertyAsStream(JdbcPersistentProperty p, PersistentPropertyAccessor propertyAccessor) { + Object property = propertyAccessor.getProperty(p); if (property == null) { return Stream.empty(); diff --git a/src/main/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentProperty.java b/src/main/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentProperty.java index a03d6a14..b5c54b90 100644 --- a/src/main/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentProperty.java +++ b/src/main/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentProperty.java @@ -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) { diff --git a/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentProperty.java b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentProperty.java index 32be8cb9..c3f756af 100644 --- a/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentProperty.java +++ b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentProperty.java @@ -39,4 +39,9 @@ public interface JdbcPersistentProperty extends PersistentProperty getColumnType(); + + @Override + JdbcPersistentEntity getOwner(); + + String getReverseColumnName(); } diff --git a/src/main/java/org/springframework/data/jdbc/support/JdbcUtil.java b/src/main/java/org/springframework/data/jdbc/support/JdbcUtil.java new file mode 100644 index 00000000..f7209fb3 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/support/JdbcUtil.java @@ -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 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); + } +} diff --git a/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java new file mode 100644 index 00000000..935d5bad --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java @@ -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 EntityRowMapper createRowMapper(Class 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) context.getRequiredPersistentEntity(type), + new DefaultConversionService(), context, template); + } + + private static ResultSet mockResultSet(List 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> result = convertValues(columns, values); + + return mock(ResultSet.class, new ResultSetAnswer(result)); + } + + private static List> convertValues(List columns, Object[] values) { + + List> result = new ArrayList<>(); + + int index = 0; + while (index < values.length) { + + Map 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> values; + private int index = -1; + + public ResultSetAnswer(List> 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 children; + } + +} diff --git a/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorFixedNamingStrategyUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorFixedNamingStrategyUnitTests.java index 3a9bf101..733ad5be 100644 --- a/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorFixedNamingStrategyUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorFixedNamingStrategyUnitTests.java @@ -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. * diff --git a/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorUnitTests.java index 8d5bddb6..d4f0844e 100644 --- a/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorUnitTests.java @@ -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 elements; } @SuppressWarnings("unused") @@ -125,4 +130,9 @@ public class SqlGeneratorUnitTests { @Id Long l2id; String something; } + + static class Element { + @Id Long id; + String content; + } } diff --git a/src/test/java/org/springframework/data/jdbc/core/conversion/JdbcEntityWriterUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/conversion/JdbcEntityWriterUnitTests.java index 87ce0441..2843a0b8 100644 --- a/src/test/java/org/springframework/data/jdbc/core/conversion/JdbcEntityWriterUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/conversion/JdbcEntityWriterUnitTests.java @@ -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 aggregateChange = new AggregateChange(Kind.SAVE, SomeEntity.class, entity); + SingleReferenceEntity entity = new SingleReferenceEntity(null); + AggregateChange 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 aggregateChange = new AggregateChange(Kind.SAVE, SomeEntity.class, entity); + SingleReferenceEntity entity = new SingleReferenceEntity(23L); + AggregateChange 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 aggregateChange = new AggregateChange(Kind.SAVE, SomeEntity.class, entity); + AggregateChange 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 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 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 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 = new HashSet<>(); + } + + @RequiredArgsConstructor + private static class CascadingReferenceEntity { + + @Id final Long id; + final Set other = new HashSet<>(); + } + + @RequiredArgsConstructor + private static class SetContainer { + + @Id final Long id; + Set elements = new HashSet<>(); + } + + @RequiredArgsConstructor + private static class Element { @Id final Long id; } + } diff --git a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryWithCollectionsIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryWithCollectionsIntegrationTests.java new file mode 100644 index 00000000..4cadae54 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryWithCollectionsIntegrationTests.java @@ -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 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 {} + + @Data + static class DummyEntity { + + @Id private Long id; + String name; + Set content = new HashSet<>(); + + } + + @RequiredArgsConstructor + static class Element { + + @Id private Long id; + String content; + } + +} diff --git a/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsIntegrationTests-hsql.sql b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsIntegrationTests-hsql.sql new file mode 100644 index 00000000..814e583a --- /dev/null +++ b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsIntegrationTests-hsql.sql @@ -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); diff --git a/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsIntegrationTests-mysql.sql b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsIntegrationTests-mysql.sql new file mode 100644 index 00000000..0e0a7e56 --- /dev/null +++ b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsIntegrationTests-mysql.sql @@ -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); diff --git a/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsIntegrationTests-postgres.sql b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsIntegrationTests-postgres.sql new file mode 100644 index 00000000..cfab77d5 --- /dev/null +++ b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsIntegrationTests-postgres.sql @@ -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);