diff --git a/README.adoc b/README.adoc index bb40623f38..19ad15a6b6 100644 --- a/README.adoc +++ b/README.adoc @@ -22,7 +22,7 @@ This means that it does rather little out of the box. But it offers plenty of pl === Fast running tests -Fast running tests can executed with a simple +Fast running tests can executed with a simple [source] ---- @@ -44,8 +44,9 @@ This will also execute the unit tests. Currently the following _databasetypes_ are available: -* hsql (default, does not need to be running) +* hsql (default, does not require a running database) * mysql +* postgres === Run tests with all databases @@ -65,4 +66,4 @@ Here are some ways for you to get involved in the community: * Github is for social coding: if you want to write code, we encourage contributions through pull requests from http://help.github.com/forking/[forks of this repository]. If you want to contribute code this way, please reference a JIRA ticket as well covering the specific issue you are addressing. * Watch for upcoming articles on Spring by http://spring.io/blog[subscribing] to spring.io. -Before we accept a non-trivial patch or pull request we will need you to https://cla.pivotal.io/sign/spring[sign the Contributor License Agreement]. Signing the contributor’s agreement does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do. If you forget to do so, you'll be reminded when you submit a pull request. Active contributors might be asked to join the core team, and given the ability to merge pull requests. \ No newline at end of file +Before we accept a non-trivial patch or pull request we will need you to https://cla.pivotal.io/sign/spring[sign the Contributor License Agreement]. Signing the contributor’s agreement does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do. If you forget to do so, you'll be reminded when you submit a pull request. Active contributors might be asked to join the core team, and given the ability to merge pull requests. diff --git a/pom.xml b/pom.xml index 4ffa5540e7..fdde189902 100644 --- a/pom.xml +++ b/pom.xml @@ -1,243 +1,263 @@ - - 4.0.0 - - org.springframework.data - spring-data-jdbc - 1.0.0.BUILD-SNAPSHOT - - Spring Data JDBC - Spring Data module for JDBC repositories. - http://projects.spring.io/spring-data-jdbc - - - org.springframework.data.build - spring-data-parent - 2.0.0.BUILD-SNAPSHOT - - - - - DATAJDBC - - 2.0.0.BUILD-SNAPSHOT - - reuseReports - 1.8.0.10 - - - - - - release - - - - org.jfrog.buildinfo - artifactory-maven-plugin - false - - - - - - - all-dbs - - - - org.apache.maven.plugins - maven-surefire-plugin - - - mysql-test - test - - test - - - - **/*IntegrationTests.java - - - mysql - - - - - postgres-test - test - - test - - - - **/*IntegrationTests.java - - - postgres - - - - - - - - - - - - - - ${project.groupId} - spring-data-commons - ${springdata.commons} - - - - org.springframework - spring-tx - - - - org.springframework - spring-context - - - - org.springframework - spring-beans - - - - org.springframework - spring-jdbc - - - - org.springframework - spring-core - - - commons-logging - commons-logging - - - - - - org.hsqldb - hsqldb - 2.2.8 - test - - - - org.assertj - assertj-core - 3.6.2 - test - - - - mysql - mysql-connector-java - 5.1.41 - test - - - - org.postgresql - postgresql - 42.0.0 - test - - - - de.schauderhaft.degraph - degraph-check - 0.1.4 - test - - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 2.12 - - - - - - - - org.jacoco - jacoco-maven-plugin - ${jacoco} - - ${jacoco.destfile} - - - - jacoco-initialize - - prepare-agent - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - - default-test - - - **/*Tests.java - - - - - - - org.apache.maven.plugins - maven-assembly-plugin - - - org.codehaus.mojo - wagon-maven-plugin - - - org.asciidoctor - asciidoctor-maven-plugin - - - - - - - spring-libs-snapshot - https://repo.spring.io/libs-snapshot - - - - - - spring-plugins-snapshot - https://repo.spring.io/plugins-snapshot - - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + + 4.0.0 + + org.springframework.data + spring-data-jdbc + 1.0.0.DATAJDBC-123-SNAPSHOT + + Spring Data JDBC + Spring Data module for JDBC repositories. + http://projects.spring.io/spring-data-jdbc + + + org.springframework.data.build + spring-data-parent + 2.0.0.BUILD-SNAPSHOT + + + + + DATAJDBC + + 2.0.0.BUILD-SNAPSHOT + + reuseReports + 1.8.0.10 + + + + + + release + + + + org.jfrog.buildinfo + artifactory-maven-plugin + false + + + + + + + all-dbs + + + + org.apache.maven.plugins + maven-surefire-plugin + + + mysql-test + test + + test + + + + **/*IntegrationTests.java + + + **/*HsqlIntegrationTests.java + + + mysql + + + + + postgres-test + test + + test + + + + **/*IntegrationTests.java + + + **/*HsqlIntegrationTests.java + + + postgres + + + + + + + + + + + + + + ${project.groupId} + spring-data-commons + ${springdata.commons} + + + + org.springframework + spring-tx + + + + org.springframework + spring-context + + + + org.springframework + spring-beans + + + + org.springframework + spring-jdbc + + + + org.springframework + spring-core + + + commons-logging + commons-logging + + + + + + org.mybatis + mybatis-spring + 1.3.1 + true + + + + org.mybatis + mybatis + 3.4.4 + true + + + + org.hsqldb + hsqldb + 2.2.8 + test + + + + org.assertj + assertj-core + 3.6.2 + test + + + + mysql + mysql-connector-java + 5.1.41 + test + + + + org.postgresql + postgresql + 42.0.0 + test + + + + de.schauderhaft.degraph + degraph-check + 0.1.4 + test + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.12 + + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco} + + ${jacoco.destfile} + + + + jacoco-initialize + + prepare-agent + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + default-test + + + **/*Tests.java + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + org.codehaus.mojo + wagon-maven-plugin + + + org.asciidoctor + asciidoctor-maven-plugin + + + + + + + spring-libs-snapshot + https://repo.spring.io/libs-snapshot + + + + + + spring-plugins-snapshot + https://repo.spring.io/plugins-snapshot + + \ No newline at end of file diff --git a/src/main/java/org/springframework/data/jdbc/core/CascadingDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/CascadingDataAccessStrategy.java new file mode 100644 index 0000000000..5ffc4493b4 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/CascadingDataAccessStrategy.java @@ -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 strategies; + + public CascadingDataAccessStrategy(List strategies) { + this.strategies = new ArrayList<>(strategies); + } + + @Override + public void insert(T instance, Class domainType, Map additionalParameters) { + collectVoid(das -> das.insert(instance, domainType, additionalParameters)); + } + + @Override + public void update(S instance, Class 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 void deleteAll(Class domainType) { + collectVoid(das -> das.deleteAll(domainType)); + } + + @Override + public void deleteAll(PropertyPath propertyPath) { + collectVoid(das -> das.deleteAll(propertyPath)); + } + + @Override + public long count(Class domainType) { + return collect(das -> das.count(domainType)); + } + + @Override + public T findById(Object id, Class domainType) { + return collect(das -> das.findById(id, domainType)); + } + + @Override + public Iterable findAll(Class domainType) { + return collect(das -> das.findAll(domainType)); + } + + @Override + public Iterable findAllById(Iterable ids, Class domainType) { + return collect(das -> das.findAllById(ids, domainType)); + } + + @Override + public Iterable findAllByProperty(Object rootId, JdbcPersistentProperty property) { + return collect(das -> das.findAllByProperty(rootId, property)); + } + + @Override + public boolean existsById(Object id, Class domainType) { + return collect(das -> das.existsById(id, domainType)); + } + + private T collect(Function function) { + return strategies.stream().collect(new FunctionCollector<>(function)); + } + + private void collectVoid(Consumer consumer) { + + collect(das -> { + consumer.accept(das); + return null; + }); + } + +} diff --git a/src/main/java/org/springframework/data/jdbc/core/DataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/DataAccessStrategy.java new file mode 100644 index 0000000000..61f9b90ec7 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/DataAccessStrategy.java @@ -0,0 +1,71 @@ +/* + * 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 { + + void insert(T instance, Class domainType, Map additionalParameters); + + void update(S instance, Class 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); + + void deleteAll(Class domainType); + + /** + * Deletes all entities reachable via {@literal propertyPath} from any instance. + * + * @param propertyPath Leading from the root object to the entities to be deleted. + */ + void deleteAll(PropertyPath propertyPath); + + long count(Class domainType); + + T findById(Object id, Class domainType); + + Iterable findAll(Class domainType); + + Iterable findAllById(Iterable ids, Class 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. + */ + Iterable findAllByProperty(Object rootId, JdbcPersistentProperty property); + + boolean existsById(Object id, Class domainType); + +} diff --git a/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java new file mode 100644 index 0000000000..81ebcac45e --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java @@ -0,0 +1,317 @@ +/* + * 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; + +/** + * The default {@link DataAccessStrategy} is to generate SQL statements based on meta data from the entity. + * + * @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 void insert(T instance, Class domainType, Map additionalParameters) { + + KeyHolder holder = new GeneratedKeyHolder(); + JdbcPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); + JdbcPersistentEntityInformation 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 void update(S instance, Class domainType) { + + JdbcPersistentEntity 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 parameters = new HashMap<>(); + parameters.put("rootId", rootId); + operations.update(format, parameters); + } + + @Override + public void deleteAll(Class domainType) { + operations.getJdbcOperations().update(sql(domainType).createDeleteAllSql(null)); + } + + @Override + public 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 findById(Object id, Class 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 Iterable findAll(Class domainType) { + return operations.query(sql(domainType).getFindAll(), getEntityRowMapper(domainType)); + } + + @Override + public Iterable findAllById(Iterable ids, Class domainType) { + + String findAllInListSql = sql(domainType).getFindAllInList(); + Class targetType = getRequiredPersistentEntity(domainType).getRequiredIdProperty().getColumnType(); + + MapSqlParameterSource parameter = new MapSqlParameterSource( // + "ids", // + StreamSupport.stream(ids.spliterator(), false) // + .map(id -> convert(id, targetType)) // + .collect(Collectors.toList()) // + ); + + return operations.query(findAllInListSql, parameter, getEntityRowMapper(domainType)); + } + + @Override + public Iterable 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) operations.query(findAllByProperty, parameter, getEntityRowMapper(actualType)); + } + + @Override + public boolean existsById(Object id, Class domainType) { + + String existsSql = sql(domainType).getExists(); + MapSqlParameterSource parameter = createIdParameterSource(id, domainType); + return operations.queryForObject(existsSql, parameter, Boolean.class); + } + + private static GenericConversionService getDefaultConversionService() { + + DefaultConversionService conversionService = new DefaultConversionService(); + Jsr310Converters.getConvertersToRegister().forEach(conversionService::addConverter); + + return conversionService; + } + + private MapSqlParameterSource getPropertyMap(final S instance, JdbcPersistentEntity persistentEntity) { + + MapSqlParameterSource parameters = new MapSqlParameterSource(); + + persistentEntity.doWithProperties((PropertyHandler) 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 ID getIdValueOrNull(S instance, JdbcPersistentEntity persistentEntity) { + + EntityInformation entityInformation = new BasicJdbcPersistentEntityInformation<>(persistentEntity); + + ID idValue = entityInformation.getId(instance); + + return isIdPropertyNullOrScalarZero(idValue, persistentEntity) ? null : idValue; + } + + private boolean isIdPropertyNullOrScalarZero(ID idValue, JdbcPersistentEntity 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 void setIdFromJdbc(S instance, KeyHolder holder, JdbcPersistentEntity persistentEntity) { + + JdbcPersistentEntityInformation entityInformation = new BasicJdbcPersistentEntityInformation<>( + persistentEntity); + + try { + + getIdFromHolder(holder, persistentEntity).ifPresent(it -> { + + Class targetType = persistentEntity.getRequiredIdProperty().getType(); + Object converted = convert(it, targetType); + entityInformation.setId(instance, converted); + }); + + } catch (NonTransientDataAccessException e) { + throw new UnableToSetId("Unable to set id of " + instance, e); + } + } + + private Optional getIdFromHolder(KeyHolder holder, JdbcPersistentEntity persistentEntity) { + + try { + // MySQL just returns one value with a special name + return Optional.ofNullable(holder.getKey()); + } catch (InvalidDataAccessApiUsageException e) { + // Postgres returns a value for each column + return Optional.ofNullable(holder.getKeys().get(persistentEntity.getIdColumn())); + } + } + + private EntityRowMapper getEntityRowMapper(Class domainType) { + return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), conversions, context, accessStrategy); + } + + private MapSqlParameterSource createIdParameterSource(Object id, Class domainType) { + return new MapSqlParameterSource("id", + convert(id, getRequiredPersistentEntity(domainType).getRequiredIdProperty().getColumnType())); + } + + @SuppressWarnings("unchecked") + private JdbcPersistentEntity getRequiredPersistentEntity(Class domainType) { + return (JdbcPersistentEntity) context.getRequiredPersistentEntity(domainType); + } + + private V convert(Object from, Class 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); + } + +} diff --git a/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java b/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java index 9c17d7d1a6..7d876d6c0a 100644 --- a/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java +++ b/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java @@ -28,7 +28,7 @@ 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,51 +36,59 @@ 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 void interpret(Insert insert) { - - Map additionalColumnValues = new HashMap<>(); - DbAction dependingOn = insert.getDependingOn(); - - if (dependingOn != null) { - - JdbcPersistentEntity persistentEntity = context.getRequiredPersistentEntity(dependingOn.getEntityType()); - String columnName = persistentEntity.getTableName(); - Object entity = dependingOn.getEntity(); - Object identifier = persistentEntity.getIdentifierAccessor(entity).getIdentifier(); - - additionalColumnValues.put(columnName, identifier); - } - - template.insert(insert.getEntity(), insert.getEntityType(), additionalColumnValues); + accessStrategy.insert(insert.getEntity(), insert.getEntityType(), createAdditionalColumnValues(insert)); } @Override public void interpret(Update update) { - template.update(update.getEntity(), update.getEntityType()); + accessStrategy.update(update.getEntity(), update.getEntityType()); } @Override public void interpret(Delete delete) { if (delete.getPropertyPath() == null) { - template.doDelete(delete.getRootId(), delete.getEntityType()); + accessStrategy.delete(delete.getRootId(), delete.getEntityType()); } else { - template.doDelete(delete.getRootId(), delete.getPropertyPath()); + accessStrategy.delete(delete.getRootId(), delete.getPropertyPath()); } - } @Override public void interpret(DeleteAll delete) { - template.doDeleteAll(delete.getEntityType(), delete.getPropertyPath()); + + if (delete.getEntityType() == null) { + accessStrategy.deleteAll(delete.getPropertyPath()); + } else { + accessStrategy.deleteAll(delete.getEntityType()); + } } + + private Map createAdditionalColumnValues(Insert insert) { + + Map additionalColumnValues = new HashMap<>(); + DbAction dependingOn = insert.getDependingOn(); + + if (dependingOn != null) { + + JdbcPersistentEntity persistentEntity = context.getRequiredPersistentEntity(dependingOn.getEntityType()); + String columnName = persistentEntity.getTableName(); + Object entity = dependingOn.getEntity(); + Object identifier = persistentEntity.getIdentifierAccessor(entity).getIdentifier(); + + additionalColumnValues.put(columnName, identifier); + } + return additionalColumnValues; + } + } diff --git a/src/main/java/org/springframework/data/jdbc/core/DelegatingDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/DelegatingDataAccessStrategy.java new file mode 100644 index 0000000000..763fa532cc --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/DelegatingDataAccessStrategy.java @@ -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 void insert(T instance, Class domainType, Map additionalParameters) { + delegate.insert(instance, domainType, additionalParameters); + } + + @Override + public void update(S instance, Class 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 void deleteAll(Class domainType) { + delegate.deleteAll(domainType); + } + + @Override + public void deleteAll(PropertyPath propertyPath) { + delegate.deleteAll(propertyPath); + } + + @Override + public long count(Class domainType) { + return delegate.count(domainType); + } + + @Override + public T findById(Object id, Class domainType) { + + Assert.notNull(delegate, "Delegate is null"); + + return delegate.findById(id, domainType); + } + + @Override + public Iterable findAll(Class domainType) { + return delegate.findAll(domainType); + } + + @Override + public Iterable findAllById(Iterable ids, Class domainType) { + return delegate.findAllById(ids, domainType); + } + + @Override + public Iterable findAllByProperty(Object rootId, JdbcPersistentProperty property) { + + Assert.notNull(delegate, "Delegate is null"); + + return delegate.findAllByProperty(rootId, property); + } + + @Override + public boolean existsById(Object id, Class 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; + } +} diff --git a/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java b/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java index bf57d08432..fe5a513529 100644 --- a/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java +++ b/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java @@ -48,16 +48,16 @@ class EntityRowMapper implements RowMapper { 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 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 @@ public T mapRow(ResultSet resultSet, int rowNumber) throws SQLException { 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, "")); } diff --git a/src/main/java/org/springframework/data/jdbc/core/FunctionCollector.java b/src/main/java/org/springframework/data/jdbc/core/FunctionCollector.java new file mode 100644 index 0000000000..04dfedcbf7 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/FunctionCollector.java @@ -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 implements Collector.ResultOrException, T> { + + private final Function method; + + FunctionCollector(Function method) { + this.method = method; + } + + @Override + public Supplier supplier() { + return ResultOrException::new; + } + + @Override + public BiConsumer accumulator() { + + return (roe, das) -> { + + if (!roe.hasResult()) { + + try { + roe.setResult(method.apply(das)); + } catch (Exception ex) { + roe.add(ex); + } + } + }; + } + + @Override + public BinaryOperator combiner() { + + return (roe1, roe2) -> { + throw new UnsupportedOperationException("Can't combine method calls"); + }; + } + + @Override + public Function 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() { + 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 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 exceptions) { + super(combineMessage(message, exceptions), exceptions.get(exceptions.size() - 1)); + } + + private static String combineMessage(String message, List exceptions) { + return message + exceptions.stream().map(Exception::getMessage).collect(Collectors.joining("\n\t", "\n\t", "")); + } + } +} diff --git a/src/main/java/org/springframework/data/jdbc/core/JdbcEntityOperations.java b/src/main/java/org/springframework/data/jdbc/core/JdbcEntityOperations.java index 7d74f424ce..68e5124096 100644 --- a/src/main/java/org/springframework/data/jdbc/core/JdbcEntityOperations.java +++ b/src/main/java/org/springframework/data/jdbc/core/JdbcEntityOperations.java @@ -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 Domain Type. * @@ -28,10 +24,6 @@ public interface JdbcEntityOperations { void save(T instance, Class domainType); - void insert(T instance, Class domainType, Map additionalParameter); - - void update(T instance, Class domainType); - void deleteById(Object id, Class domainType); void delete(T entity, Class domainType); @@ -46,8 +38,6 @@ public interface JdbcEntityOperations { Iterable findAll(Class domainType); - Iterable findAllByProperty(Object id, JdbcPersistentProperty property); - boolean existsById(Object id, Class domainType); } diff --git a/src/main/java/org/springframework/data/jdbc/core/JdbcEntityTemplate.java b/src/main/java/org/springframework/data/jdbc/core/JdbcEntityTemplate.java index b17f10ad9f..abe97f113e 100644 --- a/src/main/java/org/springframework/data/jdbc/core/JdbcEntityTemplate.java +++ b/src/main/java/org/springframework/data/jdbc/core/JdbcEntityTemplate.java @@ -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.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 void save(T instance, Class domainType) { )); } - @Override - public void insert(T instance, Class domainType, Map additionalParameters) { - - KeyHolder holder = new GeneratedKeyHolder(); - JdbcPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); - JdbcPersistentEntityInformation 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 void update(S instance, Class domainType) { - - JdbcPersistentEntity 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 findById(Object id, Class 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 boolean existsById(Object id, Class 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 Iterable findAll(Class domainType) { - return operations.query(sql(domainType).getFindAll(), getEntityRowMapper(domainType)); + return accessStrategy.findAll(domainType); } @Override public Iterable findAllById(Iterable ids, Class domainType) { - - String findAllInListSql = sql(domainType).getFindAllInList(); - Class targetType = getRequiredPersistentEntity(domainType).getRequiredIdProperty().getColumnType(); - - MapSqlParameterSource parameter = new MapSqlParameterSource( // - "ids", // - StreamSupport.stream(ids.spliterator(), false) // - .map(id -> convert(id, targetType)) // - .collect(Collectors.toList()) // - ); - - return operations.query(findAllInListSql, parameter, getEntityRowMapper(domainType)); - } - - @Override - public Iterable 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) operations.query(findAllByProperty, parameter, getEntityRowMapper(actualType)); + return accessStrategy.findAllById(ids, domainType); } @Override @@ -240,30 +139,6 @@ private void deleteTree(Object id, Object entity, Class domainType) { 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 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 AggregateChange createChange(T instance) { @@ -286,110 +161,4 @@ private AggregateChange createDeletingChange(Class domainType) { jdbcEntityDeleteWriter.write(null, aggregateChange); return aggregateChange; } - - private MapSqlParameterSource createIdParameterSource(Object id, Class domainType) { - return new MapSqlParameterSource("id", - convert(id, getRequiredPersistentEntity(domainType).getRequiredIdProperty().getColumnType())); - } - - private MapSqlParameterSource getPropertyMap(final S instance, JdbcPersistentEntity persistentEntity) { - - MapSqlParameterSource parameters = new MapSqlParameterSource(); - - persistentEntity.doWithProperties((PropertyHandler) 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 ID getIdValueOrNull(S instance, JdbcPersistentEntity persistentEntity) { - - EntityInformation entityInformation = new BasicJdbcPersistentEntityInformation<>(persistentEntity); - - ID idValue = entityInformation.getId(instance); - - return isIdPropertySimpleTypeAndValueZero(idValue, persistentEntity) ? null : idValue; - } - - private void setIdFromJdbc(S instance, KeyHolder holder, JdbcPersistentEntity persistentEntity) { - - JdbcPersistentEntityInformation entityInformation = new BasicJdbcPersistentEntityInformation<>( - persistentEntity); - - try { - - getIdFromHolder(holder, persistentEntity).ifPresent(it -> { - - Class targetType = persistentEntity.getRequiredIdProperty().getType(); - Object converted = convert(it, targetType); - entityInformation.setId(instance, converted); - }); - - } catch (NonTransientDataAccessException e) { - throw new UnableToSetId("Unable to set id of " + instance, e); - } - } - - private Optional getIdFromHolder(KeyHolder holder, JdbcPersistentEntity persistentEntity) { - - try { - // MySQL just returns one value with a special name - return Optional.ofNullable(holder.getKey()); - } catch (InvalidDataAccessApiUsageException e) { - // Postgres returns a value for each column - return Optional.ofNullable(holder.getKeys().get(persistentEntity.getIdColumn())); - } - } - - private V convert(Object from, Class to) { - - 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 boolean isIdPropertySimpleTypeAndValueZero(ID idValue, JdbcPersistentEntity 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 JdbcPersistentEntity getRequiredPersistentEntity(Class domainType) { - return (JdbcPersistentEntity) context.getRequiredPersistentEntity(domainType); - } - - private SqlGenerator sql(Class domainType) { - return sqlGeneratorSource.getSqlGenerator(domainType); - } - - private EntityRowMapper getEntityRowMapper(Class domainType) { - return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), conversions, context, this); - } - - void doDeleteAll(Class domainType, PropertyPath propertyPath) { - - operations.getJdbcOperations() - .update(sql(propertyPath == null ? domainType : propertyPath.getOwningType().getType()) - .createDeleteAllSql(propertyPath)); - } - - public NamedParameterJdbcOperations getOperations() { - return operations; - } } diff --git a/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisContext.java b/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisContext.java new file mode 100644 index 0000000000..717ed360ee --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisContext.java @@ -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 additonalValues; + + public MyBatisContext(Object id, Object instance, Class domainType, Map 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); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java new file mode 100644 index 0000000000..d9e041bfca --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java @@ -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 void insert(T instance, Class domainType, Map additionalParameters) { + sqlSession().insert(mapper(domainType) + ".insert", + new MyBatisContext(null, instance, domainType, additionalParameters)); + } + + @Override + public void update(S instance, Class 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 void deleteAll(Class domainType) { + + sqlSession().delete( // + mapper(domainType) + ".deleteAll", // + new MyBatisContext(null, null, domainType, Collections.emptyMap()) // + ); + } + + @Override + public 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 findById(Object id, Class domainType) { + return sqlSession().selectOne(mapper(domainType) + ".findById", + new MyBatisContext(id, null, domainType, Collections.emptyMap())); + } + + @Override + public Iterable findAll(Class domainType) { + return sqlSession().selectList(mapper(domainType) + ".findAll", + new MyBatisContext(null, null, domainType, Collections.emptyMap())); + } + + @Override + public Iterable findAllById(Iterable ids, Class domainType) { + return sqlSession().selectList(mapper(domainType) + ".findAllById", + new MyBatisContext(ids, null, domainType, Collections.emptyMap())); + } + + @Override + public Iterable 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 boolean existsById(Object id, Class 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(); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/SimpleJdbcRepository.java b/src/main/java/org/springframework/data/jdbc/repository/SimpleJdbcRepository.java index 9ec47855b9..bf465fa89c 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/SimpleJdbcRepository.java +++ b/src/main/java/org/springframework/data/jdbc/repository/SimpleJdbcRepository.java @@ -19,7 +19,6 @@ 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 Iterable saveAll(Iterable entities) { */ @Override public Optional 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())); } /* diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java index eacaa7a9af..a50d89294b 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java +++ b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java @@ -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 @@ 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 @@ protected Object getTargetRepository(RepositoryInformation repositoryInformation 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 @@ protected Object getTargetRepository(RepositoryInformation repositoryInformation protected Class getRepositoryBaseClass(RepositoryMetadata repositoryMetadata) { return SimpleJdbcRepository.class; } + } diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java index 91b2d2de59..1156e8206f 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java @@ -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.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, 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,58 @@ public class JdbcRepositoryFactoryBean, S, ID extend @Override protected RepositoryFactorySupport doCreateRepositoryFactory() { - return new JdbcRepositoryFactory(findOrCreateJdbcOperations(), applicationEventPublisher, - findOrCreateNamingStrategy()); + + final JdbcMappingContext context = new JdbcMappingContext(findOrCreateNamingStrategy()); + + return new JdbcRepositoryFactory(applicationEventPublisher, context, createDataAccessStrategy(context)); + } + + /** + *

