From dda42233410b548f96b3e89460122223697656a0 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Wed, 7 Jun 2017 14:49:44 +0200 Subject: [PATCH] DATAJDBC-104 - Introducing conversions for storing properties. All properties get passed through a conversion step to convert them to types that can be passed to JDBC drivers. Conversions happen in the newly introduced JdbcEnitityTemplate. It contains most of the code formerly found in the SimpleJdbcRepository. --- .../mapping/context/JdbcMappingContext.java | 14 +- .../model/BasicJdbcPersistentProperty.java | 34 ++ .../mapping/model/JdbcPersistentEntity.java | 5 +- .../mapping/model/JdbcPersistentProperty.java | 11 +- .../data/jdbc/repository/EntityRowMapper.java | 2 +- .../jdbc/repository/JdbcEntityOperations.java | 46 +++ .../jdbc/repository/JdbcEntityTemplate.java | 292 ++++++++++++++++++ .../jdbc/repository/SimpleJdbcRepository.java | 184 ++--------- .../data/jdbc/repository/SqlGenerator.java | 23 +- .../support/JdbcRepositoryFactory.java | 13 +- .../BasicJdbcPersistentPropertyUnitTests.java | 54 ++++ ...oryPropertyConversionIntegrationTests.java | 190 ++++++++++++ ...ropertyConversionIntegrationTests-hsql.sql | 1 + 13 files changed, 678 insertions(+), 191 deletions(-) create mode 100644 src/main/java/org/springframework/data/jdbc/repository/JdbcEntityOperations.java create mode 100644 src/main/java/org/springframework/data/jdbc/repository/JdbcEntityTemplate.java create mode 100644 src/test/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentPropertyUnitTests.java create mode 100644 src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryPropertyConversionIntegrationTests.java create mode 100644 src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryPropertyConversionIntegrationTests-hsql.sql diff --git a/src/main/java/org/springframework/data/jdbc/mapping/context/JdbcMappingContext.java b/src/main/java/org/springframework/data/jdbc/mapping/context/JdbcMappingContext.java index 9d228ce78..5d77de6ba 100644 --- a/src/main/java/org/springframework/data/jdbc/mapping/context/JdbcMappingContext.java +++ b/src/main/java/org/springframework/data/jdbc/mapping/context/JdbcMappingContext.java @@ -16,8 +16,11 @@ package org.springframework.data.jdbc.mapping.context; 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 org.springframework.data.mapping.context.AbstractMappingContext; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.Property; @@ -30,14 +33,14 @@ import org.springframework.data.util.TypeInformation; * @author Jens Schauder * @since 2.0 */ -public class JdbcMappingContext extends AbstractMappingContext, JdbcPersistentProperty> { +public class JdbcMappingContext extends AbstractMappingContext, JdbcPersistentProperty> { /* * (non-Javadoc) * @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentEntity(org.springframework.data.util.TypeInformation) */ @Override - protected JdbcPersistentEntityImpl createPersistentEntity(TypeInformation typeInformation) { + protected JdbcPersistentEntity createPersistentEntity(TypeInformation typeInformation) { return new JdbcPersistentEntityImpl<>(typeInformation); } @@ -46,8 +49,13 @@ public class JdbcMappingContext extends AbstractMappingContext owner, + protected JdbcPersistentProperty createPersistentProperty(Property property, JdbcPersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { return new BasicJdbcPersistentProperty(property, owner, simpleTypeHolder); } + + @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/BasicJdbcPersistentProperty.java b/src/main/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentProperty.java index cda280193..fbdcd4411 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 @@ -15,11 +15,20 @@ */ package org.springframework.data.jdbc.mapping.model; +import java.time.ZonedDateTime; +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; import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty; import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.util.ClassUtils; /** * Meta data about a property to be used by repository implementations. @@ -30,6 +39,14 @@ import org.springframework.data.mapping.model.SimpleTypeHolder; public class BasicJdbcPersistentProperty extends AnnotationBasedPersistentProperty implements JdbcPersistentProperty { + private static final Map, Class> javaToDbType = new LinkedHashMap<>(); + + static { + javaToDbType.put(Enum.class, String.class); + javaToDbType.put(ZonedDateTime.class, String.class); + javaToDbType.put(Temporal.class, Date.class); + } + /** * Creates a new {@link AnnotationBasedPersistentProperty}. * @@ -58,4 +75,21 @@ public class BasicJdbcPersistentProperty extends AnnotationBasedPersistentProper public String getColumnName() { return getName(); } + + /** + * The type to be used to store this property in the database. + * + * @return a {@link Class} that is suitable for usage with JDBC drivers + */ + @SuppressWarnings("unchecked") + @Override + public Class getColumnType() { + + Class type = getType(); + return javaToDbType.entrySet().stream() // + .filter(e -> e.getKey().isAssignableFrom(type)) // + .map(e -> (Class)e.getValue()) // + .findFirst() // + .orElseGet(() -> ClassUtils.resolvePrimitiveIfNecessary(type)); + } } diff --git a/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentEntity.java b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentEntity.java index ee7a75d34..017fa3c19 100644 --- a/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentEntity.java +++ b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentEntity.java @@ -16,18 +16,19 @@ package org.springframework.data.jdbc.mapping.model; import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.model.MutablePersistentEntity; /** * @author Jens Schauder * @author Oliver Gierke * @since 2.0 */ -public interface JdbcPersistentEntity extends PersistentEntity { +public interface JdbcPersistentEntity extends MutablePersistentEntity { /** * Returns the name of the table backing the given entity. * - * @return + * @return the table name. */ String getTableName(); diff --git a/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentProperty.java b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentProperty.java index 7a8ffc634..32be8cb9e 100644 --- a/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentProperty.java +++ b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentProperty.java @@ -27,9 +27,16 @@ import org.springframework.data.mapping.PersistentProperty; public interface JdbcPersistentProperty extends PersistentProperty { /** - * Returns the name of the column backing that property. + * Returns the name of the column backing this property. * - * @return + * @return the name of the column backing this property. */ String getColumnName(); + + /** + * The type to be used to store this property in the database. + * + * @return a {@link Class} that is suitable for usage with JDBC drivers + */ + Class getColumnType(); } diff --git a/src/main/java/org/springframework/data/jdbc/repository/EntityRowMapper.java b/src/main/java/org/springframework/data/jdbc/repository/EntityRowMapper.java index 4288685eb..97f96a304 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/EntityRowMapper.java +++ b/src/main/java/org/springframework/data/jdbc/repository/EntityRowMapper.java @@ -48,7 +48,7 @@ class EntityRowMapper implements RowMapper { private final JdbcPersistentEntity entity; private final EntityInstantiator instantiator = new ClassGeneratingEntityInstantiator(); - private final ConversionService conversions = new DefaultConversionService(); + private final ConversionService conversions; /* * (non-Javadoc) diff --git a/src/main/java/org/springframework/data/jdbc/repository/JdbcEntityOperations.java b/src/main/java/org/springframework/data/jdbc/repository/JdbcEntityOperations.java new file mode 100644 index 000000000..413b53c9b --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/JdbcEntityOperations.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.repository; + +/** + * Specifies a operations one can perform on a database, based on an Domain Type. + * + * @author Jens Schauder + */ +public interface JdbcEntityOperations { + + void insert(T instance, Class domainType); + + void update(T instance, Class domainType); + + void deleteById(Object id, Class domainType); + + void delete(T entity, Class domainType); + + long count(Class domainType); + + T findById(Object id, Class domainType); + + Iterable findAllById(Iterable ids, Class domainType); + + Iterable findAll(Class domainType); + + boolean existsById(Object id, Class domainType); + + void deleteAll(Iterable entities, Class domainType); + + void deleteAll(Class domainType); +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/JdbcEntityTemplate.java b/src/main/java/org/springframework/data/jdbc/repository/JdbcEntityTemplate.java new file mode 100644 index 000000000..bf81906fc --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/JdbcEntityTemplate.java @@ -0,0 +1,292 @@ +/* + * 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 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; +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.mapping.event.AfterDelete; +import org.springframework.data.jdbc.mapping.event.AfterInsert; +import org.springframework.data.jdbc.mapping.event.AfterUpdate; +import org.springframework.data.jdbc.mapping.event.BeforeDelete; +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.JdbcPersistentEntity; +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.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; + +/** + * @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" + + " the id property in a before insert event handler, or ensure the database creates a value and your " + + "JDBC driver returns it."; + + private final ApplicationEventPublisher publisher; + private final NamedParameterJdbcOperations operations; + private final JdbcMappingContext context; + private final ConversionService conversions = getDefaultConversionService(); + + private static GenericConversionService getDefaultConversionService() { + + DefaultConversionService conversionService = new DefaultConversionService(); + Jsr310Converters.getConvertersToRegister().forEach(conversionService::addConverter); + + return conversionService; + } + + @Override + public void insert(S instance, Class domainType) { + + publisher.publishEvent(new BeforeInsert(instance)); + + KeyHolder holder = new GeneratedKeyHolder(); + JdbcPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); + JdbcPersistentEntityInformation entityInformation = context + .getRequiredPersistentEntityInformation(domainType); + + Map propertyMap = getPropertyMap(instance, persistentEntity); + + Object idValue = getIdValueOrNull(instance, persistentEntity); + JdbcPersistentProperty idProperty = persistentEntity.getRequiredIdProperty(); + propertyMap.put(idProperty.getColumnName(), convert(idValue, idProperty.getColumnType())); + + operations.update(sql(domainType).getInsert(idValue == null), new MapSqlParameterSource(propertyMap), holder); + + setIdFromJdbc(instance, holder, persistentEntity); + + if (entityInformation.isNew(instance)) { + throw new IllegalStateException(String.format(ENTITY_NEW_AFTER_INSERT, persistentEntity)); + } + + publisher.publishEvent(new AfterInsert(Identifier.of(entityInformation.getRequiredId(instance)), instance)); + + } + + @Override + public void update(S instance, Class domainType) { + + JdbcPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); + JdbcPersistentEntityInformation entityInformation = context + .getRequiredPersistentEntityInformation(domainType); + + Specified specifiedId = Identifier.of(entityInformation.getRequiredId(instance)); + publisher.publishEvent(new BeforeUpdate(specifiedId, instance)); + operations.update(sql(domainType).getUpdate(), getPropertyMap(instance, persistentEntity)); + 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); + } + + @Override + public T findById(Object id, Class domainType) { + + String findOneSql = sql(domainType).getFindOne(); + MapSqlParameterSource parameter = createIdParameterSource(id, domainType); + return operations.queryForObject(findOneSql, parameter, getEntityRowMapper(domainType)); + } + + @Override + public boolean existsById(Object id, Class domainType) { + + String existsSql = sql(domainType).getExists(); + MapSqlParameterSource parameter = createIdParameterSource(id, domainType); + return operations.queryForObject(existsSql, parameter, Boolean.class); + } + + @Override + public Iterable findAll(Class domainType) { + return operations.query(sql(domainType).getFindAll(), getEntityRowMapper(domainType)); + } + + @Override + public Iterable findAllById(Iterable ids, Class domainType) { + + String findAllInListSql = sql(domainType).getFindAllInList(); + Class targetType = getRequiredPersistentEntity(domainType).getRequiredIdProperty().getColumnType(); + MapSqlParameterSource parameter = new MapSqlParameterSource("ids", + StreamSupport.stream(ids.spliterator(), false).map(id -> convert(id, targetType)).collect(Collectors.toList())); + + return operations.query(findAllInListSql, parameter, getEntityRowMapper(domainType)); + } + + @Override + public void deleteAll(Iterable entities, Class domainType) { + + JdbcPersistentEntityInformation entityInformation = context + .getRequiredPersistentEntityInformation(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 deleteAll(Class domainType) { + operations.getJdbcOperations().update(sql(domainType).getDeleteAll()); + } + + private void delete(Specified specifiedId, Optional optionalEntity, Class domainType) { + + publisher.publishEvent(new BeforeDelete(specifiedId, optionalEntity)); + + String deleteByIdSql = sql(domainType).getDeleteById(); + + MapSqlParameterSource parameter = createIdParameterSource(specifiedId.getValue(), domainType); + + operations.update(deleteByIdSql, parameter); + + publisher.publishEvent(new AfterDelete(specifiedId, optionalEntity)); + } + + private MapSqlParameterSource createIdParameterSource(Object id, Class domainType) { + return new MapSqlParameterSource("id", + convert(id, getRequiredPersistentEntity(domainType).getRequiredIdProperty().getColumnType())); + } + + private Map getPropertyMap(final S instance, JdbcPersistentEntity persistentEntity) { + + Map parameters = new HashMap<>(); + + persistentEntity.doWithProperties((PropertyHandler) property -> { + + Optional value = persistentEntity.getPropertyAccessor(instance).getProperty(property); + + Object convertedValue = convert(value.orElse(null), property.getColumnType()); + parameters.put(property.getColumnName(), convertedValue); + }); + + return parameters; + } + + private ID getIdValueOrNull(S instance, JdbcPersistentEntity persistentEntity) { + + EntityInformation entityInformation = new BasicJdbcPersistentEntityInformation<>(persistentEntity); + + Optional idValue = entityInformation.getId(instance); + + return isIdPropertySimpleTypeAndValueZero(idValue, persistentEntity) ? null + : idValue.orElseThrow(() -> new IllegalStateException("idValue must have a value at this point.")); + } + + private void setIdFromJdbc(S instance, KeyHolder holder, JdbcPersistentEntity persistentEntity) { + + JdbcPersistentEntityInformation entityInformation = new BasicJdbcPersistentEntityInformation<>( + persistentEntity); + + try { + + getIdFromHolder(holder, persistentEntity).ifPresent(it -> { + + Class targetType = persistentEntity.getRequiredIdProperty().getType(); + Object converted = convert(it, targetType); + entityInformation.setId(instance, Optional.of(converted)); + }); + + } catch (NonTransientDataAccessException e) { + throw new UnableToSetId("Unable to set id of " + instance, e); + } + } + + private Optional getIdFromHolder(KeyHolder holder, JdbcPersistentEntity persistentEntity) { + + try { + // MySQL just returns one value with a special name + return Optional.ofNullable(holder.getKey()); + } catch (InvalidDataAccessApiUsageException e) { + // Postgres returns a value for each column + return Optional.ofNullable(holder.getKeys().get(persistentEntity.getIdColumn())); + } + } + + private V convert(Object from, Class to) { + return conversions.convert(from, to); + } + + private boolean isIdPropertySimpleTypeAndValueZero(Optional 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)); + } + + @SuppressWarnings("unchecked") + private JdbcPersistentEntity getRequiredPersistentEntity(Class domainType) { + return (JdbcPersistentEntity) context.getRequiredPersistentEntity(domainType); + } + + private SqlGenerator sql(Class domainType) { + return new SqlGenerator(context.getRequiredPersistentEntity(domainType)); + } + + private EntityRowMapper getEntityRowMapper(Class domainType) { + return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), conversions); + } +} 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 9fbd97211..bfef2c0ab 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/SimpleJdbcRepository.java +++ b/src/main/java/org/springframework/data/jdbc/repository/SimpleJdbcRepository.java @@ -17,78 +17,31 @@ package org.springframework.data.jdbc.repository; import java.io.Serializable; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Optional; -import java.util.stream.Collectors; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.dao.NonTransientDataAccessException; -import org.springframework.data.jdbc.mapping.event.AfterDelete; -import org.springframework.data.jdbc.mapping.event.AfterInsert; -import org.springframework.data.jdbc.mapping.event.AfterUpdate; -import org.springframework.data.jdbc.mapping.event.BeforeDelete; -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.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 org.springframework.data.mapping.PropertyHandler; import org.springframework.data.repository.CrudRepository; -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; /** * @author Jens Schauder * @since 2.0 */ -public class SimpleJdbcRepository implements CrudRepository { +public class SimpleJdbcRepository implements CrudRepository { - private static final String ENTITY_NEW_AFTER_INSERT = "Entity [%s] still 'new' after insert. Please set either" - + " the id property in a before insert event handler, or ensure the database creates a value and your " - + "JDBC driver returns it."; - - private final JdbcPersistentEntity persistentEntity; private final JdbcPersistentEntityInformation entityInformation; - private final NamedParameterJdbcOperations operations; - private final SqlGenerator sql; - private final EntityRowMapper entityRowMapper; - private final ApplicationEventPublisher publisher; - private final ConversionService conversions = new DefaultConversionService(); + + private final JdbcEntityOperations entityOperations; /** * Creates a new {@link SimpleJdbcRepository} for the given {@link JdbcPersistentEntityImpl} - * - * @param persistentEntity - * @param jdbcOperations - * @param publisher */ - public SimpleJdbcRepository(JdbcPersistentEntity persistentEntity, NamedParameterJdbcOperations jdbcOperations, - ApplicationEventPublisher publisher) { - - Assert.notNull(persistentEntity, "PersistentEntity must not be null."); - Assert.notNull(jdbcOperations, "JdbcOperations must not be null."); - Assert.notNull(publisher, "Publisher must not be null."); + public SimpleJdbcRepository(JdbcEntityTemplate entityOperations, + JdbcPersistentEntityInformation entityInformation) { - this.persistentEntity = persistentEntity; - this.entityInformation = new BasicJdbcPersistentEntityInformation<>(persistentEntity); - this.operations = jdbcOperations; - this.publisher = publisher; - - this.entityRowMapper = new EntityRowMapper<>(persistentEntity); - this.sql = new SqlGenerator(persistentEntity); + this.entityOperations = entityOperations; + this.entityInformation = entityInformation; } /* @@ -99,9 +52,9 @@ public class SimpleJdbcRepository implements CrudRep public S save(S instance) { if (entityInformation.isNew(instance)) { - doInsert(instance); + entityOperations.insert(instance, entityInformation.getJavaType()); } else { - doUpdate(instance); + entityOperations.update(instance, entityInformation.getJavaType()); } return instance; @@ -126,8 +79,7 @@ public class SimpleJdbcRepository implements CrudRep @Override public Optional findById(ID id) { - return Optional - .ofNullable(operations.queryForObject(sql.getFindOne(), new MapSqlParameterSource("id", id), entityRowMapper)); + return Optional.ofNullable(entityOperations.findById(id, entityInformation.getJavaType())); } /* @@ -136,7 +88,7 @@ public class SimpleJdbcRepository implements CrudRep */ @Override public boolean existsById(ID id) { - return operations.queryForObject(sql.getExists(), new MapSqlParameterSource("id", id), Boolean.class); + return entityOperations.existsById(id, entityInformation.getJavaType()); } /* @@ -145,7 +97,7 @@ public class SimpleJdbcRepository implements CrudRep */ @Override public Iterable findAll() { - return operations.query(sql.getFindAll(), entityRowMapper); + return entityOperations.findAll(entityInformation.getJavaType()); } /* @@ -154,7 +106,7 @@ public class SimpleJdbcRepository implements CrudRep */ @Override public Iterable findAllById(Iterable ids) { - return operations.query(sql.getFindAllInList(), new MapSqlParameterSource("ids", ids), entityRowMapper); + return entityOperations.findAllById(ids, entityInformation.getJavaType()); } /* @@ -163,7 +115,7 @@ public class SimpleJdbcRepository implements CrudRep */ @Override public long count() { - return operations.getJdbcOperations().queryForObject(sql.getCount(), Long.class); + return entityOperations.count(entityInformation.getJavaType()); } /* @@ -172,7 +124,7 @@ public class SimpleJdbcRepository implements CrudRep */ @Override public void deleteById(ID id) { - doDelete(Identifier.of(id), Optional.empty()); + entityOperations.deleteById(id, entityInformation.getJavaType()); } /* @@ -181,7 +133,7 @@ public class SimpleJdbcRepository implements CrudRep */ @Override public void delete(T instance) { - doDelete(Identifier.of(entityInformation.getRequiredId(instance)), Optional.of(instance)); + entityOperations.delete(instance, entityInformation.getJavaType()); } /* @@ -190,111 +142,11 @@ public class SimpleJdbcRepository implements CrudRep */ @Override public void deleteAll(Iterable entities) { - - List idList = Streamable.of(entities).stream() // - .map(e -> entityInformation.getRequiredId(e)) // - .collect(Collectors.toList()); - - MapSqlParameterSource sqlParameterSource = new MapSqlParameterSource("ids", idList); - operations.update(sql.getDeleteByList(), sqlParameterSource); + entityOperations.deleteAll(entities, entityInformation.getJavaType()); } @Override public void deleteAll() { - operations.getJdbcOperations().update(sql.getDeleteAll()); - } - - private Map getPropertyMap(final S instance) { - - Map parameters = new HashMap<>(); - - this.persistentEntity.doWithProperties((PropertyHandler) property -> { - - Object value = persistentEntity.getPropertyAccessor(instance).getProperty(property); - parameters.put(property.getColumnName(), value); - }); - - return parameters; - } - - private void doInsert(S instance) { - - publisher.publishEvent(new BeforeInsert(instance)); - - KeyHolder holder = new GeneratedKeyHolder(); - - Map propertyMap = getPropertyMap(instance); - propertyMap.put(persistentEntity.getRequiredIdProperty().getColumnName(), getIdValueOrNull(instance)); - - operations.update(sql.getInsert(), new MapSqlParameterSource(propertyMap), holder); - setIdFromJdbc(instance, holder); - - if (entityInformation.isNew(instance)) { - throw new IllegalStateException(String.format(ENTITY_NEW_AFTER_INSERT, persistentEntity)); - } - - publisher.publishEvent(new AfterInsert(Identifier.of(entityInformation.getRequiredId(instance)), instance)); - } - - private ID getIdValueOrNull(S instance) { - - ID idValue = entityInformation.getId(instance); - return isIdPropertySimpleTypeAndValueZero(idValue) ? null : idValue; - } - - private boolean isIdPropertySimpleTypeAndValueZero(ID idValue) { - - JdbcPersistentProperty idProperty = persistentEntity.getIdProperty(); - return idValue == null // - || idProperty == null // - || (idProperty.getType() == int.class && idValue.equals(0)) // - || (idProperty.getType() == long.class && idValue.equals(0L)); - } - - private void setIdFromJdbc(S instance, KeyHolder holder) { - - try { - - getIdFromHolder(holder).ifPresent(it -> { - - Class targetType = persistentEntity.getRequiredIdProperty().getType(); - Object converted = convert(it, targetType); - entityInformation.setId(instance, converted); - }); - - } catch (NonTransientDataAccessException e) { - throw new UnableToSetId("Unable to set id of " + instance, e); - } - } - - private Optional getIdFromHolder(KeyHolder holder) { - - try { - // MySQL just returns one value with a special name - return Optional.ofNullable(holder.getKey()); - } catch (InvalidDataAccessApiUsageException e) { - // Postgres returns a value for each column - return Optional.ofNullable(holder.getKeys().get(persistentEntity.getIdColumn())); - } - - } - - private V convert(Object idValueFromJdbc, Class targetType) { - return conversions.convert(idValueFromJdbc, targetType); - } - - private void doDelete(Specified specifiedId, Optional optionalEntity) { - - publisher.publishEvent(new BeforeDelete(specifiedId, optionalEntity)); - operations.update(sql.getDeleteById(), new MapSqlParameterSource("id", specifiedId.getValue())); - publisher.publishEvent(new AfterDelete(specifiedId, optionalEntity)); - } - - private void doUpdate(S instance) { - - Specified specifiedId = Identifier.of(entityInformation.getRequiredId(instance)); - publisher.publishEvent(new BeforeUpdate(specifiedId, instance)); - operations.update(sql.getUpdate(), getPropertyMap(instance)); - publisher.publishEvent(new AfterUpdate(specifiedId, instance)); + entityOperations.deleteAll(entityInformation.getJavaType()); } } diff --git a/src/main/java/org/springframework/data/jdbc/repository/SqlGenerator.java b/src/main/java/org/springframework/data/jdbc/repository/SqlGenerator.java index 89ad5918d..e86e096c8 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/SqlGenerator.java +++ b/src/main/java/org/springframework/data/jdbc/repository/SqlGenerator.java @@ -24,7 +24,7 @@ import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty; import org.springframework.data.mapping.PropertyHandler; /** - * Generates SQL statements to be used by {@Link SimpleJdbcRepository} + * Generates SQL statements to be used by {@link SimpleJdbcRepository} * * @author Jens Schauder * @since 2.0 @@ -38,8 +38,6 @@ class SqlGenerator { private final String existsSql; private final String countSql; - private final String insertSql; - private final String updateSql; private final String deleteByIdSql; @@ -63,8 +61,6 @@ class SqlGenerator { existsSql = createExistsSql(); countSql = createCountSql(); - insertSql = createInsertSql(); - updateSql = createUpdateSql(); deleteByIdSql = createDeleteSql(); @@ -72,7 +68,7 @@ class SqlGenerator { deleteByListSql = createDeleteByListSql(); } - private void initPropertyNames() { + private void initPropertyNames() { entity.doWithProperties((PropertyHandler) p -> { propertyNames.add(p.getName()); @@ -98,8 +94,8 @@ class SqlGenerator { return findOneSql; } - String getInsert() { - return insertSql; + String getInsert(boolean excludeId) { + return createInsertSql(excludeId); } String getUpdate() { @@ -138,20 +134,21 @@ class SqlGenerator { return String.format("select count(*) from %s where %s = :id", entity.getTableName(), entity.getIdColumn()); } - private String createCountSql() { + private String createCountSql() { return String.format("select count(*) from %s", entity.getTableName()); } - private String createInsertSql() { + private String createInsertSql(boolean excludeId) { String insertTemplate = "insert into %s (%s) values (%s)"; - String tableColumns = String.join(", ", nonIdPropertyNames); - String parameterNames = nonIdPropertyNames.stream().collect(Collectors.joining(", :", ":", "")); + 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() { + private String createUpdateSql() { String updateTemplate = "update %s set %s where %s = :%s"; 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 35299aaa3..a29b193fc 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 @@ -20,7 +20,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.jdbc.mapping.context.JdbcMappingContext; import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; -import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntityImpl; +import org.springframework.data.jdbc.repository.JdbcEntityTemplate; import org.springframework.data.jdbc.repository.SimpleJdbcRepository; import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.repository.core.RepositoryInformation; @@ -49,13 +49,18 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport { return new BasicJdbcPersistentEntityInformation((JdbcPersistentEntity) persistentEntity); } + @SuppressWarnings("unchecked") @Override protected Object getTargetRepository(RepositoryInformation repositoryInformation) { - JdbcPersistentEntity persistentEntity = context - .getRequiredPersistentEntity(repositoryInformation.getDomainType()); + JdbcPersistentEntity persistentEntity = context // + .getPersistentEntity(repositoryInformation.getDomainType()) // + .orElseThrow(() -> new IllegalArgumentException("%s does not represent a persistent entity")); // + JdbcPersistentEntityInformation persistentEntityInformation = context + .getRequiredPersistentEntityInformation(persistentEntity.getType()); + JdbcEntityTemplate template = new JdbcEntityTemplate(publisher, jdbcOperations, context); - return new SimpleJdbcRepository<>(persistentEntity, jdbcOperations, publisher); + return new SimpleJdbcRepository<>(template, persistentEntityInformation); } @Override 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 new file mode 100644 index 000000000..654516c02 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentPropertyUnitTests.java @@ -0,0 +1,54 @@ +package org.springframework.data.jdbc.mapping.model; + +import static org.assertj.core.api.AssertionsForClassTypes.*; + +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; + +/** + * @author Jens Schauder + */ +public class BasicJdbcPersistentPropertyUnitTests { + + @Test // DATAJDBC-104 + public void enumGetsStoredAsString() { + JdbcPersistentEntity persistentEntity = new JdbcMappingContext().getRequiredPersistentEntity(DummyEntity.class); + + persistentEntity.doWithProperties((PropertyHandler) p -> { + switch (p.getName()) { + case "someEnum": + assertThat(p.getColumnType()).isEqualTo(String.class); + break; + case "localDateTime": + assertThat(p.getColumnType()).isEqualTo(Date.class); + break; + case "zonedDateTime": + assertThat(p.getColumnType()).isEqualTo(String.class); + break; + default: + Assertions.fail("property with out assert: " + p.getName()); + } + }); + + } + + @Data + private static class DummyEntity { + private final SomeEnum someEnum; + private final LocalDateTime localDateTime; + private final ZonedDateTime zonedDateTime; + } + + private enum SomeEnum { + @SuppressWarnings("unused") + ALPHA + } +} diff --git a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryPropertyConversionIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryPropertyConversionIntegrationTests.java new file mode 100644 index 000000000..99b167add --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryPropertyConversionIntegrationTests.java @@ -0,0 +1,190 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository; + +import static java.util.Arrays.*; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.*; + +import lombok.Data; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Collections; +import java.util.Date; + +import org.assertj.core.api.Condition; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +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.event.BeforeInsert; +import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; +import org.springframework.data.jdbc.testing.TestConfiguration; +import org.springframework.data.repository.CrudRepository; +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; + +/** + * Tests storing and retrieving various data types that are considered essential and that might need conversion to + * something the database driver can handle. + * + * @author Jens Schauder + */ +@ContextConfiguration +@Transactional +public class JdbcRepositoryPropertyConversionIntegrationTests { + + @Configuration + @Import(TestConfiguration.class) + static class Config { + + @Autowired JdbcRepositoryFactory factory; + + @Bean + Class testClass() { + return JdbcRepositoryPropertyConversionIntegrationTests.class; + } + + @Bean + DummyEntityRepository dummyEntityRepository() { + return factory.getRepository(DummyEntityRepository.class); + } + + @Bean + ApplicationListener applicationListener() { + return (ApplicationListener) beforeInsert -> ((EntityWithColumnsRequiringConversions) beforeInsert + .getEntity()).setIdTimestamp(LocalDateTime.now()); + } + + } + + @ClassRule public static final SpringClassRule classRule = new SpringClassRule(); + @Rule public SpringMethodRule methodRule = new SpringMethodRule(); + + @Autowired DummyEntityRepository repository; + + @Test // DATAJDBC-95 + public void saveAndLoadAnEntity() { + + EntityWithColumnsRequiringConversions entity = repository.save(createDummyEntity()); + + assertThat(repository.findById(entity.getIdTimestamp())).hasValueSatisfying(it -> { + + assertThat(it.getIdTimestamp()).isEqualTo(entity.getIdTimestamp()); + assertThat(it.getSomeEnum()).isEqualTo(entity.getSomeEnum()); + assertThat(it.getBigDecimal()).isEqualTo(entity.getBigDecimal()); + assertThat(it.isBool()).isEqualTo(entity.isBool()); + assertThat(it.getBigInteger()).isEqualTo(entity.getBigInteger()); + assertThat(it.getDate()).is(representingTheSameAs(entity.getDate())); + assertThat(it.getLocalDateTime()).isEqualTo(entity.getLocalDateTime()); + }); + } + + @Test // DATAJDBC-95 + public void existsById() { + + EntityWithColumnsRequiringConversions entity = repository.save(createDummyEntity()); + + assertThat(repository.existsById(entity.getIdTimestamp())).isTrue(); + } + + @Test // DATAJDBC-95 + public void findAllById() { + + EntityWithColumnsRequiringConversions entity = repository.save(createDummyEntity()); + + assertThat(repository.findAllById(Collections.singletonList(entity.getIdTimestamp()))).hasSize(1); + } + + @Test // DATAJDBC-95 + public void deleteAll() { + + EntityWithColumnsRequiringConversions entity = repository.save(createDummyEntity()); + + repository.deleteAll(singletonList(entity)); + + assertThat(repository.findAll()).hasSize(0); + } + + @Test // DATAJDBC-95 + public void deleteById() { + + EntityWithColumnsRequiringConversions entity = repository.save(createDummyEntity()); + + repository.deleteById(entity.getIdTimestamp()); + + assertThat(repository.findAll()).hasSize(0); + } + + private static EntityWithColumnsRequiringConversions createDummyEntity() { + + EntityWithColumnsRequiringConversions entity = new EntityWithColumnsRequiringConversions(); + entity.setSomeEnum(SomeEnum.VALUE); + entity.setBigDecimal(new BigDecimal(Double.MAX_VALUE).multiply(BigDecimal.TEN)); + entity.setBool(true); + entity.setBigInteger(BigInteger.valueOf(Long.MAX_VALUE).multiply(BigInteger.TEN)); + entity.setDate(new Date()); + entity.setLocalDateTime(LocalDateTime.now()); + + return entity; + } + + private Condition representingTheSameAs(Date other) { + + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + String expected = format.format(other); + + return new Condition<>(date -> format.format(date).equals(expected), expected); + } + + interface DummyEntityRepository extends CrudRepository {} + + @Data + static class EntityWithColumnsRequiringConversions { + + // ensures conversion on id querying + @Id private LocalDateTime idTimestamp; + + boolean bool; + + SomeEnum someEnum; + + BigDecimal bigDecimal; + + BigInteger bigInteger; + + Date date; + + LocalDateTime localDateTime; + + } + + enum SomeEnum { + VALUE + } +} diff --git a/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryPropertyConversionIntegrationTests-hsql.sql b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryPropertyConversionIntegrationTests-hsql.sql new file mode 100644 index 000000000..e60eb17be --- /dev/null +++ b/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryPropertyConversionIntegrationTests-hsql.sql @@ -0,0 +1 @@ +CREATE TABLE ENTITYWITHCOLUMNSREQUIRINGCONVERSIONS ( idTimestamp DATETIME PRIMARY KEY, bool boolean, SOMEENUM VARCHAR(100), bigDecimal DECIMAL(1025), bigInteger DECIMAL(20), date DATETIME, localDateTime DATETIME, zonedDateTime VARCHAR(30))