Browse Source

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.
pull/9/merge
Jens Schauder 9 years ago committed by Greg Turnquist
parent
commit
dda4223341
No known key found for this signature in database
GPG Key ID: CB2FA4D512B5C413
  1. 14
      src/main/java/org/springframework/data/jdbc/mapping/context/JdbcMappingContext.java
  2. 34
      src/main/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentProperty.java
  3. 5
      src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentEntity.java
  4. 11
      src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentProperty.java
  5. 2
      src/main/java/org/springframework/data/jdbc/repository/EntityRowMapper.java
  6. 46
      src/main/java/org/springframework/data/jdbc/repository/JdbcEntityOperations.java
  7. 292
      src/main/java/org/springframework/data/jdbc/repository/JdbcEntityTemplate.java
  8. 184
      src/main/java/org/springframework/data/jdbc/repository/SimpleJdbcRepository.java
  9. 23
      src/main/java/org/springframework/data/jdbc/repository/SqlGenerator.java
  10. 13
      src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java
  11. 54
      src/test/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentPropertyUnitTests.java
  12. 190
      src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryPropertyConversionIntegrationTests.java
  13. 1
      src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryPropertyConversionIntegrationTests-hsql.sql

14
src/main/java/org/springframework/data/jdbc/mapping/context/JdbcMappingContext.java

@ -16,8 +16,11 @@ @@ -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; @@ -30,14 +33,14 @@ import org.springframework.data.util.TypeInformation;
* @author Jens Schauder
* @since 2.0
*/
public class JdbcMappingContext extends AbstractMappingContext<JdbcPersistentEntityImpl<?>, JdbcPersistentProperty> {
public class JdbcMappingContext extends AbstractMappingContext<JdbcPersistentEntity<?>, JdbcPersistentProperty> {
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentEntity(org.springframework.data.util.TypeInformation)
*/
@Override
protected <T> JdbcPersistentEntityImpl<?> createPersistentEntity(TypeInformation<T> typeInformation) {
protected <T> JdbcPersistentEntity<T> createPersistentEntity(TypeInformation<T> typeInformation) {
return new JdbcPersistentEntityImpl<>(typeInformation);
}
@ -46,8 +49,13 @@ public class JdbcMappingContext extends AbstractMappingContext<JdbcPersistentEnt @@ -46,8 +49,13 @@ public class JdbcMappingContext extends AbstractMappingContext<JdbcPersistentEnt
* @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentProperty(org.springframework.data.mapping.model.Property, org.springframework.data.mapping.model.MutablePersistentEntity, org.springframework.data.mapping.model.SimpleTypeHolder)
*/
@Override
protected JdbcPersistentProperty createPersistentProperty(Property property, JdbcPersistentEntityImpl<?> owner,
protected JdbcPersistentProperty createPersistentProperty(Property property, JdbcPersistentEntity<?> owner,
SimpleTypeHolder simpleTypeHolder) {
return new BasicJdbcPersistentProperty(property, owner, simpleTypeHolder);
}
@SuppressWarnings("unchecked")
public <T> JdbcPersistentEntityInformation<T, ?> getRequiredPersistentEntityInformation(Class<T> type) {
return new BasicJdbcPersistentEntityInformation<>((JdbcPersistentEntity<T>) getRequiredPersistentEntity(type));
}
}

34
src/main/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentProperty.java

@ -15,11 +15,20 @@ @@ -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; @@ -30,6 +39,14 @@ import org.springframework.data.mapping.model.SimpleTypeHolder;
public class BasicJdbcPersistentProperty extends AnnotationBasedPersistentProperty<JdbcPersistentProperty>
implements JdbcPersistentProperty {
private static final Map<Class<?>, 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 @@ -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));
}
}

5
src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentEntity.java