+ * Create the {@link DataAccessStrategy}, by combining all applicable strategies into one. + *

+ *

+ * The challenge is that the {@link DefaultDataAccessStrategy} when used for reading needs a + * {@link DataAccessStrategy} for loading referenced entities (see. + * {@link DefaultDataAccessStrategy#getEntityRowMapper(Class)}. But it should use all configured + * {@link DataAccessStrategy}s for this. This creates a cyclic dependency. In order to build this the + * {@link DefaultDataAccessStrategy} gets passed in a {@link DelegatingDataAccessStrategy} which at the end gets set + * to the full {@link CascadingDataAccessStrategy}. + *

+ */ + private CascadingDataAccessStrategy createDataAccessStrategy(JdbcMappingContext context) { + + DelegatingDataAccessStrategy delegatingDataAccessStrategy = new DelegatingDataAccessStrategy(); + + List 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 strategy; + } + + private Optional 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 createDefaultAccessStrategy(JdbcMappingContext context, + DelegatingDataAccessStrategy delegatingDataAccessStrategy) { + + return Optional.of(new DefaultDataAccessStrategy(new SqlGeneratorSource(context), findOrCreateJdbcOperations(), + context, delegatingDataAccessStrategy)); } private NamedParameterJdbcOperations findOrCreateJdbcOperations() { diff --git a/src/test/java/org/springframework/data/jdbc/core/CascadingDataAccessStrategyUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/CascadingDataAccessStrategyUnitTests.java new file mode 100644 index 0000000000..349c419c61 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/core/CascadingDataAccessStrategyUnitTests.java @@ -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 findAll = access.findAllByProperty(23L, mock(JdbcPersistentProperty.class)); + + assertThat(findAll).containsExactly("success"); + } + +} diff --git a/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java index 22a8a46313..8f370af77a 100644 --- a/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java @@ -99,7 +99,7 @@ public void collectionReferenceGetsLoadedWithAdditionalSelect() throws SQLExcept private EntityRowMapper createRowMapper(Class 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)); diff --git a/src/test/java/org/springframework/data/jdbc/core/JdbcEntityTemplateIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/core/JdbcEntityTemplateIntegrationTests.java index 3eb0b81a14..fda034367f 100644 --- a/src/test/java/org/springframework/data/jdbc/core/JdbcEntityTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/JdbcEntityTemplateIntegrationTests.java @@ -254,7 +254,23 @@ Class testClass() { 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; } } } diff --git a/src/test/java/org/springframework/data/jdbc/core/MyBatisDataAccessStrategyUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/MyBatisDataAccessStrategyUnitTests.java new file mode 100644 index 0000000000..b169b13ee0 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/core/MyBatisDataAccessStrategyUnitTests.java @@ -0,0 +1,301 @@ +/* + * 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.Before; +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 captor = ArgumentCaptor.forClass(MyBatisContext.class); + + MyBatisDataAccessStrategy accessStrategy = new MyBatisDataAccessStrategy(sessionFactory); + + @Before + public void before() { + + 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 // + ); + } + +} diff --git a/src/test/java/org/springframework/data/jdbc/mybatis/DummyEntity.java b/src/test/java/org/springframework/data/jdbc/mybatis/DummyEntity.java new file mode 100644 index 0000000000..3a1e96ba83 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/mybatis/DummyEntity.java @@ -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; + } +} diff --git a/src/test/java/org/springframework/data/jdbc/mybatis/DummyEntityMapper.java b/src/test/java/org/springframework/data/jdbc/mybatis/DummyEntityMapper.java new file mode 100644 index 0000000000..3b51723ab6 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/mybatis/DummyEntityMapper.java @@ -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 { + +} diff --git a/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisHsqlIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisHsqlIntegrationTests.java new file mode 100644 index 0000000000..babb18f218 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisHsqlIntegrationTests.java @@ -0,0 +1,110 @@ +/* + * 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 { + + } +} diff --git a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java index 232416631f..d48926384a 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java @@ -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.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 @@ * @author Greg Turnquist */ @ContextConfiguration -@EnableJdbcRepositories(considerNestedRepositories = true) public class JdbcRepositoryIdGenerationIntegrationTests { @Configuration @@ -124,6 +121,7 @@ static class PrimitiveIdEntity { @Configuration @ComponentScan("org.springframework.data.jdbc.testing") + @EnableJdbcRepositories(considerNestedRepositories = true) public static class TestConfiguration { @Bean @@ -149,19 +147,5 @@ public String getTableName(Class type) { 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); - } } } diff --git a/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java b/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java index fe3b04ed13..5688894c87 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java @@ -19,6 +19,8 @@ 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.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); } diff --git a/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java b/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java index b5211825f7..0cca7ab3a9 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java @@ -5,16 +5,24 @@ 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 @@ */ 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 dataSources = new HashMap<>(); Map jdbcOperations = new HashMap<>(); Map namedJdbcOperations = new HashMap<>(); + Map 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 void exceptionWithUsefulMessage() { new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); assertThatExceptionOfType(IllegalStateException.class) // - .isThrownBy(() -> factoryBean.doCreateRepositoryFactory()); + .isThrownBy(factoryBean::doCreateRepositoryFactory); } @@ -171,19 +183,51 @@ public void namedParameterJdbcOperationsTakePrecedenceOverJdbcOperations() { assertThat(factoryBean.doCreateRepositoryFactory()).is(using(expectedOperations)); } + @Test // DATAJDBC-123 + public void withoutSqlSessionFactoryThereIsNoMyBatisIntegration() { + + dataSources.put("anyname", mock(DataSource.class)); + sqlSessionFactories.clear(); + + JdbcRepositoryFactoryBean 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 factoryBean = // + new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); + + RepositoryFactorySupport factory = factoryBean.doCreateRepositoryFactory(); + + assertThat(findDataAccessStrategy(factory, MyBatisDataAccessStrategy.class)).isNotNull(); + } + private Condition using(NamedParameterJdbcOperations expectedOperations) { - Predicate predicate = r -> getField(r, JDBC_OPERATIONS_FIELD_NAME) == expectedOperations; + Predicate 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 using(JdbcOperations expectedOperations) { - Predicate predicate = r -> { - NamedParameterJdbcOperations namedOperations = (NamedParameterJdbcOperations) getField(r, - JDBC_OPERATIONS_FIELD_NAME); - return namedOperations.getJdbcOperations() == expectedOperations; - }; + Predicate predicate = r -> extractNamedParameterJdbcOperations(r) + .getJdbcOperations() == expectedOperations; return new Condition<>(predicate, "uses " + expectedOperations); } @@ -192,8 +236,7 @@ private Condition using(DataSource expectedDat Predicate 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 @@ private Condition using(DataSource expectedDat return new Condition<>(predicate, "using " + expectedDataSource); } + private static T findDataAccessStrategy(RepositoryFactorySupport r, Class type) { + + DataAccessStrategy accessStrategy = (DataAccessStrategy) getField(r, ACCESS_STRATEGY_FIELD_NAME_IN_FACTORY); + return findDataAccessStrategy(accessStrategy, type); + } + + private static T findDataAccessStrategy(DataAccessStrategy accessStrategy, + Class type) { + + if (type.isInstance(accessStrategy)) + return (T) accessStrategy; + + if (accessStrategy instanceof DelegatingDataAccessStrategy) { + return findDataAccessStrategy((DataAccessStrategy) getField(accessStrategy, "delegate"), type); + } + + if (accessStrategy instanceof CascadingDataAccessStrategy) { + List strategies = (List) 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; } diff --git a/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java b/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java index caa1460b28..c30210049f 100644 --- a/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java +++ b/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java @@ -17,12 +17,16 @@ 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 @@ * 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 diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml index 9aab4bd674..e5cc8699c9 100644 --- a/src/test/resources/logback.xml +++ b/src/test/resources/logback.xml @@ -11,6 +11,8 @@ + + diff --git a/src/test/resources/org.springframework.data.jdbc.mybatis/MyBatisHsqlIntegrationTests-hsql.sql b/src/test/resources/org.springframework.data.jdbc.mybatis/MyBatisHsqlIntegrationTests-hsql.sql new file mode 100644 index 0000000000..a2f6eb9021 --- /dev/null +++ b/src/test/resources/org.springframework.data.jdbc.mybatis/MyBatisHsqlIntegrationTests-hsql.sql @@ -0,0 +1 @@ +CREATE TABLE dummyentity(id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY); \ No newline at end of file diff --git a/src/test/resources/org/springframework/data/jdbc/mybatis/DummyEntityMapper.xml b/src/test/resources/org/springframework/data/jdbc/mybatis/DummyEntityMapper.xml new file mode 100644 index 0000000000..c2e00541c5 --- /dev/null +++ b/src/test/resources/org/springframework/data/jdbc/mybatis/DummyEntityMapper.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + INSERT INTO DummyEntity (id) VALUES (DEFAULT) + + + \ No newline at end of file