DATAJDBC-123 - MyBatis integration on DbAction level.

If MyBatis-spring is available and a SqlSessionFactory in the Application Context we look for matching mapped sql statements instead of using the default generated SQL.

Introduced DataAccessStrategy as a new abstraction level, operating on a single entity. JdbcEntityOperations only contain operations related to complete Aggregates. Thereby also solving DATAJDBC-132.

Integration tests ending in HsqlIntegrationTest will only get executed using HsqlDb.

Related issue: DATAJDBC-132.
This commit is contained in:
Jens Schauder
2017-08-28 15:05:30 +02:00
committed by Greg Turnquist
parent fd2ae05795
commit 806bb24ff5
30 changed files with 1989 additions and 544 deletions

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.core;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty;
import org.springframework.data.mapping.PropertyPath;
/**
* Delegates each methods to the {@link DataAccessStrategy}s passed to the constructor in turn until the first that does
* not throw an exception.
*
* @author Jens Schauder
*/
public class CascadingDataAccessStrategy implements DataAccessStrategy {
private final List<DataAccessStrategy> strategies;
public CascadingDataAccessStrategy(List<DataAccessStrategy> strategies) {
this.strategies = new ArrayList<>(strategies);
}
@Override
public <T> void insert(T instance, Class<T> domainType, Map<String, Object> additionalParameters) {
collectVoid(das -> das.insert(instance, domainType, additionalParameters));
}
@Override
public <S> void update(S instance, Class<S> domainType) {
collectVoid(das -> das.update(instance, domainType));
}
@Override
public void delete(Object id, Class<?> domainType) {
collectVoid(das -> das.delete(id, domainType));
}
@Override
public void delete(Object rootId, PropertyPath propertyPath) {
collectVoid(das -> das.delete(rootId, propertyPath));
}
@Override
public <T> void deleteAll(Class<T> domainType) {
collectVoid(das -> das.deleteAll(domainType));
}
@Override
public <T> void deleteAll(PropertyPath propertyPath) {
collectVoid(das -> das.deleteAll(propertyPath));
}
@Override
public long count(Class<?> domainType) {
return collect(das -> das.count(domainType));
}
@Override
public <T> T findById(Object id, Class<T> domainType) {
return collect(das -> das.findById(id, domainType));
}
@Override
public <T> Iterable<T> findAll(Class<T> domainType) {
return collect(das -> das.findAll(domainType));
}
@Override
public <T> Iterable<T> findAllById(Iterable<?> ids, Class<T> domainType) {
return collect(das -> das.findAllById(ids, domainType));
}
@Override
public <T> Iterable<T> findAllByProperty(Object rootId, JdbcPersistentProperty property) {
return collect(das -> das.findAllByProperty(rootId, property));
}
@Override
public <T> boolean existsById(Object id, Class<T> domainType) {
return collect(das -> das.existsById(id, domainType));
}
private <T> T collect(Function<DataAccessStrategy, T> function) {
return strategies.stream().collect(new FunctionCollector<>(function));
}
private void collectVoid(Consumer<DataAccessStrategy> consumer) {
collect(das -> {
consumer.accept(das);
return null;
});
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.core;
import java.util.Map;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty;
import org.springframework.data.mapping.PropertyPath;
/**
* Abstraction for accesses to the database that should be implementable with a single SQL statement and relates to a single entity as opposed to {@link JdbcEntityOperations} which provides interactions related to complete aggregates.
*
* @author Jens Schauder
*/
public interface DataAccessStrategy {
<T> void insert(T instance, Class<T> domainType, Map<String, Object> additionalParameters);
<S> void update(S instance, Class<S> domainType);
void delete(Object id, Class<?> domainType);
/** Deletes all entities reachable via {@literal propertyPath} from the instance identified by {@literal rootId}.
*
* @param rootId Id of the root object on which the {@literal propertyPath} is based.
* @param propertyPath Leading from the root object to the entities to be deleted.
*/
void delete(Object rootId, PropertyPath propertyPath);
<T> void deleteAll(Class<T> domainType);
/** Deletes all entities reachable via {@literal propertyPath} from any instance.
*
* @param propertyPath Leading from the root object to the entities to be deleted.
*/
<T> void deleteAll(PropertyPath propertyPath);
long count(Class<?> domainType);
<T> T findById(Object id, Class<T> domainType);
<T> Iterable<T> findAll(Class<T> domainType);
<T> Iterable<T> findAllById(Iterable<?> ids, Class<T> domainType);
/**
* Finds all entities reachable via {@literal property} from the instance identified by {@literal rootId}.
*
* @param rootId Id of the root object on which the {@literal propertyPath} is based.
* @param property Leading from the root object to the entities to be found.
*/
<T> Iterable<T> findAllByProperty(Object rootId, JdbcPersistentProperty property);
<T> boolean existsById(Object id, Class<T> domainType);
}

View File

@@ -0,0 +1,320 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.core;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.data.convert.Jsr310Converters;
import org.springframework.data.jdbc.mapping.model.BasicJdbcPersistentEntityInformation;
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntityInformation;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty;
import org.springframework.data.jdbc.support.JdbcUtil;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.repository.core.EntityInformation;
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;
/**
* Generates and executes actual SQL statements.
*
* @author Jens Schauder
*/
public class DefaultDataAccessStrategy implements DataAccessStrategy {
private static final String ENTITY_NEW_AFTER_INSERT = "Entity [%s] still 'new' after insert. Please set either"
+ " the id property in a BeforeInsert event handler, or ensure the database creates a value and your "
+ "JDBC driver returns it.";
private final SqlGeneratorSource sqlGeneratorSource;
private final NamedParameterJdbcOperations operations;
private final JdbcMappingContext context;
private final ConversionService conversions = getDefaultConversionService();
private final DataAccessStrategy accessStrategy;
public DefaultDataAccessStrategy(SqlGeneratorSource sqlGeneratorSource, NamedParameterJdbcOperations operations,
JdbcMappingContext context, DataAccessStrategy accessStrategy) {
this.sqlGeneratorSource = sqlGeneratorSource;
this.operations = operations;
this.context = context;
this.accessStrategy = accessStrategy;
}
/**
* creates a {@link DefaultDataAccessStrategy} which references it self for resolution of recursive data accesses.
*
* Only suitable if this is the only access strategy in use.
*/
public DefaultDataAccessStrategy(SqlGeneratorSource sqlGeneratorSource, NamedParameterJdbcOperations operations,
JdbcMappingContext context) {
this.sqlGeneratorSource = sqlGeneratorSource;
this.operations = operations;
this.context = context;
this.accessStrategy = this;
}
@Override
public <T> void insert(T instance, Class<T> domainType, Map<String, Object> additionalParameters) {
KeyHolder holder = new GeneratedKeyHolder();
JdbcPersistentEntity<T> persistentEntity = getRequiredPersistentEntity(domainType);
JdbcPersistentEntityInformation<T, ?> entityInformation = context
.getRequiredPersistentEntityInformation(domainType);
MapSqlParameterSource parameterSource = getPropertyMap(instance, persistentEntity);
Object idValue = getIdValueOrNull(instance, persistentEntity);
JdbcPersistentProperty idProperty = persistentEntity.getRequiredIdProperty();
parameterSource.addValue(idProperty.getColumnName(), convert(idValue, idProperty.getColumnType()),
JdbcUtil.sqlTypeFor(idProperty.getColumnType()));
additionalParameters.forEach(parameterSource::addValue);
operations.update(sql(domainType).getInsert(idValue == null, additionalParameters.keySet()), parameterSource,
holder);
setIdFromJdbc(instance, holder, persistentEntity);
if (entityInformation.isNew(instance)) {
throw new IllegalStateException(String.format(ENTITY_NEW_AFTER_INSERT, persistentEntity));
}
}
@Override
public <S> void update(S instance, Class<S> domainType) {
JdbcPersistentEntity<S> persistentEntity = getRequiredPersistentEntity(domainType);
operations.update(sql(domainType).getUpdate(), getPropertyMap(instance, persistentEntity));
}
@Override
public void delete(Object id, Class<?> domainType) {
String deleteByIdSql = sql(domainType).getDeleteById();
MapSqlParameterSource parameter = createIdParameterSource(id, domainType);
operations.update(deleteByIdSql, parameter);
}
@Override
public void delete(Object rootId, PropertyPath propertyPath) {
JdbcPersistentEntity<?> rootEntity = context.getRequiredPersistentEntity(propertyPath.getOwningType());
JdbcPersistentProperty referencingProperty = rootEntity.getRequiredPersistentProperty(propertyPath.getSegment());
Assert.notNull(referencingProperty, "No property found matching the PropertyPath " + propertyPath);
String format = sql(rootEntity.getType()).createDeleteByPath(propertyPath);
HashMap<String, Object> parameters = new HashMap<>();
parameters.put("rootId", rootId);
operations.update(format, parameters);
}
@Override
public <T> void deleteAll(Class<T> domainType) {
operations.getJdbcOperations().update(sql(domainType).createDeleteAllSql(null));
}
@Override
public <T> void deleteAll(PropertyPath propertyPath) {
operations.getJdbcOperations().update(sql(propertyPath.getOwningType().getType()).createDeleteAllSql(propertyPath));
}
@SuppressWarnings("ConstantConditions")
@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);
try {
return operations.queryForObject(findOneSql, parameter, getEntityRowMapper(domainType));
} catch (EmptyResultDataAccessException e) {
return null;
}
}
@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> Iterable<T> findAllByProperty(Object rootId, JdbcPersistentProperty property) {
Class<?> actualType = property.getActualType();
String findAllByProperty = sql(actualType).getFindAllByProperty(property.getReverseColumnName());
MapSqlParameterSource parameter = new MapSqlParameterSource(property.getReverseColumnName(), rootId);
return (Iterable<T>) operations.query(findAllByProperty, parameter, getEntityRowMapper(actualType));
}
@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);
}
private static GenericConversionService getDefaultConversionService() {
DefaultConversionService conversionService = new DefaultConversionService();
Jsr310Converters.getConvertersToRegister().forEach(conversionService::addConverter);
return conversionService;
}
private <S> MapSqlParameterSource getPropertyMap(final S instance, JdbcPersistentEntity<S> persistentEntity) {
MapSqlParameterSource parameters = new MapSqlParameterSource();
persistentEntity.doWithProperties((PropertyHandler<JdbcPersistentProperty>) property -> {
if (!property.isEntity()) {
Object value = persistentEntity.getPropertyAccessor(instance).getProperty(property);
Object convertedValue = convert(value, property.getColumnType());
parameters.addValue(property.getColumnName(), convertedValue, JdbcUtil.sqlTypeFor(property.getColumnType()));
}
});
return parameters;
}
private <S, ID> ID getIdValueOrNull(S instance, JdbcPersistentEntity<S> persistentEntity) {
EntityInformation<S, ID> entityInformation = new BasicJdbcPersistentEntityInformation<>(persistentEntity);
ID idValue = entityInformation.getId(instance);
return isIdPropertySimpleTypeAndValueZero(idValue, persistentEntity) ? null : idValue;
}
private <S, ID> boolean isIdPropertySimpleTypeAndValueZero(ID idValue, JdbcPersistentEntity<S> persistentEntity) {
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> 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, 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 <T> EntityRowMapper<T> getEntityRowMapper(Class<T> domainType) {
return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), conversions, context, accessStrategy);
}
private <T> MapSqlParameterSource createIdParameterSource(Object id, Class<T> domainType) {
return new MapSqlParameterSource("id",
convert(id, getRequiredPersistentEntity(domainType).getRequiredIdProperty().getColumnType()));
}
@SuppressWarnings("unchecked")
private <S> JdbcPersistentEntity<S> getRequiredPersistentEntity(Class<S> domainType) {
return (JdbcPersistentEntity<S>) context.getRequiredPersistentEntity(domainType);
}
private <V> V convert(Object from, Class<V> to) {
if (from == null) {
return null;
}
JdbcPersistentEntity<?> persistentEntity = context.getPersistentEntity(from.getClass());
Object id = persistentEntity == null ? null : persistentEntity.getIdentifierAccessor(from).getIdentifier();
return conversions.convert(id == null ? from : id, to);
}
private SqlGenerator sql(Class<?> domainType) {
return sqlGeneratorSource.getSqlGenerator(domainType);
}
}

