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:
committed by
Greg Turnquist
parent
fd2ae05795
commit
806bb24ff5
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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, ""));
|
||||
}
|
||||
|
||||
@@ -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", ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 //
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
CREATE TABLE dummyentity(id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY);
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user