From fec4f99257520e4d1d0d75c618fdfcd822bfecef Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Thu, 20 Jul 2017 14:07:15 +0200 Subject: [PATCH] DATAJDBC-112 - Allow aggregate roots to refer to other entities. Aggregate roots that reference other entities get correctly stored, updated and loaded. In order to keep this a simple as possible, referenced entities get deleted and recreated on each update. While this is inefficient it gets the job done and makes sense if we assume referenced entities are owned by that aggregate root. References to other aggregate root could be implemented by using just the id. With the event mechanism one could even inject repositories into the entities, so they could provide getters to the actual referenced aggregate root. The process of storing an entity now works as follows: The request to store an entity is converted into a DbChange object containing a list of DbActions. Each DbAction represents a single change made to the data base, I.e. an insert, update or delete statement. This DbChange then gets interpreted by the interpreter to actually create sql statements and execute them. This should allow various customizations in the future and for users: - We could read the DbActions from the aggregate root itself, if it implements a given interface - A user can modify the way DbActions get created for a requested change. Either by replacing the Jdbc[Delete]EntityWriter that performs this conversion or by modifying the DbChange in an event handler. - By changing the interpreter actually performing the actions one could change completely the sql used for the actions. Since we currently only do one-to-one relationships selects are simply joins. In order to encapsulate the construction of select statements from entities the SelectBuilder got introduced. Also SqlGenerators and the generated sql is properly cached, if it does not depend on additional information. This commit also contains necessary changes due to DATACMNS-1101. Related issues: DATACMNS-1101 DATAJDBC-115 --- .../{repository => core}/EntityRowMapper.java | 70 +++-- .../EventPublishingEntityRowMapper.java | 4 +- .../JdbcEntityOperations.java | 10 +- .../JdbcEntityTemplate.java | 191 +++++++---- .../data/jdbc/core/JdbcInterpreter.java | 88 ++++++ .../data/jdbc/core/SelectBuilder.java | 296 ++++++++++++++++++ .../data/jdbc/core/SqlGenerator.java | 267 ++++++++++++++++ .../data/jdbc/core/SqlGeneratorSource.java | 43 +++ .../{repository => core}/UnableToSetId.java | 4 +- .../data/jdbc/core/conversion/DbAction.java | 183 +++++++++++ .../data/jdbc/core/conversion/DbChange.java | 52 +++ .../jdbc/core/conversion/Interpreter.java | 33 ++ .../conversion/JdbcEntityDeleteWriter.java | 58 ++++ .../core/conversion/JdbcEntityWriter.java | 122 ++++++++ .../conversion/JdbcEntityWriterSupport.java | 46 +++ .../BasicJdbcPersistentEntityInformation.java | 8 +- .../model/BasicJdbcPersistentProperty.java | 35 ++- .../JdbcMappingContext.java | 56 +++- .../model/JdbcPersistentEntityImpl.java | 9 +- .../JdbcPersistentEntityInformation.java | 9 +- .../jdbc/mapping/model/PropertyPaths.java | 44 +++ .../jdbc/repository/SimpleJdbcRepository.java | 21 +- .../data/jdbc/repository/SqlGenerator.java | 173 ---------- .../support/JdbcRepositoryFactory.java | 15 +- .../JdbcEntityTemplateIntegrationTests.java | 258 +++++++++++++++ .../jdbc/core/SelectBuilderUnitTests.java | 89 ++++++ .../data/jdbc/core/SqlGeneratorUnitTests.java | 113 +++++++ .../JdbcEntityDeleteWriterUnitTests.java | 77 +++++ .../conversion/JdbcEntityWriterUnitTests.java | 102 ++++++ .../BasicJdbcPersistentPropertyUnitTests.java | 23 +- ...ntPublishingEntityRowMapperUnitTests.java} | 12 +- ...nableJdbcRepositoriesIntegrationTests.java | 2 +- ...EnableJdbcRepositoriesIntegrationTests.sql | 1 - src/test/resources/logback.xml | 6 +- ...dbcEntityTemplateIntegrationTests-hsql.sql | 5 + ...eJdbcRepositoriesIntegrationTests-hsql.sql | 1 + 36 files changed, 2207 insertions(+), 319 deletions(-) rename src/main/java/org/springframework/data/jdbc/{repository => core}/EntityRowMapper.java (59%) rename src/main/java/org/springframework/data/jdbc/{repository => core}/EventPublishingEntityRowMapper.java (92%) rename src/main/java/org/springframework/data/jdbc/{repository => core}/JdbcEntityOperations.java (85%) rename src/main/java/org/springframework/data/jdbc/{repository => core}/JdbcEntityTemplate.java (63%) create mode 100644 src/main/java/org/springframework/data/jdbc/core/JdbcInterpreter.java create mode 100644 src/main/java/org/springframework/data/jdbc/core/SelectBuilder.java create mode 100644 src/main/java/org/springframework/data/jdbc/core/SqlGenerator.java create mode 100644 src/main/java/org/springframework/data/jdbc/core/SqlGeneratorSource.java rename src/main/java/org/springframework/data/jdbc/{repository => core}/UnableToSetId.java (89%) create mode 100644 src/main/java/org/springframework/data/jdbc/core/conversion/DbAction.java create mode 100644 src/main/java/org/springframework/data/jdbc/core/conversion/DbChange.java create mode 100644 src/main/java/org/springframework/data/jdbc/core/conversion/Interpreter.java create mode 100644 src/main/java/org/springframework/data/jdbc/core/conversion/JdbcEntityDeleteWriter.java create mode 100644 src/main/java/org/springframework/data/jdbc/core/conversion/JdbcEntityWriter.java create mode 100644 src/main/java/org/springframework/data/jdbc/core/conversion/JdbcEntityWriterSupport.java rename src/main/java/org/springframework/data/jdbc/{repository/support => mapping/model}/BasicJdbcPersistentEntityInformation.java (80%) rename src/main/java/org/springframework/data/jdbc/mapping/{context => model}/JdbcMappingContext.java (61%) rename src/main/java/org/springframework/data/jdbc/{repository/support => mapping/model}/JdbcPersistentEntityInformation.java (85%) create mode 100644 src/main/java/org/springframework/data/jdbc/mapping/model/PropertyPaths.java delete mode 100644 src/main/java/org/springframework/data/jdbc/repository/SqlGenerator.java create mode 100644 src/test/java/org/springframework/data/jdbc/core/JdbcEntityTemplateIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/jdbc/core/SelectBuilderUnitTests.java create mode 100644 src/test/java/org/springframework/data/jdbc/core/SqlGeneratorUnitTests.java create mode 100644 src/test/java/org/springframework/data/jdbc/core/conversion/JdbcEntityDeleteWriterUnitTests.java create mode 100644 src/test/java/org/springframework/data/jdbc/core/conversion/JdbcEntityWriterUnitTests.java rename src/test/java/org/springframework/data/jdbc/repository/{EventPublishingEntityRowMapperTest.java => EventPublishingEntityRowMapperUnitTests.java} (82%) delete mode 100644 src/test/resources/EnableJdbcRepositoriesIntegrationTests.sql create mode 100644 src/test/resources/org.springframework.data.jdbc.core/JdbcEntityTemplateIntegrationTests-hsql.sql create mode 100644 src/test/resources/org.springframework.data.jdbc.repository.config/EnableJdbcRepositoriesIntegrationTests-hsql.sql 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