diff --git a/src/main/java/org/springframework/data/jdbc/repository/EntityRowMapper.java b/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java similarity index 59% rename from src/main/java/org/springframework/data/jdbc/repository/EntityRowMapper.java rename to src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java index 97f96a30..22c986c7 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/EntityRowMapper.java +++ b/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java @@ -13,31 +13,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository; +package org.springframework.data.jdbc.core; +import lombok.NonNull; import lombok.RequiredArgsConstructor; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.Optional; import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.convert.ClassGeneratingEntityInstantiator; import org.springframework.data.convert.EntityInstantiator; +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.data.mapping.MappingException; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PreferredConstructor.Parameter; -import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mapping.model.ParameterValueProvider; import org.springframework.jdbc.core.RowMapper; /** - * Maps a ResultSet to an entity of type {@code T} + * Maps a ResultSet to an entity of type {@code T}, including entities referenced. * * @author Jens Schauder * @author Oliver Gierke @@ -49,6 +48,7 @@ class EntityRowMapper implements RowMapper { private final JdbcPersistentEntity entity; private final EntityInstantiator instantiator = new ClassGeneratingEntityInstantiator(); private final ConversionService conversions; + private final JdbcMappingContext context; /* * (non-Javadoc) @@ -58,36 +58,59 @@ 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); - entity.doWithProperties((PropertyHandler) property -> { - - PersistentPropertyAccessor accessor = entity.getPropertyAccessor(result); - ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor(accessor, conversions); - - propertyAccessor.setProperty(property, readFrom(resultSet, property)); - }); + for (JdbcPersistentProperty property : entity) { + propertyAccessor.setProperty(property, readFrom(resultSet, property, "")); + } return result; } private T createInstance(ResultSet rs) { - return instantiator.createInstance(entity, ResultSetParameterValueProvider.of(rs, conversions)); + return instantiator.createInstance(entity, ResultSetParameterValueProvider.of(rs, conversions, "")); } - private static Object readFrom(ResultSet resultSet, PersistentProperty property) { + private Object readFrom(ResultSet resultSet, JdbcPersistentProperty property, String prefix) { try { - return resultSet.getObject(property.getName()); + if (property.isEntity()) + return readEntityFrom(resultSet, property); + return resultSet.getObject(prefix + property.getColumnName()); } catch (SQLException o_O) { throw new MappingException(String.format("Could not read property %s from result set!", property), o_O); } } + private S readEntityFrom(ResultSet rs, PersistentProperty property) { + + String prefix = property.getName() + "_"; + + @SuppressWarnings("unchecked") + JdbcPersistentEntity entity = (JdbcPersistentEntity) context.getRequiredPersistentEntity(property.getType()); + + if (readFrom(rs, entity.getRequiredIdProperty(), prefix) == null) + return null; + + S instance = instantiator.createInstance(entity, ResultSetParameterValueProvider.of(rs, conversions, prefix)); + + PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance); + ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor(accessor, conversions); + + for (JdbcPersistentProperty p : entity) { + propertyAccessor.setProperty(p, readFrom(rs, p, prefix)); + } + + return instance; + } + @RequiredArgsConstructor(staticName = "of") private static class ResultSetParameterValueProvider implements ParameterValueProvider { - private final ResultSet resultSet; - private final ConversionService conversionService; + @NonNull private final ResultSet resultSet; + @NonNull private final ConversionService conversionService; + @NonNull private final String prefix; /* * (non-Javadoc) @@ -97,13 +120,14 @@ class EntityRowMapper implements RowMapper { public T getParameterValue(Parameter parameter) { String name = parameter.getName(); - if (name == null ) return null; + if (name == null) + return null; - try { - return conversionService.convert(resultSet.getObject(name), parameter.getType().getType()); - } catch (SQLException o_O) { - throw new MappingException(String.format("Couldn't read column %s from ResultSet.", name), o_O); - } + try { + return conversionService.convert(resultSet.getObject(prefix + name), parameter.getType().getType()); + } catch (SQLException o_O) { + throw new MappingException(String.format("Couldn't read column %s from ResultSet.", name), o_O); + } } } } diff --git a/src/main/java/org/springframework/data/jdbc/repository/EventPublishingEntityRowMapper.java b/src/main/java/org/springframework/data/jdbc/core/EventPublishingEntityRowMapper.java similarity index 92% rename from src/main/java/org/springframework/data/jdbc/repository/EventPublishingEntityRowMapper.java rename to src/main/java/org/springframework/data/jdbc/core/EventPublishingEntityRowMapper.java index 24723ff0..a543cd44 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/EventPublishingEntityRowMapper.java +++ b/src/main/java/org/springframework/data/jdbc/core/EventPublishingEntityRowMapper.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository; +package org.springframework.data.jdbc.core; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -24,7 +24,7 @@ import java.sql.SQLException; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.jdbc.mapping.event.AfterCreation; import org.springframework.data.jdbc.mapping.event.Identifier; -import org.springframework.data.jdbc.repository.support.JdbcPersistentEntityInformation; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntityInformation; import org.springframework.jdbc.core.RowMapper; /** diff --git a/src/main/java/org/springframework/data/jdbc/repository/JdbcEntityOperations.java b/src/main/java/org/springframework/data/jdbc/core/JdbcEntityOperations.java similarity index 85% rename from src/main/java/org/springframework/data/jdbc/repository/JdbcEntityOperations.java rename to src/main/java/org/springframework/data/jdbc/core/JdbcEntityOperations.java index 413b53c9..1b120304 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/JdbcEntityOperations.java +++ b/src/main/java/org/springframework/data/jdbc/core/JdbcEntityOperations.java @@ -13,7 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository; +package org.springframework.data.jdbc.core; + +import java.util.Map; /** * Specifies a operations one can perform on a database, based on an Domain Type. @@ -22,7 +24,7 @@ package org.springframework.data.jdbc.repository; */ public interface JdbcEntityOperations { - void insert(T instance, Class domainType); + void insert(T instance, Class domainType, Map additionalParameter); void update(T instance, Class domainType); @@ -40,7 +42,7 @@ public interface JdbcEntityOperations { boolean existsById(Object id, Class domainType); - void deleteAll(Iterable entities, Class domainType); - void deleteAll(Class domainType); + + void save(T instance, Class domainType); } diff --git a/src/main/java/org/springframework/data/jdbc/repository/JdbcEntityTemplate.java b/src/main/java/org/springframework/data/jdbc/core/JdbcEntityTemplate.java similarity index 63% rename from src/main/java/org/springframework/data/jdbc/repository/JdbcEntityTemplate.java rename to src/main/java/org/springframework/data/jdbc/core/JdbcEntityTemplate.java index bf81906f..b7cfad53 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/JdbcEntityTemplate.java +++ b/src/main/java/org/springframework/data/jdbc/core/JdbcEntityTemplate.java @@ -13,14 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository; +package org.springframework.data.jdbc.core; -import lombok.RequiredArgsConstructor; - -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -28,13 +23,16 @@ import java.util.stream.StreamSupport; import org.springframework.context.ApplicationEventPublisher; import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.NonTransientDataAccessException; import org.springframework.data.convert.Jsr310Converters; -import org.springframework.data.jdbc.mapping.context.JdbcMappingContext; +import org.springframework.data.jdbc.core.conversion.DbChange; +import org.springframework.data.jdbc.core.conversion.DbChange.Kind; +import org.springframework.data.jdbc.core.conversion.Interpreter; +import org.springframework.data.jdbc.core.conversion.JdbcEntityWriter; +import org.springframework.data.jdbc.core.conversion.JdbcEntityDeleteWriter; import org.springframework.data.jdbc.mapping.event.AfterDelete; import org.springframework.data.jdbc.mapping.event.AfterInsert; import org.springframework.data.jdbc.mapping.event.AfterUpdate; @@ -43,22 +41,25 @@ import org.springframework.data.jdbc.mapping.event.BeforeInsert; import org.springframework.data.jdbc.mapping.event.BeforeUpdate; import org.springframework.data.jdbc.mapping.event.Identifier; import org.springframework.data.jdbc.mapping.event.Identifier.Specified; +import org.springframework.data.jdbc.mapping.model.BasicJdbcPersistentEntityInformation; +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.repository.support.BasicJdbcPersistentEntityInformation; -import org.springframework.data.jdbc.repository.support.JdbcPersistentEntityInformation; import org.springframework.data.mapping.PropertyHandler; +import org.springframework.data.mapping.PropertyPath; import org.springframework.data.repository.core.EntityInformation; -import org.springframework.data.util.Streamable; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.KeyHolder; +import org.springframework.util.Assert; /** + * {@link JdbcEntityOperations} implementation, storing complete entities including references in a JDBC data store. + * * @author Jens Schauder */ -@RequiredArgsConstructor public class JdbcEntityTemplate implements JdbcEntityOperations { private static final String ENTITY_NEW_AFTER_INSERT = "Entity [%s] still 'new' after insert. Please set either" @@ -69,6 +70,24 @@ public class JdbcEntityTemplate implements JdbcEntityOperations { private final NamedParameterJdbcOperations operations; private final JdbcMappingContext context; private final ConversionService conversions = getDefaultConversionService(); + private final Interpreter interpreter; + private final SqlGeneratorSource sqlGeneratorSource; + + private final JdbcEntityWriter jdbcConverter; + private final JdbcEntityDeleteWriter jdbcEntityDeleteConverter; + + public JdbcEntityTemplate(ApplicationEventPublisher publisher, NamedParameterJdbcOperations operations, + JdbcMappingContext context) { + + this.publisher = publisher; + this.operations = operations; + this.context = context; + + jdbcConverter = new JdbcEntityWriter(this.context); + jdbcEntityDeleteConverter = new JdbcEntityDeleteWriter(this.context); + sqlGeneratorSource = new SqlGeneratorSource(this.context); + interpreter = new JdbcInterpreter(this.context, this); + } private static GenericConversionService getDefaultConversionService() { @@ -79,13 +98,18 @@ public class JdbcEntityTemplate implements JdbcEntityOperations { } @Override - public void insert(S instance, Class domainType) { + public void save(T instance, Class domainType) { + createDbChange(instance).executeWith(interpreter); + } + + @Override + public void insert(T instance, Class domainType, Map additionalParameter) { publisher.publishEvent(new BeforeInsert(instance)); KeyHolder holder = new GeneratedKeyHolder(); - JdbcPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); - JdbcPersistentEntityInformation entityInformation = context + JdbcPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); + JdbcPersistentEntityInformation entityInformation = context .getRequiredPersistentEntityInformation(domainType); Map propertyMap = getPropertyMap(instance, persistentEntity); @@ -94,7 +118,10 @@ public class JdbcEntityTemplate implements JdbcEntityOperations { JdbcPersistentProperty idProperty = persistentEntity.getRequiredIdProperty(); propertyMap.put(idProperty.getColumnName(), convert(idValue, idProperty.getColumnType())); - operations.update(sql(domainType).getInsert(idValue == null), new MapSqlParameterSource(propertyMap), holder); + propertyMap.putAll(additionalParameter); + + operations.update(sql(domainType).getInsert(idValue == null, additionalParameter.keySet()), + new MapSqlParameterSource(propertyMap), holder); setIdFromJdbc(instance, holder, persistentEntity); @@ -119,19 +146,6 @@ public class JdbcEntityTemplate implements JdbcEntityOperations { publisher.publishEvent(new AfterUpdate(specifiedId, instance)); } - @Override - public void delete(S entity, Class domainType) { - - JdbcPersistentEntityInformation entityInformation = context - .getRequiredPersistentEntityInformation(domainType); - delete(Identifier.of(entityInformation.getRequiredId(entity)), Optional.of(entity), domainType); - } - - @Override - public void deleteById(Object id, Class domainType) { - delete(Identifier.of(id), Optional.empty(), domainType); - } - @Override public long count(Class domainType) { return operations.getJdbcOperations().queryForObject(sql(domainType).getCount(), Long.class); @@ -170,27 +184,43 @@ public class JdbcEntityTemplate implements JdbcEntityOperations { } @Override - public void deleteAll(Iterable entities, Class domainType) { + public void delete(S entity, Class domainType) { - JdbcPersistentEntityInformation entityInformation = context + JdbcPersistentEntityInformation entityInformation = context .getRequiredPersistentEntityInformation(domainType); + deleteTree(entityInformation.getRequiredId(entity), entity, domainType); + } - Class targetType = context.getRequiredPersistentEntity(domainType).getRequiredIdProperty().getColumnType(); - List idList = Streamable.of(entities).stream() // - .map(entityInformation::getRequiredId) // - .map(id -> convert(id, targetType)) - .collect(Collectors.toList()); - - MapSqlParameterSource sqlParameterSource = new MapSqlParameterSource("ids", idList); - operations.update(sql(domainType).getDeleteByList(), sqlParameterSource); + @Override + public void deleteById(Object id, Class domainType) { + deleteTree(id, null, domainType); } @Override public void deleteAll(Class domainType) { - operations.getJdbcOperations().update(sql(domainType).getDeleteAll()); + + DbChange change = createDeletingDbChange(domainType); + change.executeWith(interpreter); } - private void delete(Specified specifiedId, Optional optionalEntity, Class domainType) { + void doDelete(Object rootId, PropertyPath propertyPath) { + + JdbcPersistentEntity entityToDelete = context.getRequiredPersistentEntity(propertyPath.getTypeInformation()); + + JdbcPersistentEntity rootEntity = context.getRequiredPersistentEntity(propertyPath.getOwningType()); + + JdbcPersistentProperty referencingProperty = rootEntity.getRequiredPersistentProperty(propertyPath.getSegment()); + Assert.notNull(referencingProperty, "No property found matching the PropertyPath " + propertyPath); + + String format = sql(rootEntity.getType()).createDeleteByPath(propertyPath); + + HashMap parameters = new HashMap<>(); + parameters.put("rootId", rootId); + operations.update(format, parameters); + + } + + void doDelete(Specified specifiedId, Optional optionalEntity, Class domainType) { publisher.publishEvent(new BeforeDelete(specifiedId, optionalEntity)); @@ -203,6 +233,34 @@ public class JdbcEntityTemplate implements JdbcEntityOperations { publisher.publishEvent(new AfterDelete(specifiedId, optionalEntity)); } + private void deleteTree(Object id, Object entity, Class domainType) { + + DbChange change = createDeletingDbChange(id, entity, domainType); + + change.executeWith(interpreter); + } + + private DbChange createDbChange(T instance) { + + DbChange dbChange = new DbChange(Kind.SAVE, instance.getClass(), instance); + jdbcConverter.write(instance, dbChange); + return dbChange; + } + + private DbChange createDeletingDbChange(Object id, Object entity, Class domainType) { + + DbChange dbChange = new DbChange(Kind.DELETE, domainType, entity); + jdbcEntityDeleteConverter.write(id, dbChange); + return dbChange; + } + + private DbChange createDeletingDbChange(Class domainType) { + + DbChange dbChange = new DbChange(Kind.DELETE, domainType, null); + jdbcEntityDeleteConverter.write(null, dbChange); + return dbChange; + } + private MapSqlParameterSource createIdParameterSource(Object id, Class domainType) { return new MapSqlParameterSource("id", convert(id, getRequiredPersistentEntity(domainType).getRequiredIdProperty().getColumnType())); @@ -213,11 +271,12 @@ public class JdbcEntityTemplate implements JdbcEntityOperations { Map parameters = new HashMap<>(); persistentEntity.doWithProperties((PropertyHandler) property -> { + if (!property.isEntity()) { + Object value = persistentEntity.getPropertyAccessor(instance).getProperty(property); - Optional value = persistentEntity.getPropertyAccessor(instance).getProperty(property); - - Object convertedValue = convert(value.orElse(null), property.getColumnType()); - parameters.put(property.getColumnName(), convertedValue); + Object convertedValue = convert(value, property.getColumnType()); + parameters.put(property.getColumnName(), convertedValue); + } }); return parameters; @@ -227,10 +286,9 @@ public class JdbcEntityTemplate implements JdbcEntityOperations { EntityInformation entityInformation = new BasicJdbcPersistentEntityInformation<>(persistentEntity); - Optional idValue = entityInformation.getId(instance); + ID idValue = entityInformation.getId(instance); - return isIdPropertySimpleTypeAndValueZero(idValue, persistentEntity) ? null - : idValue.orElseThrow(() -> new IllegalStateException("idValue must have a value at this point.")); + return isIdPropertySimpleTypeAndValueZero(idValue, persistentEntity) ? null : idValue; } private void setIdFromJdbc(S instance, KeyHolder holder, JdbcPersistentEntity persistentEntity) { @@ -244,7 +302,7 @@ public class JdbcEntityTemplate implements JdbcEntityOperations { Class targetType = persistentEntity.getRequiredIdProperty().getType(); Object converted = convert(it, targetType); - entityInformation.setId(instance, Optional.of(converted)); + entityInformation.setId(instance, converted); }); } catch (NonTransientDataAccessException e) { @@ -264,17 +322,25 @@ public class JdbcEntityTemplate implements JdbcEntityOperations { } private V convert(Object from, Class to) { - return conversions.convert(from, to); + + if (from == null) { + return null; + } + + JdbcPersistentEntity persistentEntity = context.getPersistentEntity(from.getClass()); + + Object id = persistentEntity == null ? null : persistentEntity.getIdentifierAccessor(from).getIdentifier(); + + return conversions.convert(id == null ? from : id, to); } - private boolean isIdPropertySimpleTypeAndValueZero(Optional idValue, - JdbcPersistentEntity persistentEntity) { + private boolean isIdPropertySimpleTypeAndValueZero(ID idValue, JdbcPersistentEntity persistentEntity) { - Optional idProperty = persistentEntity.getIdProperty(); - return !idValue.isPresent() // - || !idProperty.isPresent() // - || (idProperty.get().getType() == int.class && idValue.get().equals(0)) // - || (idProperty.get().getType() == long.class && idValue.get().equals(0L)); + JdbcPersistentProperty idProperty = persistentEntity.getIdProperty(); + return idValue == null // + || idProperty == null // + || (idProperty.getType() == int.class && idValue.equals(0)) // + || (idProperty.getType() == long.class && idValue.equals(0L)); } @SuppressWarnings("unchecked") @@ -283,10 +349,17 @@ public class JdbcEntityTemplate implements JdbcEntityOperations { } private SqlGenerator sql(Class domainType) { - return new SqlGenerator(context.getRequiredPersistentEntity(domainType)); + return sqlGeneratorSource.getSqlGenerator(domainType); } private EntityRowMapper getEntityRowMapper(Class domainType) { - return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), conversions); + return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), conversions, context); + } + + void doDeleteAll(Class domainType, PropertyPath propertyPath) { + + operations.getJdbcOperations() + .update(sql(propertyPath == null ? domainType : propertyPath.getOwningType().getType()) + .createDeleteAllSql(propertyPath)); } } diff --git a/src/main/java/org/springframework/data/jdbc/core/JdbcInterpreter.java b/src/main/java/org/springframework/data/jdbc/core/JdbcInterpreter.java new file mode 100644 index 00000000..a46b1d56 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/JdbcInterpreter.java @@ -0,0 +1,88 @@ +/* + * 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 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; +import org.springframework.data.jdbc.core.conversion.DbAction.DeleteAll; +import org.springframework.data.jdbc.core.conversion.DbAction.Insert; +import org.springframework.data.jdbc.core.conversion.DbAction.Update; +import org.springframework.data.jdbc.core.conversion.Interpreter; +import org.springframework.data.jdbc.mapping.event.Identifier; +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; + +/** + * {@link Interpreter} for {@link DbAction}s using a {@link JdbcEntityTemplate} for performing actual database + * interactions. + * + * @author Jens Schauder + */ +class JdbcInterpreter implements Interpreter { + + private final JdbcMappingContext context; + private final JdbcEntityTemplate template; + + JdbcInterpreter(JdbcMappingContext context, JdbcEntityTemplate template) { + this.context = context; + this.template = template; + } + + @Override + public void interpret(Insert insert) { + + Map additionalColumnValues = new HashMap<>(); + DbAction dependingOn = insert.getDependingOn(); + + if (dependingOn != null) { + + JdbcPersistentEntity persistentEntity = context.getRequiredPersistentEntity(dependingOn.getEntityType()); + String columnName = persistentEntity.getTableName(); + Object entity = dependingOn.getEntity(); + Object identifier = persistentEntity.getIdentifierAccessor(entity).getIdentifier(); + + additionalColumnValues.put(columnName, identifier); + } + + template.insert(insert.getEntity(), insert.getEntityType(), additionalColumnValues); + } + + @Override + public void interpret(Update update) { + template.update(update.getEntity(), update.getEntityType()); + } + + @Override + public void interpret(Delete delete) { + + if (delete.getPropertyPath() == null) { + template.doDelete(Identifier.of(delete.getRootId()), Optional.ofNullable(delete.getEntity()), + delete.getEntityType()); + } else { + template.doDelete(delete.getRootId(), delete.getPropertyPath()); + } + + } + + @Override + public void interpret(DeleteAll delete) { + template.doDeleteAll(delete.getEntityType(), delete.getPropertyPath()); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/core/SelectBuilder.java b/src/main/java/org/springframework/data/jdbc/core/SelectBuilder.java new file mode 100644 index 00000000..75fee019 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/SelectBuilder.java @@ -0,0 +1,296 @@ +/* + * 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 lombok.Builder; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Builder for creating Select-statements. Not intended for general purpose, but only for the needs of the + * {@link JdbcEntityTemplate}. + * + * @author Jens Schauder + */ +class SelectBuilder { + + private final List columns = new ArrayList<>(); + private final String tableName; + private final List joins = new ArrayList<>(); + private final List conditions = new ArrayList<>(); + + SelectBuilder(String tableName) { + + this.tableName = tableName; + } + + SelectBuilder column(Function columnSpec) { + + columns.add(columnSpec.apply(Column.builder()).build()); + return this; + } + + SelectBuilder where(Function whereSpec) { + conditions.add(whereSpec.apply(new WhereConditionBuilder(this)).build()); + return this; + } + + SelectBuilder join(Function joinSpec) { + joins.add(joinSpec.apply(Join.builder()).build()); + return this; + } + + String build() { + + return selectFrom() + joinClause() + whereClause(); + } + + private String whereClause() { + if (conditions.isEmpty()) + return ""; + return conditions.stream().map(wc -> wc.toSql()).collect(Collectors.joining("AND", " WHERE ", "")); + } + + private String joinClause() { + return joins.stream().map(j -> joinTable(j) + joinConditions(j)).collect(Collectors.joining(" ")); + } + + private String joinTable(Join j) { + return String.format("%s JOIN %s AS %s", j.outerJoinModifier(), j.table, j.as); + } + + private String joinConditions(Join j) { + return j.conditions.stream().map(w -> String.format("%s %s %s", w.fromExpression, w.operation, w.toExpression)) + .collect(Collectors.joining(" AND ", " ON ", "")); + } + + private String selectFrom() { + return columns.stream() // + .map(c -> c.columnDefinition()) // + .collect(Collectors.joining(", ", "SELECT ", " FROM " + tableName)); + } + + static class WhereConditionBuilder { + + private String fromTable; + private String fromColumn; + private final SelectBuilder selectBuilder; + + private String operation = "="; + private String expression; + + WhereConditionBuilder(SelectBuilder selectBuilder) { + this.selectBuilder = selectBuilder; + } + + WhereConditionBuilder eq() { + + operation = "="; + return this; + } + + public WhereConditionBuilder in() { + + operation = "in"; + return this; + } + + WhereConditionBuilder tableAlias(String fromTable) { + + this.fromTable = fromTable; + return this; + } + + WhereConditionBuilder column(String fromColumn) { + + this.fromColumn = fromColumn; + return this; + } + + WhereConditionBuilder variable(String var) { + + expression = ":" + var; + return this; + } + + WhereCondition build() { + return new WhereCondition(fromTable + "." + fromColumn, operation, expression); + } + + } + + static class Join { + + private final String table; + private final String as; + private final Outer outer; + private final List conditions = new ArrayList<>(); + + Join(String table, String as, List conditions, Outer outer) { + + this.table = table; + this.as = as; + this.outer = outer; + this.conditions.addAll(conditions); + } + + static JoinBuilder builder() { + return new JoinBuilder(); + } + + private String outerJoinModifier() { + + switch (outer) { + case NONE: + return ""; + default: + return String.format(" %s OUTER", outer.name()); + + } + } + + public static class JoinBuilder { + + private String table; + private String as; + private List conditions = new ArrayList<>(); + private Outer outer = Outer.NONE; + + JoinBuilder() {} + + public Join.JoinBuilder table(String table) { + + this.table = table; + return this; + } + + public Join.JoinBuilder as(String as) { + + this.as = as; + return this; + } + + WhereConditionBuilder where(String column) { + return new WhereConditionBuilder(this, column); + } + + private JoinBuilder where(WhereCondition condition) { + + conditions.add(condition); + return this; + } + + Join build() { + return new Join(table, as, conditions, outer); + } + + public String toString() { + return "org.springframework.data.jdbc.core.SelectBuilder.Join.JoinBuilder(table=" + this.table + ", as=" + + this.as + ")"; + } + + JoinBuilder rightOuter() { + + outer = Outer.RIGHT; + return this; + } + + JoinBuilder leftOuter() { + outer = Outer.LEFT; + return this; + } + + static class WhereConditionBuilder { + + private final JoinBuilder joinBuilder; + private final String fromColumn; + + private String operation = "="; + + WhereConditionBuilder(JoinBuilder joinBuilder, String column) { + + this.joinBuilder = joinBuilder; + this.fromColumn = column; + } + + WhereConditionBuilder eq() { + operation = "="; + return this; + } + + JoinBuilder column(String table, String column) { + + return joinBuilder.where(new WhereCondition( // + joinBuilder.as + "." + fromColumn, // + operation, // + table + "." + column // + )); + + } + + } + + } + + private enum Outer { + NONE, RIGHT, LEFT + } + } + + static class WhereCondition { + + private final String operation; + private final String fromExpression; + private final String toExpression; + + WhereCondition(String fromExpression, String operation, String toExpression) { + + this.fromExpression = fromExpression; + this.toExpression = toExpression; + this.operation = operation; + } + + String toSql() { + + if (operation.equals("in")) { + return String.format("%s %s(%s)", fromExpression, operation, toExpression); + } + + return String.format("%s %s %s", fromExpression, operation, toExpression); + } + } + + @Builder + static class Column { + + private final String tableAlias; + private final String column; + private final String as; + + String columnDefinition() { + StringBuilder b = new StringBuilder(); + if (tableAlias != null) + b.append(tableAlias).append('.'); + b.append(column); + if (as != null) + b.append(" AS ").append(as); + return b.toString(); + } + } +} diff --git a/src/main/java/org/springframework/data/jdbc/core/SqlGenerator.java b/src/main/java/org/springframework/data/jdbc/core/SqlGenerator.java new file mode 100644 index 00000000..0461f2c1 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/SqlGenerator.java @@ -0,0 +1,267 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +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.data.jdbc.mapping.model.PropertyPaths; +import org.springframework.data.jdbc.repository.SimpleJdbcRepository; +import org.springframework.data.mapping.PropertyHandler; +import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.util.Lazy; +import org.springframework.data.util.StreamUtils; +import org.springframework.util.Assert; + +/** + * Generates SQL statements to be used by {@link SimpleJdbcRepository} + * + * @author Jens Schauder + * @since 2.0 + */ +class SqlGenerator { + + private final JdbcPersistentEntity entity; + private final JdbcMappingContext context; + private final List propertyNames = new ArrayList<>(); + private final List nonIdPropertyNames = new ArrayList<>(); + + private final Lazy findOneSql = Lazy.of(this::createFindOneSelectSql); + private final Lazy findAllSql = Lazy.of(this::createFindAllSql); + private final Lazy findAllInListSql = Lazy.of(this::createFindAllInListSql); + + private final Lazy existsSql = Lazy.of(this::createExistsSql); + private final Lazy countSql = Lazy.of(this::createCountSql); + + private final Lazy updateSql = Lazy.of(this::createUpdateSql); + + private final Lazy deleteByIdSql = Lazy.of(this::createDeleteSql); + private final Lazy deleteByListSql = Lazy.of(this::createDeleteByListSql); + private final SqlGeneratorSource sqlGeneratorSource; + + SqlGenerator(JdbcMappingContext context, JdbcPersistentEntity entity, + SqlGeneratorSource sqlGeneratorSource) { + + this.context = context; + this.entity = entity; + this.sqlGeneratorSource = sqlGeneratorSource; + initPropertyNames(); + } + + private void initPropertyNames() { + + entity.doWithProperties((PropertyHandler) p -> { + // the referencing column of referenced entity is expected to be on the other side of the relation + if (!p.isEntity()) { + propertyNames.add(p.getName()); + if (!entity.isIdProperty(p)) { + nonIdPropertyNames.add(p.getName()); + } + } + }); + } + + String getFindAllInList() { + return findAllInListSql.get(); + } + + String getFindAll() { + return findAllSql.get(); + } + + String getExists() { + return existsSql.get(); + } + + String getFindOne() { + return findOneSql.get(); + } + + String getInsert(boolean excludeId, Set additionalColumns) { + return createInsertSql(excludeId, additionalColumns); + } + + String getUpdate() { + return updateSql.get(); + } + + String getCount() { + return countSql.get(); + } + + String getDeleteById() { + return deleteByIdSql.get(); + } + + String getDeleteByList() { + return deleteByListSql.get(); + } + + private String createFindOneSelectSql() { + + return createSelectBuilder() // + .where(wb -> wb.tableAlias(entity.getTableName()).column(entity.getIdColumn()).eq().variable("id")) // + .build(); + } + + 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()) // + ); + } + } + } + + return builder; + } + + private Stream getColumnNameStream(String prefix) { + return StreamUtils.createStreamFromIterator(entity.iterator()).flatMap(p -> getColumnNameStream(p, prefix)); + } + + private Stream getColumnNameStream(JdbcPersistentProperty p, String prefix) { + + if (p.isEntity()) { + return sqlGeneratorSource.getSqlGenerator(p.getType()).getColumnNameStream(prefix + p.getColumnName() + "_"); + } else { + return Stream.of(prefix + p.getColumnName()); + } + } + + private String createFindAllSql() { + return createSelectBuilder().build(); + } + + private String createFindAllInListSql() { + + return createSelectBuilder() // + .where(wb -> wb.tableAlias(entity.getTableName()).column(entity.getIdColumn()).in().variable("ids")) // + .build(); + } + + private String createExistsSql() { + return String.format("select count(*) from %s where %s = :id", entity.getTableName(), entity.getIdColumn()); + } + + private String createCountSql() { + return String.format("select count(*) from %s", entity.getTableName()); + } + + 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(", :", ":", "")); + + return String.format(insertTemplate, entity.getTableName(), tableColumns, parameterNames); + } + + private String createUpdateSql() { + + String updateTemplate = "update %s set %s where %s = :%s"; + + String setClause = propertyNames.stream()// + .map(n -> String.format("%s = :%s", n, n))// + .collect(Collectors.joining(", ")); + + return String.format(updateTemplate, entity.getTableName(), setClause, entity.getIdColumn(), entity.getIdColumn()); + } + + private String createDeleteSql() { + return String.format("DELETE from %s where %s = :id", entity.getTableName(), entity.getIdColumn()); + } + + String createDeleteAllSql(PropertyPath path) { + if (path == null) { + return String.format("DELETE FROM %s", entity.getTableName()); + } + + JdbcPersistentEntity entityToDelete = context.getRequiredPersistentEntity(PropertyPaths.getLeafType(path)); + + String innerMostCondition = String.format("%s IS NOT NULL", entity.getTableName(), entity.getTableName(), + entity.getIdColumn()); + + String condition = cascadeConditions(innerMostCondition, path.next()); + + return String.format("DELETE FROM %s WHERE %s", entityToDelete.getTableName(), condition, condition); + + } + + private String createDeleteByListSql() { + return String.format("doDelete from %s where %s in (:ids)", entity.getTableName(), entity.getIdColumn()); + } + + String createDeleteByPath(PropertyPath path) { + + JdbcPersistentEntity entityToDelete = context.getRequiredPersistentEntity(PropertyPaths.getLeafType(path)); + + String innerMostCondition = String.format("%s = :rootId", entity.getTableName()); + + String condition = cascadeConditions(innerMostCondition, path.next()); + + return String.format("DELETE FROM %s WHERE %s", entityToDelete.getTableName(), condition); + + } + + private String cascadeConditions(String innerCondition, PropertyPath path) { + + if (path == null) { + return innerCondition; + } + + JdbcPersistentEntity entity = context.getRequiredPersistentEntity(path.getOwningType()); + JdbcPersistentProperty property = entity.getPersistentProperty(path.getSegment()); + + 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); + + } +} diff --git a/src/main/java/org/springframework/data/jdbc/core/SqlGeneratorSource.java b/src/main/java/org/springframework/data/jdbc/core/SqlGeneratorSource.java new file mode 100644 index 00000000..319dc48a --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/SqlGeneratorSource.java @@ -0,0 +1,43 @@ +/* + * 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 lombok.RequiredArgsConstructor; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; + +/** + * Provides {@link SqlGenerator}s per domain type. Instances get cched, so when asked multiple times for the same domain + * type, the same generator will get returned. + * + * @author Jens Schauder + */ +@RequiredArgsConstructor +public class SqlGeneratorSource { + + private final Map sqlGeneratorCache = new HashMap<>(); + private final JdbcMappingContext context; + + SqlGenerator getSqlGenerator(Class domainType) { + + return sqlGeneratorCache.computeIfAbsent(domainType, + t -> new SqlGenerator(context, context.getRequiredPersistentEntity(t), this)); + + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/UnableToSetId.java b/src/main/java/org/springframework/data/jdbc/core/UnableToSetId.java similarity index 89% rename from src/main/java/org/springframework/data/jdbc/repository/UnableToSetId.java rename to src/main/java/org/springframework/data/jdbc/core/UnableToSetId.java index e6e22c1b..a837d69a 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/UnableToSetId.java +++ b/src/main/java/org/springframework/data/jdbc/core/UnableToSetId.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository; +package org.springframework.data.jdbc.core; import org.springframework.dao.NonTransientDataAccessException; @@ -27,7 +27,7 @@ public class UnableToSetId extends NonTransientDataAccessException { private static final long serialVersionUID = 3285001352389420376L; - public UnableToSetId(String message, Throwable cause) { + UnableToSetId(String message, Throwable cause) { super(message, cause); } } diff --git a/src/main/java/org/springframework/data/jdbc/core/conversion/DbAction.java b/src/main/java/org/springframework/data/jdbc/core/conversion/DbAction.java new file mode 100644 index 00000000..341370bd --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/conversion/DbAction.java @@ -0,0 +1,183 @@ +/* + * 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.conversion; + +import lombok.Getter; +import lombok.ToString; + +import org.springframework.data.mapping.PropertyPath; +import org.springframework.util.Assert; + +/** + * Abstracts over a single interaction with a database. + * + * @author Jens Schauder + */ +@ToString +@Getter +public abstract class DbAction { + + /** + * {@link Class} of the entity of which the database representation is affected by this action. + */ + private final Class entityType; + + /** + * The entity of which the database representation is affected by this action. Might be {@literal null}. + */ + private final T entity; + + /** + * Another action, this action depends on. For example the insert for one entity might need the id of another entity, + * which gets insert before this one. That action would be referenced by this property, so that the id becomes + * available at execution time. Might be {@literal null}. + */ + private final DbAction dependingOn; + + private DbAction(Class entityType, T entity, DbAction dependingOn) { + + this.entityType = entityType; + this.entity = entity; + this.dependingOn = dependingOn; + } + + public static Insert insert(T entity, DbAction dependingOn) { + return new Insert<>(entity, dependingOn); + } + + public static Update update(T entity, DbAction dependingOn) { + return new Update<>(entity, dependingOn); + } + + public static Delete delete(Object id, Class type, T entity, PropertyPath propertyPath, + DbAction dependingOn) { + return new Delete<>(id, type, entity, propertyPath, dependingOn); + } + + public static DeleteAll deleteAll(Class type, PropertyPath propertyPath, DbAction dependingOn) { + return new DeleteAll<>(type, propertyPath, dependingOn); + } + + abstract void executeWith(Interpreter interpreter); + + /** + * {@link InsertOrUpdate} must reference an entity. + * + * @param type o the entity for which this represents a database interaction + */ + abstract static class InsertOrUpdate extends DbAction { + + InsertOrUpdate(T entity, DbAction dependingOn) { + super((Class) entity.getClass(), entity, dependingOn); + } + } + + /** + * Represents an insert statement. + * + * @param type o the entity for which this represents a database interaction + */ + public static class Insert extends InsertOrUpdate { + + private Insert(T entity, DbAction dependingOn) { + super(entity, dependingOn); + } + + @Override + void executeWith(Interpreter interpreter) { + interpreter.interpret(this); + } + } + + /** + * Represents an update statement. + * + * @param type o the entity for which this represents a database interaction + */ + public static class Update extends InsertOrUpdate { + + private Update(T entity, DbAction dependingOn) { + super(entity, dependingOn); + } + + @Override + void executeWith(Interpreter interpreter) { + interpreter.interpret(this); + } + } + + /** + * Represents an delete statement, possibly a cascading delete statement, i.e. the delete necessary because one + * aggregate root gets deleted. + * + * @param type o the entity for which this represents a database interaction + */ + @Getter + public static class Delete extends DbAction { + + /** + * Id of the root for which all via {@link #propertyPath} referenced entities shall get deleted + */ + private final Object rootId; + + /** + * {@link PropertyPath} which connects the aggregate root with the entities to be deleted. If this is the action to + * delete the root enity itself, this is {@literal null}. + */ + private final PropertyPath propertyPath; + + private Delete(Object rootId, Class type, T entity, PropertyPath propertyPath, DbAction dependingOn) { + + super(type, entity, dependingOn); + + Assert.notNull(rootId, "rootId must not be null."); + + this.rootId = rootId; + this.propertyPath = propertyPath; + } + + @Override + void executeWith(Interpreter interpreter) { + interpreter.interpret(this); + } + } + + /** + * Represents an delete statement. + * + * @param type o the entity for which this represents a database interaction + */ + @Getter + public static class DeleteAll extends DbAction { + + /** + * + */ + private final PropertyPath propertyPath; + + private DeleteAll(Class entityType, PropertyPath propertyPath, DbAction dependingOn) { + + super(entityType, null, dependingOn); + + this.propertyPath = propertyPath; + } + + @Override + void executeWith(Interpreter interpreter) { + interpreter.interpret(this); + } + } +} diff --git a/src/main/java/org/springframework/data/jdbc/core/conversion/DbChange.java b/src/main/java/org/springframework/data/jdbc/core/conversion/DbChange.java new file mode 100644 index 00000000..3cdfceec --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/conversion/DbChange.java @@ -0,0 +1,52 @@ +/* + * 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.conversion; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Jens Schauder + */ +@RequiredArgsConstructor +@Getter +public class DbChange { + + private final Kind kind; + + /** Type of the aggregate root to be changed */ + private final Class entityType; + + /** Aggregate root, to which the change applies, if available */ + private final T entity; + + private final List actions = new ArrayList<>(); + + public void executeWith(Interpreter interpreter) { + actions.forEach(a -> a.executeWith(interpreter)); + } + + public void addAction(DbAction action) { + actions.add(action); + } + + public enum Kind { + SAVE, DELETE + } +} diff --git a/src/main/java/org/springframework/data/jdbc/core/conversion/Interpreter.java b/src/main/java/org/springframework/data/jdbc/core/conversion/Interpreter.java new file mode 100644 index 00000000..c9f5769c --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/conversion/Interpreter.java @@ -0,0 +1,33 @@ +/* + * 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.conversion; + +import org.springframework.data.jdbc.core.conversion.DbAction.Delete; +import org.springframework.data.jdbc.core.conversion.DbAction.DeleteAll; +import org.springframework.data.jdbc.core.conversion.DbAction.Insert; +import org.springframework.data.jdbc.core.conversion.DbAction.Update; + +/** + * @author Jens Schauder + */ +public interface Interpreter { + + void interpret(Update update); + void interpret(Insert insert); + + void interpret(Delete delete); + void interpret(DeleteAll delete); +} diff --git a/src/main/java/org/springframework/data/jdbc/core/conversion/JdbcEntityDeleteWriter.java b/src/main/java/org/springframework/data/jdbc/core/conversion/JdbcEntityDeleteWriter.java new file mode 100644 index 00000000..3e602fe7 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/conversion/JdbcEntityDeleteWriter.java @@ -0,0 +1,58 @@ +/* + * 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.conversion; + +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; +import org.springframework.data.jdbc.mapping.model.PropertyPaths; + +/** + * Converts an entity that is about to be deleted into {@link DbAction}s inside a {@link DbChange} that need to be + * executed against the database to recreate the appropriate state in the database. + * + * @author Jens Schauder + */ +public class JdbcEntityDeleteWriter extends JdbcEntityWriterSupport { + + public JdbcEntityDeleteWriter(JdbcMappingContext context) { + super(context); + } + + @Override + public void write(Object id, DbChange dbChange) { + + if (id == null) { + deleteAll(dbChange); + } else { + deleteById(id, dbChange); + } + } + + private void deleteAll(DbChange dbChange) { + + context.referencedEntities(dbChange.getEntityType(), null) + .forEach(p -> dbChange.addAction(DbAction.deleteAll(PropertyPaths.getLeafType(p), p, null))); + + dbChange.addAction(DbAction.deleteAll(dbChange.getEntityType(), null, null)); + } + + private void deleteById(Object id, DbChange dbChange) { + + deleteReferencedEntities(id, dbChange); + + dbChange.addAction(DbAction.delete(id, dbChange.getEntityType(), dbChange.getEntity(), null, null)); + } + +} 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 new file mode 100644 index 00000000..6ee83a0f --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/conversion/JdbcEntityWriter.java @@ -0,0 +1,122 @@ +/* + * 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.conversion; + +import java.util.stream.Stream; + +import org.springframework.data.jdbc.core.conversion.DbAction.Insert; +import org.springframework.data.jdbc.core.conversion.DbAction.Update; +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.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.util.StreamUtils; + +/** + * Converts an entity that is about to be saved into {@link DbAction}s inside a {@link DbChange} that need to be + * executed against the database to recreate the appropriate state in the database. + * + * @author Jens Schauder + */ +public class JdbcEntityWriter extends JdbcEntityWriterSupport { + + public JdbcEntityWriter(JdbcMappingContext context) { + super(context); + } + + @Override + public void write(Object o, DbChange dbChange) { + write(o, dbChange, null); + } + + private void write(Object o, DbChange dbChange, DbAction dependingOn) { + + JdbcPersistentEntity persistentEntity = context.getRequiredPersistentEntity(dbChange.getEntityType()); + + JdbcPersistentEntityInformation entityInformation = context + .getRequiredPersistentEntityInformation((Class) o.getClass()); + + if (entityInformation.isNew(o)) { + Insert insert = DbAction.insert(o, dependingOn); + dbChange.addAction(insert); + + referencedEntities(o).forEach(e -> saveReferencedEntities(e, dbChange, insert)); + } else { + + deleteReferencedEntities(entityInformation.getRequiredId(o), dbChange); + + Update update = DbAction.update(o, dependingOn); + dbChange.addAction(update); + + referencedEntities(o).forEach(e -> insertReferencedEntities(e, dbChange, update)); + } + } + + private void saveReferencedEntities(Object o, DbChange dbChange, DbAction dependingOn) { + + DbAction action = saveAction(o, dependingOn); + dbChange.addAction(action); + + referencedEntities(o).forEach(e -> saveReferencedEntities(e, dbChange, action)); + } + + private DbAction saveAction(T t, DbAction dependingOn) { + + JdbcPersistentEntityInformation entityInformation = context + .getRequiredPersistentEntityInformation((Class) t.getClass()); + + if (entityInformation.isNew(t)) + return DbAction.insert(t, dependingOn); + else + return DbAction.update(t, dependingOn); + } + + private void insertReferencedEntities(Object o, DbChange dbChange, DbAction dependingOn) { + + System.out.println("adding an insert"); + dbChange.addAction(DbAction.insert(o, dependingOn)); + referencedEntities(o).forEach(e -> insertReferencedEntities(e, dbChange, dependingOn)); + } + + private Stream referencedEntities(Object o) { + + JdbcPersistentEntity persistentEntity = context.getRequiredPersistentEntity(o.getClass()); + + return StreamUtils.createStreamFromIterator(persistentEntity.iterator()) // + .filter(PersistentProperty::isEntity) + .flatMap(p -> referencedEntity(p, persistentEntity.getPropertyAccessor(o))); + } + + private Stream referencedEntity(JdbcPersistentProperty p, PersistentPropertyAccessor propertyAccessor) { + + Class type = p.getActualType(); + JdbcPersistentEntity persistentEntity = context // + .getPersistentEntity(type); + + if (persistentEntity == null) { + return Stream.empty(); + } + + Object property = propertyAccessor.getProperty(p); + if (property == null) { + return Stream.empty(); + } + + return Stream.of(property); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/core/conversion/JdbcEntityWriterSupport.java b/src/main/java/org/springframework/data/jdbc/core/conversion/JdbcEntityWriterSupport.java new file mode 100644 index 00000000..0f514187 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/conversion/JdbcEntityWriterSupport.java @@ -0,0 +1,46 @@ +/* + * 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.conversion; + +import org.springframework.data.convert.EntityWriter; +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; +import org.springframework.data.jdbc.mapping.model.PropertyPaths; + +/** + * Common infrastructure needed by different implementations of {@link EntityWriter}. + * + * @author Jens Schauder + */ +abstract class JdbcEntityWriterSupport implements EntityWriter { + protected final JdbcMappingContext context; + + JdbcEntityWriterSupport(JdbcMappingContext context) { + this.context = context; + } + + /** + * add {@link org.springframework.data.jdbc.core.conversion.DbAction.Delete} actions to the {@link DbChange} for + * deleting all referenced entities. + * + * @param id id of the aggregate root, of which the referenced entities get deleted. + * @param dbChange the change object to which the actions should get added. Must not be {@literal null} + */ + void deleteReferencedEntities(Object id, DbChange dbChange) { + + context.referencedEntities(dbChange.getEntityType(), null) + .forEach(p -> dbChange.addAction(DbAction.delete(id, PropertyPaths.getLeafType(p), null, p, null))); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/BasicJdbcPersistentEntityInformation.java b/src/main/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentEntityInformation.java similarity index 80% rename from src/main/java/org/springframework/data/jdbc/repository/support/BasicJdbcPersistentEntityInformation.java rename to src/main/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentEntityInformation.java index 9d3e4ab3..5c139365 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/BasicJdbcPersistentEntityInformation.java +++ b/src/main/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentEntityInformation.java @@ -13,12 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository.support; +package org.springframework.data.jdbc.mapping.model; -import java.io.Serializable; -import java.util.Optional; - -import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; import org.springframework.data.repository.core.support.PersistentEntityInformation; /** @@ -39,7 +35,7 @@ public class BasicJdbcPersistentEntityInformation extends PersistentEntit /* * (non-Javadoc) - * @see org.springframework.data.jdbc.repository.support.JdbcPersistentEntityInformation#setId(java.lang.Object, java.util.Optional) + * @see org.springframework.data.jdbc.mapping.model.JdbcPersistentEntityInformation#setId(java.lang.Object, java.util.Optional) */ @Override public void setId(T instance, Object value) { 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 fbdcd441..015103c4 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 @@ -20,8 +20,6 @@ import java.time.temporal.Temporal; import java.util.Date; import java.util.LinkedHashMap; import java.util.Map; -import java.util.Map.Entry; -import java.util.Optional; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PersistentEntity; @@ -40,6 +38,7 @@ public class BasicJdbcPersistentProperty extends AnnotationBasedPersistentProper implements JdbcPersistentProperty { private static final Map, Class> javaToDbType = new LinkedHashMap<>(); + private final JdbcMappingContext context; static { javaToDbType.put(Enum.class, String.class); @@ -49,14 +48,16 @@ public class BasicJdbcPersistentProperty extends AnnotationBasedPersistentProper /** * Creates a new {@link AnnotationBasedPersistentProperty}. - * + * * @param property must not be {@literal null}. * @param owner must not be {@literal null}. * @param simpleTypeHolder must not be {@literal null}. + * @param context */ public BasicJdbcPersistentProperty(Property property, PersistentEntity owner, - SimpleTypeHolder simpleTypeHolder) { + SimpleTypeHolder simpleTypeHolder, JdbcMappingContext context) { super(property, owner, simpleTypeHolder); + this.context = context; } /* @@ -85,10 +86,32 @@ public class BasicJdbcPersistentProperty extends AnnotationBasedPersistentProper @Override public Class getColumnType() { - Class type = getType(); + Class columnType = columnTypeIfEntity(getType()); + + return columnType == null ? columnTypeForNonEntity(getType()) : columnType; + } + + private Class columnTypeIfEntity(Class type) { + + JdbcPersistentEntity persistentEntity = context.getPersistentEntity(type); + + if (persistentEntity == null) { + return null; + } + + JdbcPersistentProperty idProperty = persistentEntity.getIdProperty(); + + if (idProperty == null) { + return null; + } + return idProperty.getColumnType(); + } + + private Class columnTypeForNonEntity(Class type) { + return javaToDbType.entrySet().stream() // .filter(e -> e.getKey().isAssignableFrom(type)) // - .map(e -> (Class)e.getValue()) // + .map(e -> (Class) e.getValue()) // .findFirst() // .orElseGet(() -> ClassUtils.resolvePrimitiveIfNecessary(type)); } diff --git a/src/main/java/org/springframework/data/jdbc/mapping/context/JdbcMappingContext.java b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcMappingContext.java similarity index 61% rename from src/main/java/org/springframework/data/jdbc/mapping/context/JdbcMappingContext.java rename to src/main/java/org/springframework/data/jdbc/mapping/model/JdbcMappingContext.java index 5d77de6b..d2c07430 100644 --- a/src/main/java/org/springframework/data/jdbc/mapping/context/JdbcMappingContext.java +++ b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcMappingContext.java @@ -13,14 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.mapping.context; +package org.springframework.data.jdbc.mapping.model; -import org.springframework.data.jdbc.mapping.model.BasicJdbcPersistentProperty; -import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; -import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntityImpl; -import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty; -import org.springframework.data.jdbc.repository.support.BasicJdbcPersistentEntityInformation; -import org.springframework.data.jdbc.repository.support.JdbcPersistentEntityInformation; +import static java.util.Arrays.*; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.temporal.Temporal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.context.AbstractMappingContext; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.Property; @@ -35,6 +40,40 @@ import org.springframework.data.util.TypeInformation; */ public class JdbcMappingContext extends AbstractMappingContext, JdbcPersistentProperty> { + private static final HashSet> CUSTOM_SIMPLE_TYPES = new HashSet<>(asList( // + BigDecimal.class, // + BigInteger.class, // + Temporal.class // + )); + + public JdbcMappingContext() { + setSimpleTypeHolder(new SimpleTypeHolder(CUSTOM_SIMPLE_TYPES, true)); + } + + public List referencedEntities(Class rootType, PropertyPath path) { + + List paths = new ArrayList<>(); + + Class currentType = path == null ? rootType : PropertyPaths.getLeafType(path); + JdbcPersistentEntity persistentEntity = getRequiredPersistentEntity(currentType); + + String rootPrefix = path == null ? "" : path.toDotPath() + "."; + + for (JdbcPersistentProperty property : persistentEntity) { + if (property.isEntity()) { + + PropertyPath nextPath = path == null ? PropertyPath.from(property.getName(), rootType) + : PropertyPaths.extendBy(path, property.getColumnName()); + paths.add(nextPath); + paths.addAll(referencedEntities(rootType, nextPath)); + } + } + + Collections.reverse(paths); + + return paths; + } + /* * (non-Javadoc) * @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentEntity(org.springframework.data.util.TypeInformation) @@ -51,11 +90,12 @@ public class JdbcMappingContext extends AbstractMappingContext owner, SimpleTypeHolder simpleTypeHolder) { - return new BasicJdbcPersistentProperty(property, owner, simpleTypeHolder); + return new BasicJdbcPersistentProperty(property, owner, simpleTypeHolder, this); } @SuppressWarnings("unchecked") public JdbcPersistentEntityInformation getRequiredPersistentEntityInformation(Class type) { return new BasicJdbcPersistentEntityInformation<>((JdbcPersistentEntity) getRequiredPersistentEntity(type)); } + } diff --git a/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentEntityImpl.java b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentEntityImpl.java index 744423f1..53342792 100644 --- a/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentEntityImpl.java +++ b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentEntityImpl.java @@ -26,7 +26,7 @@ import org.springframework.data.util.TypeInformation; * @author Jens Schauder * @since 2.0 */ -public class JdbcPersistentEntityImpl extends BasicPersistentEntity +class JdbcPersistentEntityImpl extends BasicPersistentEntity implements JdbcPersistentEntity { private final @Getter String tableName; @@ -36,7 +36,7 @@ public class JdbcPersistentEntityImpl extends BasicPersistentEntity information) { + JdbcPersistentEntityImpl(TypeInformation information) { super(information); @@ -51,4 +51,9 @@ public class JdbcPersistentEntityImpl extends BasicPersistentEntity", getType()); + } } diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcPersistentEntityInformation.java b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentEntityInformation.java similarity index 85% rename from src/main/java/org/springframework/data/jdbc/repository/support/JdbcPersistentEntityInformation.java rename to src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentEntityInformation.java index 9b9e6500..0230e900 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcPersistentEntityInformation.java +++ b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentEntityInformation.java @@ -13,9 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository.support; - -import java.util.Optional; +package org.springframework.data.jdbc.mapping.model; import org.springframework.data.repository.core.EntityInformation; @@ -35,8 +33,11 @@ public interface JdbcPersistentEntityInformation extends EntityInformatio * @throws IllegalArgumentException in case no identifier can be obtained for the given entity. */ default ID getRequiredId(T entity) { + ID id = getId(entity); - if (id == null) throw new IllegalStateException(String.format("Could not obtain required identifier from entity %s!", entity)); + if (id == null) + throw new IllegalStateException(String.format("Could not obtain required identifier from entity %s!", entity)); + return id; } } diff --git a/src/main/java/org/springframework/data/jdbc/mapping/model/PropertyPaths.java b/src/main/java/org/springframework/data/jdbc/mapping/model/PropertyPaths.java new file mode 100644 index 00000000..9d78e3d1 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mapping/model/PropertyPaths.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.mapping.model; + +import lombok.experimental.UtilityClass; + +import org.springframework.data.mapping.PropertyPath; +import org.springframework.util.Assert; + +/** + * Utilities for working with {@link PropertyPath}s. + * + * @author Jens Schauder + */ +@UtilityClass +public class PropertyPaths { + + public static Class getLeafType(PropertyPath path) { + if (path.hasNext()) + return getLeafType(path.next()); + return path.getType(); + } + + public static PropertyPath extendBy(PropertyPath path, String name) { + + Assert.notNull(path, "Path must not be null."); + Assert.hasText(name, "Name must not be empty"); + + return PropertyPath.from(path.toDotPath() + "." + name, path.getOwningType()); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/SimpleJdbcRepository.java b/src/main/java/org/springframework/data/jdbc/repository/SimpleJdbcRepository.java index bfef2c0a..cbd2102b 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/SimpleJdbcRepository.java +++ b/src/main/java/org/springframework/data/jdbc/repository/SimpleJdbcRepository.java @@ -15,13 +15,14 @@ */ package org.springframework.data.jdbc.repository; -import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Optional; -import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntityImpl; -import org.springframework.data.jdbc.repository.support.JdbcPersistentEntityInformation; +import org.springframework.data.jdbc.core.JdbcEntityOperations; +import org.springframework.data.jdbc.core.JdbcEntityTemplate; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntityInformation; import org.springframework.data.repository.CrudRepository; /** @@ -35,7 +36,7 @@ public class SimpleJdbcRepository implements CrudRepository { private final JdbcEntityOperations entityOperations; /** - * Creates a new {@link SimpleJdbcRepository} for the given {@link JdbcPersistentEntityImpl} + * Creates a new {@link SimpleJdbcRepository}. */ public SimpleJdbcRepository(JdbcEntityTemplate entityOperations, JdbcPersistentEntityInformation entityInformation) { @@ -51,11 +52,7 @@ public class SimpleJdbcRepository implements CrudRepository { @Override public S save(S instance) { - if (entityInformation.isNew(instance)) { - entityOperations.insert(instance, entityInformation.getJavaType()); - } else { - entityOperations.update(instance, entityInformation.getJavaType()); - } + entityOperations.save(instance, entityInformation.getJavaType()); return instance; } @@ -142,7 +139,11 @@ public class SimpleJdbcRepository implements CrudRepository { */ @Override public void deleteAll(Iterable entities) { - entityOperations.deleteAll(entities, entityInformation.getJavaType()); + + for (T entity : entities) { + entityOperations.delete(entity, (Class) entity.getClass()); + + } } @Override diff --git a/src/main/java/org/springframework/data/jdbc/repository/SqlGenerator.java b/src/main/java/org/springframework/data/jdbc/repository/SqlGenerator.java deleted file mode 100644 index e86e096c..00000000 --- a/src/main/java/org/springframework/data/jdbc/repository/SqlGenerator.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright 2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.jdbc.repository; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; -import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty; -import org.springframework.data.mapping.PropertyHandler; - -/** - * Generates SQL statements to be used by {@link SimpleJdbcRepository} - * - * @author Jens Schauder - * @since 2.0 - */ -class SqlGenerator { - - private final String findOneSql; - private final String findAllSql; - private final String findAllInListSql; - - private final String existsSql; - private final String countSql; - - private final String updateSql; - - private final String deleteByIdSql; - private final String deleteAllSql; - private final String deleteByListSql; - - private final JdbcPersistentEntity entity; - private final List propertyNames = new ArrayList<>(); - private final List nonIdPropertyNames = new ArrayList<>(); - - SqlGenerator(JdbcPersistentEntity entity) { - - this.entity = entity; - - initPropertyNames(); - - findOneSql = createFindOneSelectSql(); - findAllSql = createFindAllSql(); - findAllInListSql = createFindAllInListSql(); - - existsSql = createExistsSql(); - countSql = createCountSql(); - - updateSql = createUpdateSql(); - - deleteByIdSql = createDeleteSql(); - deleteAllSql = createDeleteAllSql(); - deleteByListSql = createDeleteByListSql(); - } - - private void initPropertyNames() { - - entity.doWithProperties((PropertyHandler) p -> { - propertyNames.add(p.getName()); - if (!entity.isIdProperty(p)) { - nonIdPropertyNames.add(p.getName()); - } - }); - } - - String getFindAllInList() { - return findAllInListSql; - } - - String getFindAll() { - return findAllSql; - } - - String getExists() { - return existsSql; - } - - String getFindOne() { - return findOneSql; - } - - String getInsert(boolean excludeId) { - return createInsertSql(excludeId); - } - - String getUpdate() { - return updateSql; - } - - String getCount() { - return countSql; - } - - String getDeleteById() { - return deleteByIdSql; - } - - String getDeleteAll() { - return deleteAllSql; - } - - String getDeleteByList() { - return deleteByListSql; - } - - private String createFindOneSelectSql() { - return String.format("select * from %s where %s = :id", entity.getTableName(), entity.getIdColumn()); - } - - private String createFindAllSql() { - return String.format("select * from %s", entity.getTableName()); - } - - private String createFindAllInListSql() { - return String.format("select * from %s where %s in (:ids)", entity.getTableName(), entity.getIdColumn()); - } - - private String createExistsSql() { - return String.format("select count(*) from %s where %s = :id", entity.getTableName(), entity.getIdColumn()); - } - - private String createCountSql() { - return String.format("select count(*) from %s", entity.getTableName()); - } - - private String createInsertSql(boolean excludeId) { - - String insertTemplate = "insert into %s (%s) values (%s)"; - List propertyNamesForInsert = excludeId ? nonIdPropertyNames : propertyNames; - String tableColumns = String.join(", ", propertyNamesForInsert); - String parameterNames = propertyNamesForInsert.stream().collect(Collectors.joining(", :", ":", "")); - - return String.format(insertTemplate, entity.getTableName(), tableColumns, parameterNames); - } - - private String createUpdateSql() { - - String updateTemplate = "update %s set %s where %s = :%s"; - - String setClause = propertyNames.stream()// - .map(n -> String.format("%s = :%s", n, n))// - .collect(Collectors.joining(", ")); - - return String.format(updateTemplate, entity.getTableName(), setClause, entity.getIdColumn(), entity.getIdColumn()); - } - - private String createDeleteSql() { - return String.format("delete from %s where %s = :id", entity.getTableName(), entity.getIdColumn()); - } - - private String createDeleteAllSql() { - return String.format("delete from %s", entity.getTableName()); - } - - private String createDeleteByListSql() { - return String.format("delete from %s where %s in (:ids)", entity.getTableName(), entity.getIdColumn()); - } -} diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java index a29b193f..d435cc17 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java +++ b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java @@ -18,9 +18,11 @@ package org.springframework.data.jdbc.repository.support; import lombok.RequiredArgsConstructor; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.data.jdbc.mapping.context.JdbcMappingContext; +import org.springframework.data.jdbc.core.JdbcEntityTemplate; +import org.springframework.data.jdbc.mapping.model.BasicJdbcPersistentEntityInformation; +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; -import org.springframework.data.jdbc.repository.JdbcEntityTemplate; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntityInformation; import org.springframework.data.jdbc.repository.SimpleJdbcRepository; import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.repository.core.RepositoryInformation; @@ -43,21 +45,18 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport { @Override public EntityInformation getEntityInformation(Class aClass) { - JdbcPersistentEntityImpl persistentEntity = context.getPersistentEntity(aClass); + JdbcPersistentEntity persistentEntity = context.getRequiredPersistentEntity(aClass); if (persistentEntity == null) return null; - return new BasicJdbcPersistentEntityInformation((JdbcPersistentEntity) persistentEntity); + return new BasicJdbcPersistentEntityInformation<>((JdbcPersistentEntity) persistentEntity); } @SuppressWarnings("unchecked") @Override protected Object getTargetRepository(RepositoryInformation repositoryInformation) { - JdbcPersistentEntity persistentEntity = context // - .getPersistentEntity(repositoryInformation.getDomainType()) // - .orElseThrow(() -> new IllegalArgumentException("%s does not represent a persistent entity")); // JdbcPersistentEntityInformation persistentEntityInformation = context - .getRequiredPersistentEntityInformation(persistentEntity.getType()); + .getRequiredPersistentEntityInformation(repositoryInformation.getDomainType()); JdbcEntityTemplate template = new JdbcEntityTemplate(publisher, jdbcOperations, context); return new SimpleJdbcRepository<>(template, persistentEntityInformation); diff --git a/src/test/java/org/springframework/data/jdbc/core/JdbcEntityTemplateIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/core/JdbcEntityTemplateIntegrationTests.java new file mode 100644 index 00000000..6540a4d3 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/core/JdbcEntityTemplateIntegrationTests.java @@ -0,0 +1,258 @@ +/* + * 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.Collections.*; +import static org.assertj.core.api.Assertions.*; + +import lombok.Data; + +import org.assertj.core.api.SoftAssertions; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; +import org.springframework.data.jdbc.testing.TestConfiguration; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +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; + +/** + * Integration tests for {@link JdbcEntityTemplate}. + * + * @author Jens Schauder + */ +@ContextConfiguration +@Transactional +public class JdbcEntityTemplateIntegrationTests { + + @Configuration + @Import(TestConfiguration.class) + static class Config { + + @Bean + Class testClass() { + return JdbcEntityTemplateIntegrationTests.class; + } + + @Bean + JdbcEntityOperations operations(ApplicationEventPublisher publisher, + NamedParameterJdbcOperations namedParameterJdbcOperations) { + + return new JdbcEntityTemplate(publisher, namedParameterJdbcOperations, new JdbcMappingContext()); + } + } + + @ClassRule public static final SpringClassRule classRule = new SpringClassRule(); + @Rule public SpringMethodRule methodRule = new SpringMethodRule(); + + @Autowired JdbcEntityOperations template; + + LegoSet legoSet = createLegoSet(); + + @Test // DATAJDBC-112 + public void saveAndLoadAnEntityWithReferencedEntityById() { + + template.save(legoSet, LegoSet.class); + + assertThat(legoSet.manual.id).describedAs("id of stored manual").isNotNull(); + + LegoSet reloadedLegoSet = template.findById(legoSet.getId(), LegoSet.class); + + assertThat(reloadedLegoSet.manual).isNotNull(); + + SoftAssertions softly = new SoftAssertions(); + + softly.assertThat(reloadedLegoSet.manual.getId()) // + .isEqualTo(legoSet.getManual().getId()) // + .isNotNull(); + softly.assertThat(reloadedLegoSet.manual.getContent()).isEqualTo(legoSet.getManual().getContent()); + + softly.assertAll(); + } + + @Test // DATAJDBC-112 + public void saveAndLoadManyEntitiesWithReferencedEntity() { + + template.save(legoSet, LegoSet.class); + + Iterable reloadedLegoSets = template.findAll(LegoSet.class); + + assertThat(reloadedLegoSets).hasSize(1).extracting("id", "manual.id", "manual.content") + .contains(tuple(legoSet.getId(), legoSet.getManual().getId(), legoSet.getManual().getContent())); + } + + @Test // DATAJDBC-112 + public void saveAndLoadManyEntitiesByIdWithReferencedEntity() { + + template.save(legoSet, LegoSet.class); + + Iterable reloadedLegoSets = template.findAllById(singletonList(legoSet.getId()), LegoSet.class); + + assertThat(reloadedLegoSets).hasSize(1).extracting("id", "manual.id", "manual.content") + .contains(tuple(legoSet.getId(), legoSet.getManual().getId(), legoSet.getManual().getContent())); + } + + @Test // DATAJDBC-112 + public void saveAndLoadAnEntityWithReferencedNullEntity() { + + legoSet.setManual(null); + + template.save(legoSet, LegoSet.class); + + LegoSet reloadedLegoSet = template.findById(legoSet.getId(), LegoSet.class); + + assertThat(reloadedLegoSet.manual).isNull(); + } + + @Test // DATAJDBC-112 + public void saveAndDeleteAnEntityWithReferencedEntity() { + + template.save(legoSet, LegoSet.class); + + template.delete(legoSet, LegoSet.class); + + SoftAssertions softly = new SoftAssertions(); + + softly.assertThat(template.findAll(LegoSet.class)).isEmpty(); + softly.assertThat(template.findAll(Manual.class)).isEmpty(); + + softly.assertAll(); + } + + @Test // DATAJDBC-112 + public void saveAndDeleteAllWithReferencedEntity() { + + template.save(legoSet, LegoSet.class); + + template.deleteAll(LegoSet.class); + + SoftAssertions softly = new SoftAssertions(); + + assertThat(template.findAll(LegoSet.class)).isEmpty(); + assertThat(template.findAll(Manual.class)).isEmpty(); + + softly.assertAll(); + } + + @Test // DATAJDBC-112 + public void updateReferencedEntityFromNull() { + + legoSet.setManual(null); + template.save(legoSet, LegoSet.class); + + Manual manual = new Manual(23L); + manual.setContent("Some content"); + legoSet.setManual(manual); + + template.save(legoSet, LegoSet.class); + + LegoSet reloadedLegoSet = template.findById(legoSet.getId(), LegoSet.class); + + assertThat(reloadedLegoSet.manual.content).isEqualTo("Some content"); + } + + @Test // DATAJDBC-112 + public void updateReferencedEntityToNull() { + + template.save(legoSet, LegoSet.class); + + legoSet.setManual(null); + + template.save(legoSet, LegoSet.class); + + LegoSet reloadedLegoSet = template.findById(legoSet.getId(), LegoSet.class); + + SoftAssertions softly = new SoftAssertions(); + + softly.assertThat(reloadedLegoSet.manual).isNull(); + softly.assertThat(template.findAll(Manual.class)).describedAs("Manuals failed to delete").isEmpty(); + + softly.assertAll(); + } + + @Test // DATAJDBC-112 + public void replaceReferencedEntity() { + + template.save(legoSet, LegoSet.class); + + Manual manual = new Manual(null); + manual.setContent("other content"); + legoSet.setManual(manual); + + template.save(legoSet, LegoSet.class); + + LegoSet reloadedLegoSet = template.findById(legoSet.getId(), LegoSet.class); + + SoftAssertions softly = new SoftAssertions(); + + softly.assertThat(reloadedLegoSet.manual.content).isEqualTo("other content"); + softly.assertThat(template.findAll(Manual.class)).describedAs("The should be only one manual").hasSize(1); + + softly.assertAll(); + } + + @Test // DATAJDBC-112 + public void changeReferencedEntity() { + + template.save(legoSet, LegoSet.class); + + legoSet.manual.setContent("new content"); + + template.save(legoSet, LegoSet.class); + + LegoSet reloadedLegoSet = template.findById(legoSet.getId(), LegoSet.class); + + assertThat(reloadedLegoSet.manual.content).isEqualTo("new content"); + } + + private static LegoSet createLegoSet() { + + LegoSet entity = new LegoSet(); + entity.setName("Star Destroyer"); + + Manual manual = new Manual(null); + manual.setContent("Accelerates to 99% of light speed. Destroys almost everything. See https://what-if.xkcd.com/1/"); + entity.setManual(manual); + + return entity; + } + + @Data + static class LegoSet { + + @Id private Long id; + + private String name; + + private Manual manual; + } + + @Data + static class Manual { + @Id private final Long id; + + private String content; + } +} diff --git a/src/test/java/org/springframework/data/jdbc/core/SelectBuilderUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/SelectBuilderUnitTests.java new file mode 100644 index 00000000..d5a6edef --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/core/SelectBuilderUnitTests.java @@ -0,0 +1,89 @@ +/* + * 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 org.assertj.core.api.Assertions.*; + +import org.junit.Test; + +/** + * Unit tests for the {@link SelectBuilder}. + * + * @author Jens Schauder + */ +public class SelectBuilderUnitTests { + + @Test // DATAJDBC-112 + public void simplestSelect() { + + String sql = new SelectBuilder("mytable") // + .column(cb -> cb.tableAlias("mytable").column("mycolumn").as("myalias")) // + .build(); + + assertThat(sql).isEqualTo("SELECT mytable.mycolumn AS myalias FROM mytable"); + } + + @Test // DATAJDBC-112 + public void columnWithoutTableAlias() { + + String sql = new SelectBuilder("mytable") // + .column(cb -> cb.column("mycolumn").as("myalias")) // + .build(); + + assertThat(sql).isEqualTo("SELECT mycolumn AS myalias FROM mytable"); + } + + @Test // DATAJDBC-112 + public void whereClause() { + + String sql = new SelectBuilder("mytable") // + .column(cb -> cb.tableAlias("mytable").column("mycolumn").as("myalias")) // + .where(cb -> cb.tableAlias("mytable").column("mycolumn").eq().variable("var")).build(); + + assertThat(sql).isEqualTo("SELECT mytable.mycolumn AS myalias FROM mytable WHERE mytable.mycolumn = :var"); + } + + @Test // DATAJDBC-112 + public void multipleColumnsSelect() { + + String sql = new SelectBuilder("mytable") // + .column(cb -> cb.tableAlias("mytable").column("one").as("oneAlias")) // + .column(cb -> cb.tableAlias("mytable").column("two").as("twoAlias")) // + .build(); + + assertThat(sql).isEqualTo("SELECT mytable.one AS oneAlias, mytable.two AS twoAlias FROM mytable"); + } + + @Test // DATAJDBC-112 + public void join() { + String sql = new SelectBuilder("mytable") // + .column(cb -> cb.tableAlias("mytable").column("mycolumn").as("myalias")) // + .join(jb -> jb.table("other").as("o").where("oid").eq().column("mytable", "id")).build(); + + assertThat(sql).isEqualTo("SELECT mytable.mycolumn AS myalias FROM mytable JOIN other AS o ON o.oid = mytable.id"); + } + + @Test // DATAJDBC-112 + public void outerJoin() { + String sql = new SelectBuilder("mytable") // + .column(cb -> cb.tableAlias("mytable").column("mycolumn").as("myalias")) // + .join(jb -> jb.rightOuter().table("other").as("o").where("oid").eq().column("mytable", "id")).build(); + + assertThat(sql) + .isEqualTo("SELECT mytable.mycolumn AS myalias FROM mytable RIGHT OUTER JOIN other AS o ON o.oid = mytable.id"); + } + +} diff --git a/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorUnitTests.java new file mode 100644 index 00000000..14b027ae --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorUnitTests.java @@ -0,0 +1,113 @@ +/* + * 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 org.assertj.core.api.Assertions.*; + +import org.assertj.core.api.SoftAssertions; +import org.junit.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; +import org.springframework.data.mapping.PropertyPath; + +/** + * Unit tests for the {@link SqlGenerator}. + * + * @author Jens Schauder + */ +public class SqlGeneratorUnitTests { + + JdbcMappingContext context = new JdbcMappingContext(); + JdbcPersistentEntity persistentEntity = context.getRequiredPersistentEntity(DummyEntity.class); + SqlGenerator sqlGenerator = new SqlGenerator(context, persistentEntity, new SqlGeneratorSource(context)); + + @Test // DATAJDBC-112 + public void findOne() { + + String sql = sqlGenerator.getFindOne(); + + new SoftAssertions().assertThat(sql) // + .startsWith("SELECT") // + .contains("DummyEntity.id as id,") // + .contains("DummyEntity.name as name,") // + .contains("ref.id AS ref_id") // + .contains("ref.content AS ref_content").contains(" FROM DummyEntity"); + new SoftAssertions().assertAll(); + } + + @Test // DATAJDBC-112 + public void cascadingDeleteFirstLevel() { + + String sql = sqlGenerator.createDeleteByPath(PropertyPath.from("ref", DummyEntity.class)); + + assertThat(sql).isEqualTo("DELETE FROM ReferencedEntity WHERE DummyEntity = :rootId"); + } + + @Test // DATAJDBC-112 + public void cascadingDeleteAllSecondLevel() { + + String sql = sqlGenerator.createDeleteByPath(PropertyPath.from("ref.further", DummyEntity.class)); + + assertThat(sql).isEqualTo( + "DELETE FROM SecondLevelReferencedEntity WHERE ReferencedEntity IN (SELECT l1id FROM ReferencedEntity WHERE DummyEntity = :rootId)"); + } + + @Test // DATAJDBC-112 + public void deleteAll() { + + String sql = sqlGenerator.createDeleteAllSql(null); + + assertThat(sql).isEqualTo("DELETE FROM DummyEntity"); + } + + @Test // DATAJDBC-112 + public void cascadingDeleteAllFirstLevel() { + + String sql = sqlGenerator.createDeleteAllSql(PropertyPath.from("ref", DummyEntity.class)); + + assertThat(sql).isEqualTo("DELETE FROM ReferencedEntity WHERE DummyEntity IS NOT NULL"); + } + + @Test // DATAJDBC-112 + public void cascadingDeleteSecondLevel() { + + String sql = sqlGenerator.createDeleteAllSql(PropertyPath.from("ref.further", DummyEntity.class)); + + assertThat(sql).isEqualTo( + "DELETE FROM SecondLevelReferencedEntity WHERE ReferencedEntity IN (SELECT l1id FROM ReferencedEntity WHERE DummyEntity IS NOT NULL)"); + } + + static class DummyEntity { + + @Id Long id; + String name; + ReferencedEntity ref; + } + + static class ReferencedEntity { + + @Id Long l1id; + String content; + SecondLevelReferencedEntity further; + } + + static class SecondLevelReferencedEntity { + + @Id Long l2id; + String something; + } +} diff --git a/src/test/java/org/springframework/data/jdbc/core/conversion/JdbcEntityDeleteWriterUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/conversion/JdbcEntityDeleteWriterUnitTests.java new file mode 100644 index 00000000..98296f97 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/core/conversion/JdbcEntityDeleteWriterUnitTests.java @@ -0,0 +1,77 @@ +/* + * 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.conversion; + +import lombok.Data; + +import org.assertj.core.api.Assertions; +import org.assertj.core.groups.Tuple; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.core.conversion.DbAction.Delete; +import org.springframework.data.jdbc.core.conversion.DbChange.Kind; +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; + +/** + * Unit tests for the {@link JdbcEntityDeleteWriter} + * + * @author Jens Schauder + */ +@RunWith(MockitoJUnitRunner.class) +public class JdbcEntityDeleteWriterUnitTests { + + JdbcEntityDeleteWriter converter = new JdbcEntityDeleteWriter(new JdbcMappingContext()); + + @Test + public void deleteDeletesTheEntityAndReferencedEntities() { + + SomeEntity entity = new SomeEntity(23L); + + DbChange dbChange = new DbChange(Kind.DELETE, SomeEntity.class, entity); + + converter.write(entity, dbChange); + + Assertions.assertThat(dbChange.getActions()).extracting(DbAction::getClass, DbAction::getEntityType) + .containsExactly( // + Tuple.tuple(Delete.class, YetAnother.class), // + Tuple.tuple(Delete.class, OtherEntity.class), // + Tuple.tuple(Delete.class, SomeEntity.class) // + ); + } + + @Data + private static class SomeEntity { + + @Id final Long id; + OtherEntity other; + // should not trigger own Dbaction + String name; + } + + @Data + private class OtherEntity { + + @Id final Long id; + YetAnother yetAnother; + } + + @Data + private class YetAnother { + @Id final Long id; + } +} 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 new file mode 100644 index 00000000..f8464ceb --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/core/conversion/JdbcEntityWriterUnitTests.java @@ -0,0 +1,102 @@ +/* + * 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.conversion; + +import static org.assertj.core.api.Assertions.*; + +import lombok.Data; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.core.conversion.DbAction.Delete; +import org.springframework.data.jdbc.core.conversion.DbAction.Insert; +import org.springframework.data.jdbc.core.conversion.DbAction.Update; +import org.springframework.data.jdbc.core.conversion.DbChange.Kind; +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; + +/** + * Unit tests for the {@link JdbcEntityWriter} + * + * @author Jens Schauder + */ +@RunWith(MockitoJUnitRunner.class) +public class JdbcEntityWriterUnitTests { + + JdbcEntityWriter converter = new JdbcEntityWriter(new JdbcMappingContext()); + + @Test // DATAJDBC-112 + public void newEntityGetsConvertedToOneInsert() { + + SomeEntity entity = new SomeEntity(null); + DbChange dbChange = new DbChange(Kind.SAVE, SomeEntity.class, entity); + converter.write(entity, dbChange); + + assertThat(dbChange.getActions()).extracting(DbAction::getClass, DbAction::getEntityType) // + .containsExactly( // + tuple(Insert.class, SomeEntity.class) // + ); + } + + @Test // DATAJDBC-112 + public void existingEntityGetsConvertedToUpdate() { + + SomeEntity entity = new SomeEntity(23L); + + DbChange dbChange = new DbChange(Kind.SAVE, SomeEntity.class, entity); + + converter.write(entity, dbChange); + + assertThat(dbChange.getActions()).extracting(DbAction::getClass, DbAction::getEntityType) // + .containsExactly( // + tuple(Delete.class, OtherEntity.class), // + tuple(Update.class, SomeEntity.class) // + ); + } + + @Test // DATAJDBC-112 + public void referenceTriggersDeletePlusInsert() { + + SomeEntity entity = new SomeEntity(23L); + entity.setOther(new OtherEntity(null)); + + DbChange dbChange = new DbChange(Kind.SAVE, SomeEntity.class, entity); + + converter.write(entity, dbChange); + + assertThat(dbChange.getActions()).extracting(DbAction::getClass, DbAction::getEntityType) // + .containsExactly( // + tuple(Delete.class, OtherEntity.class), // + tuple(Update.class, SomeEntity.class), // + tuple(Insert.class, OtherEntity.class) // + ); + } + + @Data + private static class SomeEntity { + + @Id final Long id; + OtherEntity other; + // should not trigger own Dbaction + String name; + } + + @Data + private class OtherEntity { + @Id final Long id; + } +} diff --git a/src/test/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentPropertyUnitTests.java b/src/test/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentPropertyUnitTests.java index 654516c0..1df1dedb 100644 --- a/src/test/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentPropertyUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentPropertyUnitTests.java @@ -1,19 +1,35 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.data.jdbc.mapping.model; import static org.assertj.core.api.AssertionsForClassTypes.*; +import lombok.Data; + import java.time.LocalDateTime; import java.time.ZonedDateTime; import java.util.Date; -import lombok.Data; - import org.assertj.core.api.Assertions; import org.junit.Test; -import org.springframework.data.jdbc.mapping.context.JdbcMappingContext; import org.springframework.data.mapping.PropertyHandler; /** + * Unti tests for the {@link BasicJdbcPersistentProperty}. + * * @author Jens Schauder */ public class BasicJdbcPersistentPropertyUnitTests { @@ -42,6 +58,7 @@ public class BasicJdbcPersistentPropertyUnitTests { @Data private static class DummyEntity { + private final SomeEnum someEnum; private final LocalDateTime localDateTime; private final ZonedDateTime zonedDateTime; diff --git a/src/test/java/org/springframework/data/jdbc/repository/EventPublishingEntityRowMapperTest.java b/src/test/java/org/springframework/data/jdbc/repository/EventPublishingEntityRowMapperUnitTests.java similarity index 82% rename from src/test/java/org/springframework/data/jdbc/repository/EventPublishingEntityRowMapperTest.java rename to src/test/java/org/springframework/data/jdbc/repository/EventPublishingEntityRowMapperUnitTests.java index d89cb3ed..0c9b54b0 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/EventPublishingEntityRowMapperTest.java +++ b/src/test/java/org/springframework/data/jdbc/repository/EventPublishingEntityRowMapperUnitTests.java @@ -15,14 +15,15 @@ */ package org.springframework.data.jdbc.repository; -import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.*; import lombok.Value; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @@ -30,8 +31,9 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.core.EventPublishingEntityRowMapper; import org.springframework.data.jdbc.mapping.event.AfterCreation; -import org.springframework.data.jdbc.repository.support.JdbcPersistentEntityInformation; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntityInformation; import org.springframework.jdbc.core.RowMapper; /** @@ -41,7 +43,7 @@ import org.springframework.jdbc.core.RowMapper; * @author Oliver Gierke */ @RunWith(MockitoJUnitRunner.class) -public class EventPublishingEntityRowMapperTest { +public class EventPublishingEntityRowMapperUnitTests { @Mock RowMapper rowMapperDelegate; @Mock JdbcPersistentEntityInformation entityInformation; @@ -51,7 +53,7 @@ public class EventPublishingEntityRowMapperTest { public void eventGetsPublishedAfterInstantiation() throws SQLException { when(rowMapperDelegate.mapRow(any(ResultSet.class), anyInt())).thenReturn(new DummyEntity(1L)); - when(entityInformation.getId(any())).thenReturn(1L); + when(entityInformation.getRequiredId(any())).thenReturn(1L); EventPublishingEntityRowMapper rowMapper = new EventPublishingEntityRowMapper<>(rowMapperDelegate, entityInformation, publisher); diff --git a/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java index cff303f9..3fe18ad4 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java @@ -67,7 +67,7 @@ public class EnableJdbcRepositoriesIntegrationTests { @Bean Class testClass() { - return JdbcRepositoryIntegrationTests.class; + return EnableJdbcRepositoriesIntegrationTests.class; } } diff --git a/src/test/resources/EnableJdbcRepositoriesIntegrationTests.sql b/src/test/resources/EnableJdbcRepositoriesIntegrationTests.sql deleted file mode 100644 index f0484b5d..00000000 --- a/src/test/resources/EnableJdbcRepositoriesIntegrationTests.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE TABLE dummyentity ( id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY) \ No newline at end of file diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml index ad5cbef5..af95b497 100644 --- a/src/test/resources/logback.xml +++ b/src/test/resources/logback.xml @@ -7,9 +7,11 @@ - + - + + + diff --git a/src/test/resources/org.springframework.data.jdbc.core/JdbcEntityTemplateIntegrationTests-hsql.sql b/src/test/resources/org.springframework.data.jdbc.core/JdbcEntityTemplateIntegrationTests-hsql.sql new file mode 100644 index 00000000..a20900fd --- /dev/null +++ b/src/test/resources/org.springframework.data.jdbc.core/JdbcEntityTemplateIntegrationTests-hsql.sql @@ -0,0 +1,5 @@ +CREATE TABLE LEGOSET ( id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, NAME VARCHAR(30)); +CREATE TABLE MANUAL ( id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, LEGOSET BIGINT, CONTENT VARCHAR(2000)); + +ALTER TABLE MANUAL ADD FOREIGN KEY (LEGOSET) +REFERENCES LEGOSET(id); diff --git a/src/test/resources/org.springframework.data.jdbc.repository.config/EnableJdbcRepositoriesIntegrationTests-hsql.sql b/src/test/resources/org.springframework.data.jdbc.repository.config/EnableJdbcRepositoriesIntegrationTests-hsql.sql new file mode 100644 index 00000000..ee4d4de3 --- /dev/null +++ b/src/test/resources/org.springframework.data.jdbc.repository.config/EnableJdbcRepositoriesIntegrationTests-hsql.sql @@ -0,0 +1 @@ +CREATE TABLE DummyEntity ( id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY) \ No newline at end of file