View File

@@ -28,7 +28,7 @@ import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity;
/**
* {@link Interpreter} for {@link DbAction}s using a {@link JdbcEntityTemplate} for performing actual database
* {@link Interpreter} for {@link DbAction}s using a {@link DataAccessStrategy} for performing actual database
* interactions.
*
* @author Jens Schauder
@@ -36,16 +36,45 @@ import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity;
class DefaultJdbcInterpreter implements Interpreter {
private final JdbcMappingContext context;
private final JdbcEntityTemplate template;
private final DataAccessStrategy accessStrategy;
DefaultJdbcInterpreter(JdbcMappingContext context, JdbcEntityTemplate template) {
DefaultJdbcInterpreter(JdbcMappingContext context, DataAccessStrategy accessStrategy) {
this.context = context;
this.template = template;
this.accessStrategy = accessStrategy;
}
@Override
public <T> void interpret(Insert<T> insert) {
accessStrategy.insert(insert.getEntity(), insert.getEntityType(), createAdditionalColumnValues(insert));
}
@Override
public <T> void interpret(Update<T> update) {
accessStrategy.update(update.getEntity(), update.getEntityType());
}
@Override
public <T> void interpret(Delete<T> delete) {
if (delete.getPropertyPath() == null) {
accessStrategy.delete(delete.getRootId(), delete.getEntityType());
} else {
accessStrategy.delete(delete.getRootId(), delete.getPropertyPath());
}
}
@Override
public <T> void interpret(DeleteAll<T> delete) {
if (delete.getEntityType() == null) {
accessStrategy.deleteAll(delete.getPropertyPath());
} else {
accessStrategy.deleteAll(delete.getEntityType());
}
}
private <T> Map<String, Object> createAdditionalColumnValues(Insert<T> insert) {
Map<String, Object> additionalColumnValues = new HashMap<>();
DbAction dependingOn = insert.getDependingOn();
@@ -59,28 +88,7 @@ class DefaultJdbcInterpreter implements Interpreter {
additionalColumnValues.put(columnName, identifier);
}
template.insert(insert.getEntity(), insert.getEntityType(), additionalColumnValues);
return additionalColumnValues;
}
@Override
public <T> void interpret(Update<T> update) {
template.update(update.getEntity(), update.getEntityType());
}
@Override
public <T> void interpret(Delete<T> delete) {
if (delete.getPropertyPath() == null) {
template.doDelete(delete.getRootId(), delete.getEntityType());
} else {
template.doDelete(delete.getRootId(), delete.getPropertyPath());
}
}
@Override
public <T> void interpret(DeleteAll<T> delete) {
template.doDeleteAll(delete.getEntityType(), delete.getPropertyPath());
}
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.core;
import java.util.Map;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.util.Assert;
/**
* delegates all method calls to an instance set after construction. This is useful for {@link DataAccessStrategy}s with
* cyclical dependencies.
*
* @author Jens Schauder
*/
public class DelegatingDataAccessStrategy implements DataAccessStrategy {
private DataAccessStrategy delegate;
@Override
public <T> void insert(T instance, Class<T> domainType, Map<String, Object> additionalParameters) {
delegate.insert(instance, domainType, additionalParameters);
}
@Override
public <S> void update(S instance, Class<S> domainType) {
delegate.update(instance, domainType);
}
@Override
public void delete(Object rootId, PropertyPath propertyPath) {
delegate.delete(rootId, propertyPath);
}
@Override
public void delete(Object id, Class<?> domainType) {
delegate.delete(id, domainType);
}
@Override
public <T> void deleteAll(Class<T> domainType) {
delegate.deleteAll(domainType);
}
@Override
public <T> void deleteAll(PropertyPath propertyPath) {
delegate.deleteAll(propertyPath);
}
@Override
public long count(Class<?> domainType) {
return delegate.count(domainType);
}
@Override
public <T> T findById(Object id, Class<T> domainType) {
Assert.notNull(delegate, "Delegate is null");
return delegate.findById(id, domainType);
}
@Override
public <T> Iterable<T> findAll(Class<T> domainType) {
return delegate.findAll(domainType);
}
@Override
public <T> Iterable<T> findAllById(Iterable<?> ids, Class<T> domainType) {
return delegate.findAllById(ids, domainType);
}
@Override
public <T> Iterable<T> findAllByProperty(Object rootId, JdbcPersistentProperty property) {
Assert.notNull(delegate, "Delegate is null");
return delegate.findAllByProperty(rootId, property);
}
@Override
public <T> boolean existsById(Object id, Class<T> domainType) {
return delegate.existsById(id, domainType);
}
/**
* Must be called exactly once before calling any of the other methods.
*
* @param delegate Must not be {@literal null}
*/
public void setDelegate(DataAccessStrategy delegate) {
Assert.isNull(this.delegate, "The delegate must be set exactly once");
Assert.notNull(delegate, "The delegate must not be set to null");
this.delegate = delegate;
}
}