@ -16,18 +16,19 @@ @@ -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<T> extends PersistentEntity<T, JdbcPersistentProperty> {
public interface JdbcPersistentEntity<T> extends MutablePersistentEntity<T, JdbcPersistentProperty> {
/**
* Returns the name of the table backing the given entity.
*
* @return
* @return the table name.
*/
String getTableName();

11
src/main/java/org/springframework/data/jdbc/mapping/model/JdbcPersistentProperty.java

@ -27,9 +27,16 @@ import org.springframework.data.mapping.PersistentProperty; @@ -27,9 +27,16 @@ import org.springframework.data.mapping.PersistentProperty;
public interface JdbcPersistentProperty extends PersistentProperty<JdbcPersistentProperty> {
/**
* 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();
}

2
src/main/java/org/springframework/data/jdbc/repository/EntityRowMapper.java

@ -48,7 +48,7 @@ class EntityRowMapper<T> implements RowMapper<T> { @@ -48,7 +48,7 @@ class EntityRowMapper<T> implements RowMapper<T> {
private final JdbcPersistentEntity<T> entity;
private final EntityInstantiator instantiator = new ClassGeneratingEntityInstantiator();
private final ConversionService conversions = new DefaultConversionService();
private final ConversionService conversions;
/*
* (non-Javadoc)

46
src/main/java/org/springframework/data/jdbc/repository/JdbcEntityOperations.java

@ -0,0 +1,46 @@ @@ -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 <em>Domain Type</em>.
*
* @author Jens Schauder
*/
public interface JdbcEntityOperations {
<T> void insert(T instance, Class<T> domainType);
<T> void update(T instance, Class<T> domainType);
<T> void deleteById(Object id, Class<T> domainType);
<T> void delete(T entity, Class<T> domainType);
long count(Class<?> domainType);
<T> T findById(Object id, Class<T> domainType);
<T> Iterable<T> findAllById(Iterable<?> ids, Class<T> domainType);
<T> Iterable<T> findAll(Class<T> domainType);
<T> boolean existsById(Object id, Class<T> domainType);
<T> void deleteAll(Iterable<? extends T> entities, Class<T> domainType);
void deleteAll(Class<?> domainType);
}

292
src/main/java/org/springframework/data/jdbc/repository/JdbcEntityTemplate.java

@ -0,0 +1,292 @@ @@ -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 <S> void insert(S instance, Class<S> domainType) {
publisher.publishEvent(new BeforeInsert(instance));
KeyHolder holder = new GeneratedKeyHolder();
JdbcPersistentEntity<S> persistentEntity = getRequiredPersistentEntity(domainType);
JdbcPersistentEntityInformation<S, ?> entityInformation = context
.getRequiredPersistentEntityInformation(domainType);
Map<String, Object> 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 <S> void update(S instance, Class<S> domainType) {
JdbcPersistentEntity<S> persistentEntity = getRequiredPersistentEntity(domainType);
JdbcPersistentEntityInformation<S, ?> 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 <S> void delete(S entity, Class<S> domainType) {
JdbcPersistentEntityInformation<S, ?> entityInformation = context
.getRequiredPersistentEntityInformation(domainType);
delete(Identifier.of(entityInformation.getRequiredId(entity)), Optional.of(entity), domainType);
}
@Override
public <S> void deleteById(Object id, Class<S> 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> T findById(Object id, Class<T> domainType) {
String findOneSql = sql(domainType).getFindOne();
MapSqlParameterSource parameter = createIdParameterSource(id, domainType);
return operations.queryForObject(findOneSql, parameter, getEntityRowMapper(domainType));
}
@Override
public <T> boolean existsById(Object id, Class<T> domainType) {
String existsSql = sql(domainType).getExists();
MapSqlParameterSource parameter = createIdParameterSource(id, domainType);
return operations.queryForObject(existsSql, parameter, Boolean.class);
}
@Override
public <T> Iterable<T> findAll(Class<T> domainType) {
return operations.query(sql(domainType).getFindAll(), getEntityRowMapper(domainType));
}
@Override
public <T> Iterable<T> findAllById(Iterable<?> ids, Class<T> 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 <T> void deleteAll(Iterable<? extends T> entities, Class<T> domainType) {
JdbcPersistentEntityInformation<T, ?> 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<Object> 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 <T> MapSqlParameterSource createIdParameterSource(Object id, Class<T> domainType) {
return new MapSqlParameterSource("id",
convert(id, getRequiredPersistentEntity(domainType).getRequiredIdProperty().getColumnType()));
}
private <S> Map<String, Object> getPropertyMap(final S instance, JdbcPersistentEntity<S> persistentEntity) {
Map<String, Object> parameters = new HashMap<>();
persistentEntity.doWithProperties((PropertyHandler<JdbcPersistentProperty>) property -> {
Optional<Object> value = persistentEntity.getPropertyAccessor(instance).getProperty(property);
Object convertedValue = convert(value.orElse(null), property.getColumnType());
parameters.put(property.getColumnName(), convertedValue);
});
return parameters;
}
private <S, ID> ID getIdValueOrNull(S instance, JdbcPersistentEntity<S> persistentEntity) {
EntityInformation<S, ID> entityInformation = new BasicJdbcPersistentEntityInformation<>(persistentEntity);
Optional<ID> idValue = entityInformation.getId(instance);
return isIdPropertySimpleTypeAndValueZero(idValue, persistentEntity) ? null
: idValue.orElseThrow(() -> new IllegalStateException("idValue must have a value at this point."));
}
private <S> void setIdFromJdbc(S instance, KeyHolder holder, JdbcPersistentEntity<S> persistentEntity) {
JdbcPersistentEntityInformation<S, ?> 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 <S> Optional<Object> getIdFromHolder(KeyHolder holder, JdbcPersistentEntity<S> 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> V convert(Object from, Class<V> to) {
return conversions.convert(from, to);
}
private <S, ID> boolean isIdPropertySimpleTypeAndValueZero(Optional<ID> idValue,
JdbcPersistentEntity<S> persistentEntity) {
Optional<JdbcPersistentProperty> 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 <S> JdbcPersistentEntity<S> getRequiredPersistentEntity(Class<S> domainType) {
return (JdbcPersistentEntity<S>) context.getRequiredPersistentEntity(domainType);
}
private SqlGenerator sql(Class<?> domainType) {
return new SqlGenerator(context.getRequiredPersistentEntity(domainType));
}
private <T> EntityRowMapper<T> getEntityRowMapper(Class<T> domainType) {
return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), conversions);
}
}

184
src/main/java/org/springframework/data/jdbc/repository/SimpleJdbcRepository.java

@ -17,78 +17,31 @@ package org.springframework.data.jdbc.repository; @@ -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<T, ID extends Serializable> implements CrudRepository<T, ID> {
public class SimpleJdbcRepository<T, ID> implements CrudRepository<T, ID> {
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<T> persistentEntity;
private final JdbcPersistentEntityInformation<T, ID> entityInformation;
private final NamedParameterJdbcOperations operations;
private final SqlGenerator sql;
private final EntityRowMapper<T> 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<T> 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<T, ID> 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<T, ID extends Serializable> implements CrudRep @@ -99,9 +52,9 @@ public class SimpleJdbcRepository<T, ID extends Serializable> implements CrudRep
public <S extends T> 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<T, ID extends Serializable> implements CrudRep @@ -126,8 +79,7 @@ public class SimpleJdbcRepository<T, ID extends Serializable> implements CrudRep
@Override
public Optional<T> 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<T, ID extends Serializable> implements CrudRep @@ -136,7 +88,7 @@ public class SimpleJdbcRepository<T, ID extends Serializable> 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<T, ID extends Serializable> implements CrudRep @@ -145,7 +97,7 @@ public class SimpleJdbcRepository<T, ID extends Serializable> implements CrudRep
*/
@Override
public Iterable<T> findAll() {
return operations.query(sql.getFindAll(), entityRowMapper);
return entityOperations.findAll(entityInformation.getJavaType());
}
/*
@ -154,7 +106,7 @@ public class SimpleJdbcRepository<T, ID extends Serializable> implements CrudRep @@ -154,7 +106,7 @@ public class SimpleJdbcRepository<T, ID extends Serializable> implements CrudRep
*/
@Override
public Iterable<T> findAllById(Iterable<ID> ids) {
return operations.query(sql.getFindAllInList(), new MapSqlParameterSource("ids", ids), entityRowMapper);
return entityOperations.findAllById(ids, entityInformation.getJavaType());
}
/*
@ -163,7 +115,7 @@ public class SimpleJdbcRepository<T, ID extends Serializable> implements CrudRep @@ -163,7 +115,7 @@ public class SimpleJdbcRepository<T, ID extends Serializable> 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<T, ID extends Serializable> implements CrudRep @@ -172,7 +124,7 @@ public class SimpleJdbcRepository<T, ID extends Serializable> 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<T, ID extends Serializable> implements CrudRep @@ -181,7 +133,7 @@ public class SimpleJdbcRepository<T, ID extends Serializable> 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<T, ID extends Serializable> implements CrudRep @@ -190,111 +142,11 @@ public class SimpleJdbcRepository<T, ID extends Serializable> implements CrudRep
*/
@Override
public void deleteAll(Iterable<? extends T> entities) {
List<ID> 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 <S extends T> Map<String, Object> getPropertyMap(final S instance) {
Map<String, Object> parameters = new HashMap<>();
this.persistentEntity.doWithProperties((PropertyHandler<JdbcPersistentProperty>) property -> {
Object value = persistentEntity.getPropertyAccessor(instance).getProperty(property);
parameters.put(property.getColumnName(), value);
});
return parameters;
}
private <S extends T> void doInsert(S instance) {
publisher.publishEvent(new BeforeInsert(instance));
KeyHolder holder = new GeneratedKeyHolder();
Map<String, Object> 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 <S extends T> 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 <S extends T> 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<Object> 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> V convert(Object idValueFromJdbc, Class<V> targetType) {
return conversions.convert(idValueFromJdbc, targetType);
}
private void doDelete(Specified specifiedId, Optional<Object> optionalEntity) {
publisher.publishEvent(new BeforeDelete(specifiedId, optionalEntity));
operations.update(sql.getDeleteById(), new MapSqlParameterSource("id", specifiedId.getValue()));
publisher.publishEvent(new AfterDelete(specifiedId, optionalEntity));
}
private <S extends T> 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());
}
}

23
src/main/java/org/springframework/data/jdbc/repository/SqlGenerator.java

@ -24,7 +24,7 @@ import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty; @@ -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 { @@ -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 { @@ -63,8 +61,6 @@ class SqlGenerator {
existsSql = createExistsSql();
countSql = createCountSql();
insertSql = createInsertSql();
updateSql = createUpdateSql();
deleteByIdSql = createDeleteSql();
@ -72,7 +68,7 @@ class SqlGenerator { @@ -72,7 +68,7 @@ class SqlGenerator {
deleteByListSql = createDeleteByListSql();
}
private <T> void initPropertyNames() {
private void initPropertyNames() {
entity.doWithProperties((PropertyHandler<JdbcPersistentProperty>) p -> {
propertyNames.add(p.getName());
@ -98,8 +94,8 @@ class SqlGenerator { @@ -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 { @@ -138,20 +134,21 @@ class SqlGenerator {
return String.format("select count(*) from %s where %s = :id", entity.getTableName(), entity.getIdColumn());
}
private <T> 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<String> 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 <T> String createUpdateSql() {
private String createUpdateSql() {
String updateTemplate = "update %s set %s where %s = :%s";

13
src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java

@ -20,7 +20,7 @@ import lombok.RequiredArgsConstructor; @@ -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 { @@ -49,13 +49,18 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport {
return new BasicJdbcPersistentEntityInformation<T, ID>((JdbcPersistentEntity<T>) 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

54
src/test/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentPropertyUnitTests.java

@ -0,0 +1,54 @@ @@ -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<JdbcPersistentProperty>) 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
}
}

190
src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryPropertyConversionIntegrationTests.java

@ -0,0 +1,190 @@ @@ -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>) 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<Date> 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<EntityWithColumnsRequiringConversions, LocalDateTime> {}
@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
}
}

1
src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryPropertyConversionIntegrationTests-hsql.sql

@ -0,0 +1 @@ @@ -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))
Loading…
Cancel
Save