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))