View File

@@ -48,16 +48,16 @@ class EntityRowMapper<T> implements RowMapper<T> {
private final EntityInstantiator instantiator = new ClassGeneratingEntityInstantiator();
private final ConversionService conversions;
private final JdbcMappingContext context;
private final JdbcEntityOperations template;
private final DataAccessStrategy accessStrategy;
private final JdbcPersistentProperty idProperty;
public EntityRowMapper(JdbcPersistentEntity<T> entity, ConversionService conversions, JdbcMappingContext context,
JdbcEntityOperations template) {
DataAccessStrategy accessStrategy) {
this.entity = entity;
this.conversions = conversions;
this.context = context;
this.template = template;
this.accessStrategy = accessStrategy;
idProperty = entity.getRequiredIdProperty();
}
@@ -79,7 +79,7 @@ class EntityRowMapper<T> implements RowMapper<T> {
for (JdbcPersistentProperty property : entity) {
if (Set.class.isAssignableFrom(property.getType())) {
propertyAccessor.setProperty(property, template.findAllByProperty(id, property));
propertyAccessor.setProperty(property, accessStrategy.findAllByProperty(id, property));
} else {
propertyAccessor.setProperty(property, readFrom(resultSet, property, ""));
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.core;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import org.springframework.dao.DataAccessException;
/**
* {@link Collector} which invokes functions on the elements of a {@link java.util.stream.Stream} containing
* {@link DataAccessStrategy}s until one function completes without throwing an exception. If all invocations throw
* exceptions this {@link Collector} throws itself an exception, gathering all exceptions thrown.
*
* @author Jens Schauder
*/
class FunctionCollector<T> implements Collector<DataAccessStrategy, FunctionCollector<T>.ResultOrException, T> {
private final Function<DataAccessStrategy, T> method;
FunctionCollector(Function<DataAccessStrategy, T> method) {
this.method = method;
}
@Override
public Supplier<ResultOrException> supplier() {
return ResultOrException::new;
}
@Override
public BiConsumer<ResultOrException, DataAccessStrategy> accumulator() {
return (roe, das) -> {
if (!roe.hasResult()) {
try {
roe.setResult(method.apply(das));
} catch (Exception ex) {
roe.add(ex);
}
}
};
}
@Override
public BinaryOperator<ResultOrException> combiner() {
return (roe1, roe2) -> {
throw new UnsupportedOperationException("Can't combine method calls");
};
}
@Override
public Function<ResultOrException, T> finisher() {
return roe -> {
if (roe.hasResult)
return roe.result;
else
throw new CombinedDataAccessException("Failed to perform data access with all available strategies",
Collections.unmodifiableList(roe.exceptions));
};
}
@Override
public Set<Characteristics> characteristics() {
return Collections.emptySet();
}
/**
* Stores intermediate results. I.e. a list of exceptions caught so far, any actual result and the fact, if there
* actually is an result.
*/
class ResultOrException {
private T result;
private final List<Exception> exceptions = new LinkedList<>();
private boolean hasResult = false;
private boolean hasResult() {
return hasResult;
}
private void setResult(T result) {
this.result = result;
hasResult = true;
}
public void add(Exception ex) {
exceptions.add(ex);
}
}
static class CombinedDataAccessException extends DataAccessException {
CombinedDataAccessException(String message, List<Exception> exceptions) {
super(combineMessage(message, exceptions), exceptions.get(exceptions.size() - 1));
}
private static String combineMessage(String message, List<Exception> exceptions) {
return message + exceptions.stream().map(Exception::getMessage).collect(Collectors.joining("\n\t", "\n\t", ""));
}
}
}

View File

@@ -15,10 +15,6 @@
*/
package org.springframework.data.jdbc.core;
import java.util.Map;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty;
/**
* Specifies a operations one can perform on a database, based on an <em>Domain Type</em>.
*
@@ -28,10 +24,6 @@ public interface JdbcEntityOperations {
<T> void save(T instance, Class<T> domainType);
<T> void insert(T instance, Class<T> domainType, Map<String, Object> additionalParameter);
<T> void update(T instance, Class<T> domainType);
<T> void deleteById(Object id, Class<T> domainType);
<T> void delete(T entity, Class<T> domainType);
@@ -46,8 +38,6 @@ public interface JdbcEntityOperations {
<T> Iterable<T> findAll(Class<T> domainType);
<T> Iterable<T> findAllByProperty(Object id, JdbcPersistentProperty property);
<T> boolean existsById(Object id, Class<T> domainType);
}

View File

@@ -15,19 +15,9 @@
*/
package org.springframework.data.jdbc.core;
import java.util.HashMap;
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.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.core.conversion.AggregateChange;
import org.springframework.data.jdbc.core.conversion.AggregateChange.Kind;
import org.springframework.data.jdbc.core.conversion.Interpreter;
@@ -39,61 +29,35 @@ import org.springframework.data.jdbc.mapping.event.BeforeDelete;
import org.springframework.data.jdbc.mapping.event.BeforeSave;
import org.springframework.data.jdbc.mapping.event.Identifier;
import org.springframework.data.jdbc.mapping.event.Identifier.Specified;
import org.springframework.data.jdbc.mapping.model.BasicJdbcPersistentEntityInformation;
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntityInformation;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty;
import org.springframework.data.jdbc.support.JdbcUtil;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.util.Assert;
/**
* {@link JdbcEntityOperations} implementation, storing complete entities including references in a JDBC data store.
* {@link JdbcEntityOperations} implementation, storing aggregates in and obtaining them from a JDBC data store.
*
* @author Jens Schauder
*/
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 BeforeInsert 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 final Interpreter interpreter;
private final SqlGeneratorSource sqlGeneratorSource;
private final JdbcEntityWriter jdbcEntityWriter;
private final JdbcEntityDeleteWriter jdbcEntityDeleteWriter;
public JdbcEntityTemplate(ApplicationEventPublisher publisher, NamedParameterJdbcOperations operations,
JdbcMappingContext context) {
private final DataAccessStrategy accessStrategy;
public JdbcEntityTemplate(ApplicationEventPublisher publisher, JdbcMappingContext context,
DataAccessStrategy dataAccessStrategy) {
this.publisher = publisher;
this.operations = operations;
this.context = context;
this.jdbcEntityWriter = new JdbcEntityWriter(this.context);
this.jdbcEntityDeleteWriter = new JdbcEntityDeleteWriter(this.context);
this.sqlGeneratorSource = new SqlGeneratorSource(this.context);
this.interpreter = new DefaultJdbcInterpreter(this.context, this);
}
private static GenericConversionService getDefaultConversionService() {
DefaultConversionService conversionService = new DefaultConversionService();
Jsr310Converters.getConvertersToRegister().forEach(conversionService::addConverter);
return conversionService;
this.jdbcEntityWriter = new JdbcEntityWriter(context);
this.jdbcEntityDeleteWriter = new JdbcEntityDeleteWriter(context);
this.accessStrategy = dataAccessStrategy;
this.interpreter = new DefaultJdbcInterpreter(context, accessStrategy);
}
@Override
@@ -119,94 +83,29 @@ public class JdbcEntityTemplate implements JdbcEntityOperations {
));
}
@Override
public <T> void insert(T instance, Class<T> domainType, Map<String, Object> additionalParameters) {
KeyHolder holder = new GeneratedKeyHolder();
JdbcPersistentEntity<T> persistentEntity = getRequiredPersistentEntity(domainType);
JdbcPersistentEntityInformation<T, ?> entityInformation = context
.getRequiredPersistentEntityInformation(domainType);
MapSqlParameterSource parameterSource = getPropertyMap(instance, persistentEntity);
Object idValue = getIdValueOrNull(instance, persistentEntity);
JdbcPersistentProperty idProperty = persistentEntity.getRequiredIdProperty();
parameterSource.addValue(idProperty.getColumnName(), convert(idValue, idProperty.getColumnType()),
JdbcUtil.sqlTypeFor(idProperty.getColumnType()));
additionalParameters.forEach(parameterSource::addValue);
operations.update(sql(domainType).getInsert(idValue == null, additionalParameters.keySet()), parameterSource,
holder);
setIdFromJdbc(instance, holder, persistentEntity);
if (entityInformation.isNew(instance)) {
throw new IllegalStateException(String.format(ENTITY_NEW_AFTER_INSERT, persistentEntity));
}
}
@Override
public <S> void update(S instance, Class<S> domainType) {
JdbcPersistentEntity<S> persistentEntity = getRequiredPersistentEntity(domainType);
operations.update(sql(domainType).getUpdate(), getPropertyMap(instance, persistentEntity));
}
@SuppressWarnings("ConstantConditions")
@Override
public long count(Class<?> domainType) {
return operations.getJdbcOperations().queryForObject(sql(domainType).getCount(), Long.class);
return accessStrategy.count(domainType);
}
@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));
return accessStrategy.findById(id, 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);
return accessStrategy.existsById(id, domainType);
}
@Override
public <T> Iterable<T> findAll(Class<T> domainType) {
return operations.query(sql(domainType).getFindAll(), getEntityRowMapper(domainType));
return accessStrategy.findAll(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> Iterable<T> findAllByProperty(Object id, JdbcPersistentProperty property) {
Class<?> actualType = property.getActualType();
String findAllByProperty = sql(actualType).getFindAllByProperty(property.getReverseColumnName());
MapSqlParameterSource parameter = new MapSqlParameterSource(property.getReverseColumnName(), id);
return (Iterable<T>) operations.query(findAllByProperty, parameter, getEntityRowMapper(actualType));
return accessStrategy.findAllById(ids, domainType);
}
@Override
@@ -240,30 +139,6 @@ public class JdbcEntityTemplate implements JdbcEntityOperations {
change.executeWith(interpreter);
publisher.publishEvent(new AfterDelete(specifiedId, optionalEntity, change));
}
void doDelete(Object rootId, PropertyPath propertyPath) {
JdbcPersistentEntity<?> rootEntity = context.getRequiredPersistentEntity(propertyPath.getOwningType());
JdbcPersistentProperty referencingProperty = rootEntity.getRequiredPersistentProperty(propertyPath.getSegment());
Assert.notNull(referencingProperty, "No property found matching the PropertyPath " + propertyPath);
String format = sql(rootEntity.getType()).createDeleteByPath(propertyPath);
HashMap<String, Object> parameters = new HashMap<>();
parameters.put("rootId", rootId);
operations.update(format, parameters);
}
void doDelete(Object id, Class<?> domainType) {
String deleteByIdSql = sql(domainType).getDeleteById();
MapSqlParameterSource parameter = createIdParameterSource(id, domainType);
operations.update(deleteByIdSql, parameter);
}
private <T> AggregateChange createChange(T instance) {
@@ -286,110 +161,4 @@ public class JdbcEntityTemplate implements JdbcEntityOperations {
jdbcEntityDeleteWriter.write(null, aggregateChange);
return aggregateChange;
}
private <T> MapSqlParameterSource createIdParameterSource(Object id, Class<T> domainType) {
return new MapSqlParameterSource("id",
convert(id, getRequiredPersistentEntity(domainType).getRequiredIdProperty().getColumnType()));
}
private <S> MapSqlParameterSource getPropertyMap(final S instance, JdbcPersistentEntity<S> persistentEntity) {
MapSqlParameterSource parameters = new MapSqlParameterSource();
persistentEntity.doWithProperties((PropertyHandler<JdbcPersistentProperty>) property -> {
if (!property.isEntity()) {
Object value = persistentEntity.getPropertyAccessor(instance).getProperty(property);
Object convertedValue = convert(value, property.getColumnType());
parameters.addValue(property.getColumnName(), convertedValue, JdbcUtil.sqlTypeFor(property.getColumnType()));
}
});
return parameters;
}
private <S, ID> ID getIdValueOrNull(S instance, JdbcPersistentEntity<S> persistentEntity) {
EntityInformation<S, ID> entityInformation = new BasicJdbcPersistentEntityInformation<>(persistentEntity);
ID idValue = entityInformation.getId(instance);
return isIdPropertySimpleTypeAndValueZero(idValue, persistentEntity) ? null : idValue;
}
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, 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) {
if (from == null) {
return null;
}
JdbcPersistentEntity<?> persistentEntity = context.getPersistentEntity(from.getClass());
Object id = persistentEntity == null ? null : persistentEntity.getIdentifierAccessor(from).getIdentifier();
return conversions.convert(id == null ? from : id, to);
}
private <S, ID> boolean isIdPropertySimpleTypeAndValueZero(ID idValue, JdbcPersistentEntity<S> persistentEntity) {
JdbcPersistentProperty idProperty = persistentEntity.getIdProperty();
return idValue == null //
|| idProperty == null //
|| (idProperty.getType() == int.class && idValue.equals(0)) //
|| (idProperty.getType() == long.class && idValue.equals(0L));
}
@SuppressWarnings("unchecked")
private <S> JdbcPersistentEntity<S> getRequiredPersistentEntity(Class<S> domainType) {
return (JdbcPersistentEntity<S>) context.getRequiredPersistentEntity(domainType);
}
private SqlGenerator sql(Class<?> domainType) {
return sqlGeneratorSource.getSqlGenerator(domainType);
}
private <T> EntityRowMapper<T> getEntityRowMapper(Class<T> domainType) {
return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), conversions, context, this);
}
<T> void doDeleteAll(Class<T> domainType, PropertyPath propertyPath) {
operations.getJdbcOperations()
.update(sql(propertyPath == null ? domainType : propertyPath.getOwningType().getType())
.createDeleteAllSql(propertyPath));
}
public NamedParameterJdbcOperations getOperations() {
return operations;
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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.mybatis;
import java.util.Map;
/**
* {@link MyBatisContext} instances get passed to MyBatis mapped statements as arguments, making Ids, instances, domainType and other attributes available to the statements.
*
* All methods might return {@literal null} depending on the kind of values available on invocation.
* @author Jens Schauder
*/
public class MyBatisContext {
private final Object id;
private final Object instance;
private final Class domainType;
private final Map<String, Object> additonalValues;
public MyBatisContext(Object id, Object instance, Class domainType, Map<String, Object> additonalValues) {
this.id = id;
this.instance = instance;
this.domainType = domainType;
this.additonalValues = additonalValues;
}
public Object getId() {
return id;
}
public Object getInstance() {
return instance;
}
public Class getDomainType() {
return domainType;
}
public Object get(String key) {
return additonalValues.get(key);
}
}

View File

@@ -0,0 +1,144 @@
/*
* 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.mybatis;
import java.util.Collections;
import java.util.Map;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.data.jdbc.core.DataAccessStrategy;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty;
import org.springframework.data.mapping.PropertyPath;
/**
* {@link DataAccessStrategy} implementation based on MyBatis.
*
* Each method gets mapped to a statement. The name of the statement gets constructed as follows:
*
* The namespace is based on the class of the entity plus the suffix "Mapper". This is then followed by the method name separated by a dot.
*
* For methods taking a {@link PropertyPath} as argument, the relevant entity is that of the root of the path, and the path itself gets as dot separated String appended to the statement name.
*
* Each statement gets an instance of {@link MyBatisContext}, which at least has the entityType set.
*
* For methods taking a {@link PropertyPath} the entityTyoe if the context is set to the class of the leaf type.
*
* @author Jens Schauder
*/
public class MyBatisDataAccessStrategy implements DataAccessStrategy {
private static final String MAPPER_SUFFIX = "Mapper";
private final SqlSessionFactory sqlSessionFactory;
public MyBatisDataAccessStrategy(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
@Override
public <T> void insert(T instance, Class<T> domainType, Map<String, Object> additionalParameters) {
sqlSession().insert(mapper(domainType) + ".insert",
new MyBatisContext(null, instance, domainType, additionalParameters));
}
@Override
public <S> void update(S instance, Class<S> domainType) {
sqlSession().update(mapper(domainType) + ".update",
new MyBatisContext(null, instance, domainType, Collections.emptyMap()));
}
@Override
public void delete(Object id, Class<?> domainType) {
sqlSession().delete(mapper(domainType) + ".delete",
new MyBatisContext(id, null, domainType, Collections.emptyMap()));
}
@Override
public void delete(Object rootId, PropertyPath propertyPath) {
sqlSession().delete(mapper(propertyPath.getOwningType().getType()) + ".delete." + propertyPath.toDotPath(),
new MyBatisContext(rootId, null, propertyPath.getLeafProperty().getTypeInformation().getType(),
Collections.emptyMap()));
}
@Override
public <T> void deleteAll(Class<T> domainType) {
sqlSession().delete( //
mapper(domainType) + ".deleteAll", //
new MyBatisContext(null, null, domainType, Collections.emptyMap()) //
);
}
@Override
public <T> void deleteAll(PropertyPath propertyPath) {
Class baseType = propertyPath.getOwningType().getType();
Class leaveType = propertyPath.getLeafProperty().getTypeInformation().getType();
sqlSession().delete( //
mapper(baseType) + ".deleteAll." + propertyPath.toDotPath(), //
new MyBatisContext(null, null, leaveType, Collections.emptyMap()) //
);
}
@Override
public <T> T findById(Object id, Class<T> domainType) {
return sqlSession().selectOne(mapper(domainType) + ".findById",
new MyBatisContext(id, null, domainType, Collections.emptyMap()));
}
@Override
public <T> Iterable<T> findAll(Class<T> domainType) {
return sqlSession().selectList(mapper(domainType) + ".findAll",
new MyBatisContext(null, null, domainType, Collections.emptyMap()));
}
@Override
public <T> Iterable<T> findAllById(Iterable<?> ids, Class<T> domainType) {
return sqlSession().selectList(mapper(domainType) + ".findAllById",
new MyBatisContext(ids, null, domainType, Collections.emptyMap()));
}
@Override
public <T> Iterable<T> findAllByProperty(Object rootId, JdbcPersistentProperty property) {
return sqlSession().selectList(mapper(property.getOwner().getType()) + ".findAllByProperty." + property.getName(),
new MyBatisContext(rootId, null, property.getType(), Collections.emptyMap()));
}
@Override
public <T> boolean existsById(Object id, Class<T> domainType) {
return sqlSession().selectOne(mapper(domainType) + ".existsById",
new MyBatisContext(id, null, domainType, Collections.emptyMap()));
}
@Override
public long count(Class<?> domainType) {
return sqlSession().selectOne(mapper(domainType) + ".count");
}
private String mapper(Class<?> domainType) {
return domainType.getName() + MAPPER_SUFFIX;
}
private SqlSession sqlSession() {
return sqlSessionFactory.openSession();
}
}

View File

@@ -19,7 +19,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.data.jdbc.core.JdbcEntityOperations;
import org.springframework.data.jdbc.core.JdbcEntityTemplate;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntityInformation;
@@ -75,12 +74,7 @@ public class SimpleJdbcRepository<T, ID> implements CrudRepository<T, ID> {
*/
@Override
public Optional<T> findById(ID id) {
try {
return Optional.of(entityOperations.findById(id, entityInformation.getJavaType()));
} catch (EmptyResultDataAccessException ex) {
return Optional.empty();
}
return Optional.ofNullable(entityOperations.findById(id, entityInformation.getJavaType()));
}
/*

View File

@@ -16,18 +16,17 @@
package org.springframework.data.jdbc.repository.support;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.jdbc.core.DataAccessStrategy;
import org.springframework.data.jdbc.core.JdbcEntityTemplate;
import org.springframework.data.jdbc.mapping.model.BasicJdbcPersistentEntityInformation;
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntityInformation;
import org.springframework.data.jdbc.mapping.model.NamingStrategy;
import org.springframework.data.jdbc.repository.SimpleJdbcRepository;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
/**
* @author Jens Schauder
@@ -37,15 +36,15 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
public class JdbcRepositoryFactory extends RepositoryFactorySupport {
private final JdbcMappingContext context;
private final NamedParameterJdbcOperations jdbcOperations;
private final ApplicationEventPublisher publisher;
private final DataAccessStrategy accessStrategy;
public JdbcRepositoryFactory(NamedParameterJdbcOperations namedParameterJdbcOperations,
ApplicationEventPublisher publisher, NamingStrategy namingStrategy) {
public JdbcRepositoryFactory(ApplicationEventPublisher publisher, JdbcMappingContext context,
DataAccessStrategy dataAccessStrategy) {
this.jdbcOperations = namedParameterJdbcOperations;
this.publisher = publisher;
this.context = new JdbcMappingContext(namingStrategy);
this.context = context;
this.accessStrategy = dataAccessStrategy;
}
@SuppressWarnings("unchecked")
@@ -62,7 +61,7 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport {
JdbcPersistentEntityInformation persistentEntityInformation = context
.getRequiredPersistentEntityInformation(repositoryInformation.getDomainType());
JdbcEntityTemplate template = new JdbcEntityTemplate(publisher, jdbcOperations, context);
JdbcEntityTemplate template = new JdbcEntityTemplate(publisher, context, accessStrategy);
return new SimpleJdbcRepository<>(template, persistentEntityInformation);
}
@@ -71,4 +70,5 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport {
protected Class<?> getRepositoryBaseClass(RepositoryMetadata repositoryMetadata) {
return SimpleJdbcRepository.class;
}
}

View File

@@ -16,15 +16,26 @@
package org.springframework.data.jdbc.repository.support;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.jdbc.core.CascadingDataAccessStrategy;
import org.springframework.data.jdbc.core.DataAccessStrategy;
import org.springframework.data.jdbc.core.DefaultDataAccessStrategy;
import org.springframework.data.jdbc.core.DelegatingDataAccessStrategy;
import org.springframework.data.jdbc.core.SqlGeneratorSource;
import org.springframework.data.jdbc.mapping.model.DefaultNamingStrategy;
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
import org.springframework.data.jdbc.mapping.model.NamingStrategy;
import org.springframework.data.jdbc.mybatis.MyBatisDataAccessStrategy;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport;
@@ -32,6 +43,7 @@ import org.springframework.data.util.Optionals;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.util.ClassUtils;
/**
* Special adapter for Springs {@link org.springframework.beans.factory.FactoryBean} interface to allow easy setup of
@@ -52,6 +64,7 @@ public class JdbcRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extend
private static final String JDBC_OPERATIONS_BEAN_NAME = "jdbcTemplate";
private static final String DATA_SOURCE_BEAN_NAME = "dataSource";
private static final String NAMING_STRATEGY_BEAN_NAME = "namingStrategy";
private static final String SQL_SESSION_FACTORY_BEAN_NAME = "sqlSessionFactory";
private final ApplicationEventPublisher applicationEventPublisher;
private final ApplicationContext context;
@@ -66,8 +79,40 @@ public class JdbcRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extend
@Override
protected RepositoryFactorySupport doCreateRepositoryFactory() {
return new JdbcRepositoryFactory(findOrCreateJdbcOperations(), applicationEventPublisher,
findOrCreateNamingStrategy());
final JdbcMappingContext context = new JdbcMappingContext(findOrCreateNamingStrategy());
DelegatingDataAccessStrategy delegatingDataAccessStrategy = new DelegatingDataAccessStrategy();
List<DataAccessStrategy> accessStrategies = Stream.of( //
createMyBatisDataAccessStrategy(), //
createDefaultAccessStrategy(context, delegatingDataAccessStrategy) //
) //
.filter(Optional::isPresent) //
.map(Optional::get) //
.collect(Collectors.toList());
CascadingDataAccessStrategy strategy = new CascadingDataAccessStrategy(accessStrategies);
delegatingDataAccessStrategy.setDelegate(strategy);
return new JdbcRepositoryFactory(applicationEventPublisher, context, strategy);
}
private Optional<DataAccessStrategy> createMyBatisDataAccessStrategy() {
if (!ClassUtils.isPresent("org.apache.ibatis.session.SqlSessionFactory", this.getClass().getClassLoader())) {
return Optional.empty();
}
return getBean(SqlSessionFactory.class, SQL_SESSION_FACTORY_BEAN_NAME)
.map(ssf -> new MyBatisDataAccessStrategy(ssf));
}
private Optional<DataAccessStrategy> createDefaultAccessStrategy(JdbcMappingContext context,
DelegatingDataAccessStrategy delegatingDataAccessStrategy) {
return Optional.of(new DefaultDataAccessStrategy(new SqlGeneratorSource(context), findOrCreateJdbcOperations(),
context, delegatingDataAccessStrategy));
}
private NamedParameterJdbcOperations findOrCreateJdbcOperations() {

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.core;
import static java.util.Arrays.*;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import junit.framework.AssertionFailedError;
import java.util.Collections;
import org.junit.Test;
import org.springframework.data.jdbc.core.FunctionCollector.CombinedDataAccessException;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty;
/**
* Unit tests for {@link CascadingDataAccessStrategy}.
*
* @author Jens Schauder
*/
public class CascadingDataAccessStrategyUnitTests {
int errorIndex = 1;
String[] errorMessages = {"Sorry I don't support this method. Please try again later", "Still no luck"};
DataAccessStrategy alwaysFails = mock(DataAccessStrategy.class, i -> {
errorIndex ++;
errorIndex %=2;
throw new UnsupportedOperationException(errorMessages[errorIndex]);
});
DataAccessStrategy succeeds = mock(DataAccessStrategy.class);
DataAccessStrategy mayNotCall = mock(DataAccessStrategy.class, i -> {
throw new AssertionFailedError("this shouldn't have get called");
});
@Test // DATAJDBC-123
public void findByReturnsFirstSuccess() {
doReturn("success").when(succeeds).findById(23L, String.class);
CascadingDataAccessStrategy access = new CascadingDataAccessStrategy(asList(alwaysFails, succeeds, mayNotCall));
String byId = access.findById(23L, String.class);
assertThat(byId).isEqualTo("success");
}
@Test // DATAJDBC-123
public void findByFailsIfAllStrategiesFail() {
CascadingDataAccessStrategy access = new CascadingDataAccessStrategy(asList(alwaysFails, alwaysFails));
assertThatExceptionOfType(CombinedDataAccessException.class) //
.isThrownBy(() -> access.findById(23L, String.class)) //
.withMessageContaining("Failed to perform data access with all available strategies") //
.withMessageContaining("Sorry I don't support this method") //
.withMessageContaining("Still no luck");
}
@Test // DATAJDBC-123
public void findByPropertyReturnsFirstSuccess() {
doReturn(Collections.singletonList("success")).when(succeeds).findAllByProperty(eq(23L), any(JdbcPersistentProperty.class));
CascadingDataAccessStrategy access = new CascadingDataAccessStrategy(asList(alwaysFails, succeeds, mayNotCall));
Iterable<Object> findAll = access.findAllByProperty(23L, mock(JdbcPersistentProperty.class));
assertThat(findAll).containsExactly("success");
}
}

View File

@@ -99,7 +99,7 @@ public class EntityRowMapperUnitTests {
private <T> EntityRowMapper<T> createRowMapper(Class<T> type) {
JdbcMappingContext context = new JdbcMappingContext(new DefaultNamingStrategy());
JdbcEntityOperations template = mock(JdbcEntityOperations.class);
DataAccessStrategy template = mock(DataAccessStrategy.class);
doReturn(new HashSet<>(asList(new Trivial(), new Trivial()))).when(template).findAllByProperty(eq(23L),
any(JdbcPersistentProperty.class));

View File

@@ -254,7 +254,23 @@ public class JdbcEntityTemplateIntegrationTests {
JdbcEntityOperations operations(ApplicationEventPublisher publisher,
NamedParameterJdbcOperations namedParameterJdbcOperations) {
return new JdbcEntityTemplate(publisher, namedParameterJdbcOperations, new JdbcMappingContext(new DefaultNamingStrategy()));
final JdbcMappingContext context = new JdbcMappingContext(new DefaultNamingStrategy());
return new JdbcEntityTemplate(publisher, context, dataAccessStrategy(namedParameterJdbcOperations, context));
}
private DelegatingDataAccessStrategy dataAccessStrategy(NamedParameterJdbcOperations namedParameterJdbcOperations,
JdbcMappingContext context) {
DelegatingDataAccessStrategy accessStrategy = new DelegatingDataAccessStrategy();
accessStrategy.setDelegate(new DefaultDataAccessStrategy( //
new SqlGeneratorSource(context), //
namedParameterJdbcOperations, //
context, //
accessStrategy) //
);
return accessStrategy;
}
}
}

View File

@@ -0,0 +1,299 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.core;
import static java.util.Arrays.*;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.Collections;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty;
import org.springframework.data.jdbc.mybatis.MyBatisContext;
import org.springframework.data.jdbc.mybatis.MyBatisDataAccessStrategy;
import org.springframework.data.mapping.PropertyPath;
/**
* Unit tests for the {@link MyBatisDataAccessStrategy}, mainly ensuring that the correct statements get's looked up.
*
* @author Jens Schauder
*/
public class MyBatisDataAccessStrategyUnitTests {
SqlSessionFactory sessionFactory = mock(SqlSessionFactory.class);
SqlSession session = mock(SqlSession.class);
ArgumentCaptor<MyBatisContext> captor = ArgumentCaptor.forClass(MyBatisContext.class);
MyBatisDataAccessStrategy accessStrategy = new MyBatisDataAccessStrategy(sessionFactory);
{
doReturn(session).when(sessionFactory).openSession();
doReturn(false).when(session).selectOne(any(), any());
}
@Test // DATAJDBC-123
public void insert() {
accessStrategy.insert("x", String.class, Collections.singletonMap("key", "value"));
verify(session).insert(eq("java.lang.StringMapper.insert"), captor.capture());
assertThat(captor.getValue()) //
.isNotNull() //
.extracting( //
MyBatisContext::getInstance, //
MyBatisContext::getId, //
MyBatisContext::getDomainType, //
c -> c.get("key") //
).containsExactly( //
"x", //
null, //
String.class, //
"value" //
);
}
@Test // DATAJDBC-123
public void update() {
accessStrategy.update("x", String.class);
verify(session).update(eq("java.lang.StringMapper.update"), captor.capture());
assertThat(captor.getValue()) //
.isNotNull() //
.extracting( //
MyBatisContext::getInstance, //
MyBatisContext::getId, //
MyBatisContext::getDomainType, //
c -> c.get("key") //
).containsExactly( //
"x", //
null, //
String.class, //
null //
);
}
@Test // DATAJDBC-123
public void delete() {
accessStrategy.delete("an-id", String.class);
verify(session).delete(eq("java.lang.StringMapper.delete"), captor.capture());
assertThat(captor.getValue()) //
.isNotNull() //
.extracting( //
MyBatisContext::getInstance, //
MyBatisContext::getId, //
MyBatisContext::getDomainType, //
c -> c.get("key") //
).containsExactly( //
null, //
"an-id", //
String.class, //
null //
);
}
@Test // DATAJDBC-123
public void deleteAllByPath() {
accessStrategy.deleteAll(PropertyPath.from("class.name.bytes", String.class));
verify(session).delete(eq("java.lang.StringMapper.deleteAll.class.name.bytes"), captor.capture());
assertThat(captor.getValue()) //
.isNotNull() //
.extracting( //
MyBatisContext::getInstance, //
MyBatisContext::getId, //
MyBatisContext::getDomainType, //
c -> c.get("key") //
).containsExactly( //
null, //
null, //
byte[].class, //
null //
);
}
@Test // DATAJDBC-123
public void deleteAllByType() {
accessStrategy.deleteAll(String.class);
verify(session).delete(eq("java.lang.StringMapper.deleteAll"), captor.capture());
assertThat(captor.getValue()) //
.isNotNull() //
.extracting( //
MyBatisContext::getInstance, //
MyBatisContext::getId, //
MyBatisContext::getDomainType, //
c -> c.get("key") //
).containsExactly( //
null, //
null, //
String.class, //
null //
);
}
@Test // DATAJDBC-123
public void deleteByPath() {
accessStrategy.delete("rootid", PropertyPath.from("class.name.bytes", String.class));
verify(session).delete(eq("java.lang.StringMapper.delete.class.name.bytes"), captor.capture());
assertThat(captor.getValue()) //
.isNotNull() //
.extracting( //
MyBatisContext::getInstance, //
MyBatisContext::getId, //
MyBatisContext::getDomainType, //
c -> c.get("key") //
).containsExactly( //
null, "rootid", //
byte[].class, //
null //
);
}
@Test // DATAJDBC-123
public void findById() {
accessStrategy.findById("an-id", String.class);
verify(session).selectOne(eq("java.lang.StringMapper.findById"), captor.capture());
assertThat(captor.getValue()) //
.isNotNull() //
.extracting( //
MyBatisContext::getInstance, //
MyBatisContext::getId, //
MyBatisContext::getDomainType, //
c -> c.get("key") //
).containsExactly( //
null, "an-id", //
String.class, //
null //
);
}
@Test // DATAJDBC-123
public void findAll() {
accessStrategy.findAll(String.class);
verify(session).selectList(eq("java.lang.StringMapper.findAll"), captor.capture());
assertThat(captor.getValue()) //
.isNotNull() //
.extracting( //
MyBatisContext::getInstance, //
MyBatisContext::getId, //
MyBatisContext::getDomainType, //
c -> c.get("key") //
).containsExactly( //
null, //
null, //
String.class, //
null //
);
}
@Test // DATAJDBC-123
public void findAllById() {
accessStrategy.findAllById(asList("id1", "id2"), String.class);
verify(session).selectList(eq("java.lang.StringMapper.findAllById"), captor.capture());
assertThat(captor.getValue()) //
.isNotNull() //
.extracting( //
MyBatisContext::getInstance, //
MyBatisContext::getId, //
MyBatisContext::getDomainType, //
c -> c.get("key") //
).containsExactly( //
null, //
asList("id1", "id2"), //
String.class, //
null //
);
}
@Test // DATAJDBC-123
public void findAllByProperty() {
JdbcPersistentProperty property = mock(JdbcPersistentProperty.class, Mockito.RETURNS_DEEP_STUBS);
when(property.getOwner().getType()).thenReturn((Class) String.class);
doReturn(Number.class).when(property).getType();
doReturn("propertyName").when(property).getName();
accessStrategy.findAllByProperty("id", property);
verify(session).selectList(eq("java.lang.StringMapper.findAllByProperty.propertyName"), captor.capture());
assertThat(captor.getValue()) //
.isNotNull() //
.extracting( //
MyBatisContext::getInstance, //
MyBatisContext::getId, //
MyBatisContext::getDomainType, //
c -> c.get("key") //
).containsExactly( //
null, //
"id", //
Number.class, //
null //
);
}
@Test // DATAJDBC-123
public void existsById() {
accessStrategy.existsById("id", String.class);
verify(session).selectOne(eq("java.lang.StringMapper.existsById"), captor.capture());
assertThat(captor.getValue()) //
.isNotNull() //
.extracting( //
MyBatisContext::getInstance, //
MyBatisContext::getId, //
MyBatisContext::getDomainType, //
c -> c.get("key") //
).containsExactly( //
null, //
"id", //
String.class, //
null //
);
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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.mybatis;
import org.apache.ibatis.type.Alias;
import org.springframework.data.annotation.Id;
/**
* @author Jens Schauder
*/
@Alias("DummyEntity")
class DummyEntity {
@Id final Long id;
final String name;
public DummyEntity(Long id, String name) {
this.id = id;
this.name = name;
}
}

View File

@@ -0,0 +1,23 @@
/*
* 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.mybatis;
/**
* @author Jens Schauder
*/
public interface DummyEntityMapper {
}

View File

@@ -0,0 +1,111 @@
/*
* 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.mybatis;
import static org.assertj.core.api.Assertions.*;
import junit.framework.AssertionFailedError;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.data.jdbc.repository.JdbcRepositoryIdGenerationIntegrationTests.TestConfiguration;
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
import org.springframework.data.repository.CrudRepository;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
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 the integration with Mybatis.
*
* @author Jens Schauder
*/
@ContextConfiguration
@Transactional
public class MyBatisHsqlIntegrationTests {
@org.springframework.context.annotation.Configuration
@Import(TestConfiguration.class)
@EnableJdbcRepositories(considerNestedRepositories = true)
static class Config {
@Autowired JdbcRepositoryFactory factory;
@Bean
Class<?> testClass() {
return MyBatisHsqlIntegrationTests.class;
}
@Bean
SqlSessionFactoryBean createSessionFactory(EmbeddedDatabase db) {
Configuration configuration = new Configuration();
configuration.getTypeAliasRegistry().registerAlias("MyBatisContext", MyBatisContext.class);
configuration.getTypeAliasRegistry().registerAlias(DummyEntity.class);
configuration.addMapper(DummyEntityMapper.class);
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(db);
sqlSessionFactoryBean.setConfiguration(configuration);
return sqlSessionFactoryBean;
}
}
@ClassRule public static final SpringClassRule classRule = new SpringClassRule();
@Rule public SpringMethodRule methodRule = new SpringMethodRule();
@Autowired SqlSessionFactory sqlSessionFactory;
@Autowired DummyEntityRepository repository;
@Test // DATAJDBC-123
public void mybatisSelfTest() {
SqlSession session = sqlSessionFactory.openSession();
session.selectList("org.springframework.data.jdbc.mybatis.DummyEntityMapper.findById");
}
@Test // DATAJDBC-123
public void myBatisGetsUsedForInsertAndSelect() {
DummyEntity entity = new DummyEntity(null, "some name");
DummyEntity saved = repository.save(entity);
assertThat(saved.id).isNotNull();
DummyEntity reloaded = repository.findById(saved.id).orElseThrow(AssertionFailedError::new);
assertThat(reloaded).isNotNull().extracting(e -> e.id, e -> e.name);
}
interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {
}
}

View File

@@ -16,7 +16,6 @@
package org.springframework.data.jdbc.repository;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import lombok.Data;
import lombok.Value;
@@ -27,7 +26,6 @@ import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@@ -50,7 +48,6 @@ import org.springframework.test.context.junit4.rules.SpringMethodRule;
* @author Greg Turnquist
*/
@ContextConfiguration
@EnableJdbcRepositories(considerNestedRepositories = true)
public class JdbcRepositoryIdGenerationIntegrationTests {
@Configuration
@@ -124,6 +121,7 @@ public class JdbcRepositoryIdGenerationIntegrationTests {
@Configuration
@ComponentScan("org.springframework.data.jdbc.testing")
@EnableJdbcRepositories(considerNestedRepositories = true)
public static class TestConfiguration {
@Bean
@@ -149,19 +147,5 @@ public class JdbcRepositoryIdGenerationIntegrationTests {
NamedParameterJdbcTemplate template(DataSource db) {
return new NamedParameterJdbcTemplate(db);
}
@Bean
ReadOnlyIdEntityRepository readOnlyIdRepository(DataSource db, NamingStrategy namingStrategy) {
return new JdbcRepositoryFactory(new NamedParameterJdbcTemplate(db), mock(ApplicationEventPublisher.class),
namingStrategy).getRepository(ReadOnlyIdEntityRepository.class);
}
@Bean
PrimitiveIdEntityRepository primitiveIdRepository(NamedParameterJdbcTemplate template) {
return new JdbcRepositoryFactory(template, mock(ApplicationEventPublisher.class), new DefaultNamingStrategy())
.getRepository(PrimitiveIdEntityRepository.class);
}
}
}

View File

@@ -19,6 +19,8 @@ import org.junit.Test;
import org.mockito.stubbing.Answer;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.core.DefaultDataAccessStrategy;
import org.springframework.data.jdbc.core.SqlGeneratorSource;
import org.springframework.data.jdbc.mapping.event.AfterDelete;
import org.springframework.data.jdbc.mapping.event.AfterSave;
import org.springframework.data.jdbc.mapping.event.BeforeDelete;
@@ -26,6 +28,7 @@ import org.springframework.data.jdbc.mapping.event.BeforeSave;
import org.springframework.data.jdbc.mapping.event.Identifier;
import org.springframework.data.jdbc.mapping.event.JdbcEvent;
import org.springframework.data.jdbc.mapping.model.DefaultNamingStrategy;
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
import org.springframework.data.repository.CrudRepository;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
@@ -44,8 +47,17 @@ public class SimpleJdbcRepositoryEventsUnitTests {
@Before
public void before() {
NamedParameterJdbcOperations operations = createIdGeneratingOperations();
JdbcRepositoryFactory factory = new JdbcRepositoryFactory(operations, publisher, new DefaultNamingStrategy());
final JdbcMappingContext context = new JdbcMappingContext(new DefaultNamingStrategy());
JdbcRepositoryFactory factory = new JdbcRepositoryFactory( //
publisher, //
context, //
new DefaultDataAccessStrategy( //
new SqlGeneratorSource(context), //
createIdGeneratingOperations(), //
context //
) //
);
repository = factory.getRepository(DummyEntityRepository.class);
}

View File

@@ -5,16 +5,24 @@ import static org.mockito.Mockito.*;
import static org.springframework.test.util.ReflectionTestUtils.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.assertj.core.api.Condition;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.core.CascadingDataAccessStrategy;
import org.springframework.data.jdbc.core.DataAccessStrategy;
import org.springframework.data.jdbc.core.DefaultDataAccessStrategy;
import org.springframework.data.jdbc.core.DelegatingDataAccessStrategy;
import org.springframework.data.jdbc.mybatis.MyBatisDataAccessStrategy;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.jdbc.core.JdbcOperations;
@@ -28,22 +36,26 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
*/
public class JdbcRepositoryFactoryBeanUnitTests {
static final String JDBC_OPERATIONS_FIELD_NAME = "jdbcOperations";
static final String EXPECTED_JDBC_OPERATIONS_BEAN_NAME = "jdbcTemplate";
static final String EXPECTED_NAMED_PARAMETER_JDBC_OPERATIONS_BEAN_NAME = "namedParameterJdbcTemplate";
static final String ACCESS_STRATEGY_FIELD_NAME_IN_FACTORY = "accessStrategy";
static final String OPERATIONS_FIELD_NAME_IN_DEFAULT_ACCESS_STRATEGY = "operations";
ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class);
ApplicationContext context = mock(ApplicationContext.class);
Map<String, DataSource> dataSources = new HashMap<>();
Map<String, JdbcOperations> jdbcOperations = new HashMap<>();
Map<String, NamedParameterJdbcOperations> namedJdbcOperations = new HashMap<>();
Map<String, SqlSessionFactory> sqlSessionFactories = new HashMap<>();
{
when(context.getBeansOfType(DataSource.class)).thenReturn(dataSources);
when(context.getBeansOfType(JdbcOperations.class)).thenReturn(jdbcOperations);
when(context.getBeansOfType(NamedParameterJdbcOperations.class)).thenReturn(namedJdbcOperations);
when(context.getBeansOfType(SqlSessionFactory.class)).thenReturn(sqlSessionFactories);
}
@Test // DATAJDBC-100
@@ -53,7 +65,7 @@ public class JdbcRepositoryFactoryBeanUnitTests {
new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context);
assertThatExceptionOfType(IllegalStateException.class) //
.isThrownBy(() -> factoryBean.doCreateRepositoryFactory());
.isThrownBy(factoryBean::doCreateRepositoryFactory);
}
@@ -171,19 +183,51 @@ public class JdbcRepositoryFactoryBeanUnitTests {
assertThat(factoryBean.doCreateRepositoryFactory()).is(using(expectedOperations));
}
@Test // DATAJDBC-123
public void withoutSqlSessionFactoryThereIsNoMyBatisIntegration() {
dataSources.put("anyname", mock(DataSource.class));
sqlSessionFactories.clear();
JdbcRepositoryFactoryBean<DummyEntityRepository, DummyEntity, Long> factoryBean = //
new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context);
RepositoryFactorySupport factory = factoryBean.doCreateRepositoryFactory();
assertThat(findDataAccessStrategy(factory, MyBatisDataAccessStrategy.class)).isNull();
}
@Test // DATAJDBC-123
public void withSqlSessionFactoryThereIsMyBatisIntegration() {
dataSources.put("anyname", mock(DataSource.class));
sqlSessionFactories.put("anyname", mock(SqlSessionFactory.class));
JdbcRepositoryFactoryBean<DummyEntityRepository, DummyEntity, Long> factoryBean = //
new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context);
RepositoryFactorySupport factory = factoryBean.doCreateRepositoryFactory();
assertThat(findDataAccessStrategy(factory, MyBatisDataAccessStrategy.class)).isNotNull();
}
private Condition<? super RepositoryFactorySupport> using(NamedParameterJdbcOperations expectedOperations) {
Predicate<RepositoryFactorySupport> predicate = r -> getField(r, JDBC_OPERATIONS_FIELD_NAME) == expectedOperations;
Predicate<RepositoryFactorySupport> predicate = r -> extractNamedParameterJdbcOperations(r) == expectedOperations;
return new Condition<>(predicate, "uses " + expectedOperations);
}
private NamedParameterJdbcOperations extractNamedParameterJdbcOperations(RepositoryFactorySupport r) {
DefaultDataAccessStrategy defaultDataAccessStrategy = findDataAccessStrategy(r, DefaultDataAccessStrategy.class);
return (NamedParameterJdbcOperations) getField(defaultDataAccessStrategy,
OPERATIONS_FIELD_NAME_IN_DEFAULT_ACCESS_STRATEGY);
}
private Condition<? super RepositoryFactorySupport> using(JdbcOperations expectedOperations) {
Predicate<RepositoryFactorySupport> predicate = r -> {
NamedParameterJdbcOperations namedOperations = (NamedParameterJdbcOperations) getField(r,
JDBC_OPERATIONS_FIELD_NAME);
return namedOperations.getJdbcOperations() == expectedOperations;
};
Predicate<RepositoryFactorySupport> predicate = r -> extractNamedParameterJdbcOperations(r)
.getJdbcOperations() == expectedOperations;
return new Condition<>(predicate, "uses " + expectedOperations);
}
@@ -192,8 +236,7 @@ public class JdbcRepositoryFactoryBeanUnitTests {
Predicate<RepositoryFactorySupport> predicate = r -> {
NamedParameterJdbcOperations namedOperations = (NamedParameterJdbcOperations) getField(r,
JDBC_OPERATIONS_FIELD_NAME);
NamedParameterJdbcOperations namedOperations = extractNamedParameterJdbcOperations(r);
JdbcTemplate jdbcOperations = (JdbcTemplate) namedOperations.getJdbcOperations();
return jdbcOperations.getDataSource() == expectedDataSource;
};
@@ -201,6 +244,34 @@ public class JdbcRepositoryFactoryBeanUnitTests {
return new Condition<>(predicate, "using " + expectedDataSource);
}
private static <T extends DataAccessStrategy> T findDataAccessStrategy(RepositoryFactorySupport r, Class<T> type) {
DataAccessStrategy accessStrategy = (DataAccessStrategy) getField(r, ACCESS_STRATEGY_FIELD_NAME_IN_FACTORY);
return findDataAccessStrategy(accessStrategy, type);
}
private static <T extends DataAccessStrategy> T findDataAccessStrategy(DataAccessStrategy accessStrategy,
Class<T> type) {
if (type.isInstance(accessStrategy))
return (T) accessStrategy;
if (accessStrategy instanceof DelegatingDataAccessStrategy) {
return findDataAccessStrategy((DataAccessStrategy) getField(accessStrategy, "delegate"), type);
}
if (accessStrategy instanceof CascadingDataAccessStrategy) {
List<DataAccessStrategy> strategies = (List<DataAccessStrategy>) getField(accessStrategy, "strategies");
return strategies.stream() //
.map((DataAccessStrategy das) -> findDataAccessStrategy(das, type)) //
.filter(Objects::nonNull) //
.findFirst() //
.orElse(null);
}
return null;
}
private static class DummyEntity {
@Id private Long id;
}

View File

@@ -17,12 +17,16 @@ package org.springframework.data.jdbc.testing;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jdbc.core.DefaultDataAccessStrategy;
import org.springframework.data.jdbc.core.SqlGeneratorSource;
import org.springframework.data.jdbc.mapping.model.DefaultNamingStrategy;
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
@@ -32,6 +36,7 @@ import org.springframework.transaction.PlatformTransactionManager;
* Infrastructure configuration for integration tests.
*
* @author Oliver Gierke
* @author Jens Schauder
*/
@Configuration
@ComponentScan // To pick up configuration classes (per activated profile)
@@ -39,10 +44,21 @@ public class TestConfiguration {
@Autowired DataSource dataSource;
@Autowired ApplicationEventPublisher publisher;
@Autowired(required = false) SqlSessionFactory sqlSessionFactory;
@Bean
JdbcRepositoryFactory jdbcRepositoryFactory() {
return new JdbcRepositoryFactory(namedParameterJdbcTemplate(), publisher, new DefaultNamingStrategy());
final JdbcMappingContext context = new JdbcMappingContext(new DefaultNamingStrategy());
return new JdbcRepositoryFactory( //
publisher, //
context, //
new DefaultDataAccessStrategy( //
new SqlGeneratorSource(context), //
namedParameterJdbcTemplate(), //
context) //
);
}
@Bean

View File

@@ -11,6 +11,8 @@
<logger name="org.springframework.jdbc.core" level="trace" />
<logger name="org.springframework.data.jdbc.mybatis.DummyEntityMapper" level="trace" />
<root level="info">
<appender-ref ref="console" />
</root>

View File

@@ -0,0 +1 @@
CREATE TABLE dummyentity(id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY);

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.springframework.data.jdbc.mybatis.DummyEntityMapper">
<resultMap id="dummyEntityMap" type="DummyEntity">
<constructor>
<idArg column="id" javaType="long"/>
<arg column="name" javaType="String"/>
</constructor>
</resultMap>
<insert id="insert" parameterType="MyBatisContext" useGeneratedKeys="true" keyProperty="instance.id" keyColumn="ID">
INSERT INTO DummyEntity (id) VALUES (DEFAULT)
</insert>
<select id="findById" resultType="MyBatisContext" resultMap="dummyEntityMap">
SELECT
id,
'Name based on an id' || id AS name
FROM DummyEntity
WHERE id = #{id}
</select>
</mapper>