Browse Source

DATAJDBC-123 - MyBatis integration on DbAction level.

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

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

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

Related issue: DATAJDBC-132.
pull/18/head
Jens Schauder 8 years ago committed by Greg Turnquist
parent
commit
806bb24ff5
No known key found for this signature in database
GPG Key ID: CB2FA4D512B5C413
  1. 7
      README.adoc
  2. 486
      pom.xml
  3. 113
      src/main/java/org/springframework/data/jdbc/core/CascadingDataAccessStrategy.java
  4. 69
      src/main/java/org/springframework/data/jdbc/core/DataAccessStrategy.java
  5. 320
      src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java
  6. 56
      src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java
  7. 112
      src/main/java/org/springframework/data/jdbc/core/DelegatingDataAccessStrategy.java
  8. 8
      src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java
  9. 127
      src/main/java/org/springframework/data/jdbc/core/FunctionCollector.java
  10. 10
      src/main/java/org/springframework/data/jdbc/core/JdbcEntityOperations.java
  11. 259
      src/main/java/org/springframework/data/jdbc/core/JdbcEntityTemplate.java
  12. 56
      src/main/java/org/springframework/data/jdbc/mybatis/MyBatisContext.java
  13. 144
      src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java
  14. 8
      src/main/java/org/springframework/data/jdbc/repository/SimpleJdbcRepository.java
  15. 16
      src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java
  16. 49
      src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java
  17. 86
      src/test/java/org/springframework/data/jdbc/core/CascadingDataAccessStrategyUnitTests.java
  18. 2
      src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java
  19. 18
      src/test/java/org/springframework/data/jdbc/core/JdbcEntityTemplateIntegrationTests.java
  20. 299
      src/test/java/org/springframework/data/jdbc/core/MyBatisDataAccessStrategyUnitTests.java
  21. 34
      src/test/java/org/springframework/data/jdbc/mybatis/DummyEntity.java
  22. 23
      src/test/java/org/springframework/data/jdbc/mybatis/DummyEntityMapper.java
  23. 111
      src/test/java/org/springframework/data/jdbc/mybatis/MyBatisHsqlIntegrationTests.java
  24. 18
      src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java
  25. 16
      src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java
  26. 91
      src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java
  27. 18
      src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java
  28. 2
      src/test/resources/logback.xml
  29. 1
      src/test/resources/org.springframework.data.jdbc.mybatis/MyBatisHsqlIntegrationTests-hsql.sql
  30. 22
      src/test/resources/org/springframework/data/jdbc/mybatis/DummyEntityMapper.xml

7
README.adoc

@ -22,7 +22,7 @@ This means that it does rather little out of the box. But it offers plenty of pl @@ -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. @@ -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: @@ -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.
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.

486
pom.xml

@ -1,243 +1,263 @@ @@ -1,243 +1,263 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jdbc</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<name>Spring Data JDBC</name>
<description>Spring Data module for JDBC repositories.</description>
<url>http://projects.spring.io/spring-data-jdbc</url>
<parent>
<groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-parent</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
</parent>
<properties>
<dist.key>DATAJDBC</dist.key>
<springdata.commons>2.0.0.BUILD-SNAPSHOT</springdata.commons>
<sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis>
<hsqldb1>1.8.0.10</hsqldb1>
</properties>
<profiles>
<profile>
<id>release</id>
<build>
<plugins>
<plugin>
<groupId>org.jfrog.buildinfo</groupId>
<artifactId>artifactory-maven-plugin</artifactId>
<inherited>false</inherited>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>all-dbs</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<executions>
<execution>
<id>mysql-test</id>
<phase>test</phase>
<goals>
<goal>test</goal>
</goals>
<configuration>
<includes>
<include>**/*IntegrationTests.java</include>
</includes>
<systemPropertyVariables>
<spring.profiles.active>mysql</spring.profiles.active>
</systemPropertyVariables>
</configuration>
</execution>
<execution>
<id>postgres-test</id>
<phase>test</phase>
<goals>
<goal>test</goal>
</goals>
<configuration>
<includes>
<include>**/*IntegrationTests.java</include>
</includes>
<systemPropertyVariables>
<spring.profiles.active>postgres</spring.profiles.active>
</systemPropertyVariables>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-data-commons</artifactId>
<version>${springdata.commons}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.2.8</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.6.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.41</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.0.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>de.schauderhaft.degraph</groupId>
<artifactId>degraph-check</artifactId>
<version>0.1.4</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.12</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<!--
Jacoco plugin redeclared to make sure it's downloaded and
the agents can be explicitly added to the test executions.
-->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco}</version>
<configuration>
<destFile>${jacoco.destfile}</destFile>
</configuration>
<executions>
<execution>
<id>jacoco-initialize</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<executions>
<execution>
<id>default-test</id>
<configuration>
<includes>
<include>**/*Tests.java</include>
</includes>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>wagon-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-libs-snapshot</id>
<url>https://repo.spring.io/libs-snapshot</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-plugins-snapshot</id>
<url>https://repo.spring.io/plugins-snapshot</url>
</pluginRepository>
</pluginRepositories>
<name>Spring Data JDBC</name>
<description>Spring Data module for JDBC repositories.</description>
<url>http://projects.spring.io/spring-data-jdbc</url>
<parent>
<groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-parent</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
</parent>
<properties>
<dist.key>DATAJDBC</dist.key>
<springdata.commons>2.0.0.BUILD-SNAPSHOT</springdata.commons>
<sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis>
<hsqldb1>1.8.0.10</hsqldb1>
</properties>
<profiles>
<profile>
<id>release</id>
<build>
<plugins>
<plugin>
<groupId>org.jfrog.buildinfo</groupId>
<artifactId>artifactory-maven-plugin</artifactId>
<inherited>false</inherited>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>all-dbs</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<executions>
<execution>
<id>mysql-test</id>
<phase>test</phase>
<goals>
<goal>test</goal>
</goals>
<configuration>
<includes>
<include>**/*IntegrationTests.java</include>
</includes>
<excludes>
<exclude>**/*HsqlIntegrationTests.java</exclude>
</excludes>
<systemPropertyVariables>
<spring.profiles.active>mysql</spring.profiles.active>
</systemPropertyVariables>
</configuration>
</execution>
<execution>
<id>postgres-test</id>
<phase>test</phase>
<goals>
<goal>test</goal>
</goals>
<configuration>
<includes>
<include>**/*IntegrationTests.java</include>
</includes>
<excludes>
<exclude>**/*HsqlIntegrationTests.java</exclude>
</excludes>
<systemPropertyVariables>
<spring.profiles.active>postgres</spring.profiles.active>
</systemPropertyVariables>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-data-commons</artifactId>
<version>${springdata.commons}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.1</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.4</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.2.8</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.6.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.41</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.0.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>de.schauderhaft.degraph</groupId>
<artifactId>degraph-check</artifactId>
<version>0.1.4</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.12</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<!--
Jacoco plugin redeclared to make sure it's downloaded and
the agents can be explicitly added to the test executions.
-->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco}</version>
<configuration>
<destFile>${jacoco.destfile}</destFile>
</configuration>
<executions>
<execution>
<id>jacoco-initialize</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<executions>
<execution>
<id>default-test</id>
<configuration>
<includes>
<include>**/*Tests.java</include>
</includes>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>wagon-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-libs-snapshot</id>
<url>https://repo.spring.io/libs-snapshot</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-plugins-snapshot</id>
<url>https://repo.spring.io/plugins-snapshot</url>
</pluginRepository>
</pluginRepositories>
</project>

113
src/main/java/org/springframework/data/jdbc/core/CascadingDataAccessStrategy.java

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

69
src/main/java/org/springframework/data/jdbc/core/DataAccessStrategy.java

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

320
src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java

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

56
src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java

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

112
src/main/java/org/springframework/data/jdbc/core/DelegatingDataAccessStrategy.java

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

8
src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java

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

127
src/main/java/org/springframework/data/jdbc/core/FunctionCollector.java

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

10
src/main/java/org/springframework/data/jdbc/core/JdbcEntityOperations.java

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

259
src/main/java/org/springframework/data/jdbc/core/JdbcEntityTemplate.java

@ -15,19 +15,9 @@ @@ -15,19 +15,9 @@
*/
package org.springframework.data.jdbc.core;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.data.convert.Jsr310Converters;
import org.springframework.data.jdbc.core.conversion.AggregateChange;
import org.springframework.data.jdbc.core.conversion.AggregateChange.Kind;
import org.springframework.data.jdbc.core.conversion.Interpreter;
@ -39,61 +29,35 @@ import org.springframework.data.jdbc.mapping.event.BeforeDelete; @@ -39,61 +29,35 @@ import org.springframework.data.jdbc.mapping.event.BeforeDelete;
import org.springframework.data.jdbc.mapping.event.BeforeSave;
import org.springframework.data.jdbc.mapping.event.Identifier;
import org.springframework.data.jdbc.mapping.event.Identifier.Specified;
import org.springframework.data.jdbc.mapping.model.BasicJdbcPersistentEntityInformation;
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntityInformation;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty;
import org.springframework.data.jdbc.support.JdbcUtil;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.util.Assert;
/**
* {@link JdbcEntityOperations} implementation, storing complete entities including references in a JDBC data store.
* {@link JdbcEntityOperations} implementation, storing aggregates in and obtaining them from a JDBC data store.
*
* @author Jens Schauder
*/
public class JdbcEntityTemplate implements JdbcEntityOperations {
private static final String ENTITY_NEW_AFTER_INSERT = "Entity [%s] still 'new' after insert. Please set either"
+ " the id property in a BeforeInsert event handler, or ensure the database creates a value and your "
+ "JDBC driver returns it.";
private final ApplicationEventPublisher publisher;
private final NamedParameterJdbcOperations operations;
private final JdbcMappingContext context;
private final ConversionService conversions = getDefaultConversionService();
private final Interpreter interpreter;
private final SqlGeneratorSource sqlGeneratorSource;
private final JdbcEntityWriter jdbcEntityWriter;
private final JdbcEntityDeleteWriter jdbcEntityDeleteWriter;
public JdbcEntityTemplate(ApplicationEventPublisher publisher, NamedParameterJdbcOperations operations,
JdbcMappingContext context) {
private final DataAccessStrategy accessStrategy;
public JdbcEntityTemplate(ApplicationEventPublisher publisher, JdbcMappingContext context,
DataAccessStrategy dataAccessStrategy) {
this.publisher = publisher;
this.operations = operations;
this.context = context;
this.jdbcEntityWriter = new JdbcEntityWriter(this.context);
this.jdbcEntityDeleteWriter = new JdbcEntityDeleteWriter(this.context);
this.sqlGeneratorSource = new SqlGeneratorSource(this.context);
this.interpreter = new DefaultJdbcInterpreter(this.context, this);
}
private static GenericConversionService getDefaultConversionService() {
DefaultConversionService conversionService = new DefaultConversionService();
Jsr310Converters.getConvertersToRegister().forEach(conversionService::addConverter);
return conversionService;
this.jdbcEntityWriter = new JdbcEntityWriter(context);
this.jdbcEntityDeleteWriter = new JdbcEntityDeleteWriter(context);
this.accessStrategy = dataAccessStrategy;
this.interpreter = new DefaultJdbcInterpreter(context, accessStrategy);
}
@Override
@ -119,94 +83,29 @@ public class JdbcEntityTemplate implements JdbcEntityOperations { @@ -119,94 +83,29 @@ public class JdbcEntityTemplate implements JdbcEntityOperations {
));
}
@Override
public <T> void insert(T instance, Class<T> domainType, Map<String, Object> additionalParameters) {
KeyHolder holder = new GeneratedKeyHolder();
JdbcPersistentEntity<T> persistentEntity = getRequiredPersistentEntity(domainType);
JdbcPersistentEntityInformation<T, ?> entityInformation = context
.getRequiredPersistentEntityInformation(domainType);
MapSqlParameterSource parameterSource = getPropertyMap(instance, persistentEntity);
Object idValue = getIdValueOrNull(instance, persistentEntity);
JdbcPersistentProperty idProperty = persistentEntity.getRequiredIdProperty();
parameterSource.addValue(idProperty.getColumnName(), convert(idValue, idProperty.getColumnType()),
JdbcUtil.sqlTypeFor(idProperty.getColumnType()));
additionalParameters.forEach(parameterSource::addValue);
operations.update(sql(domainType).getInsert(idValue == null, additionalParameters.keySet()), parameterSource,
holder);
setIdFromJdbc(instance, holder, persistentEntity);
if (entityInformation.isNew(instance)) {
throw new IllegalStateException(String.format(ENTITY_NEW_AFTER_INSERT, persistentEntity));
}
}
@Override
public <S> void update(S instance, Class<S> domainType) {
JdbcPersistentEntity<S> persistentEntity = getRequiredPersistentEntity(domainType);
operations.update(sql(domainType).getUpdate(), getPropertyMap(instance, persistentEntity));
}
@SuppressWarnings("ConstantConditions")
@Override
public long count(Class<?> domainType) {
return operations.getJdbcOperations().queryForObject(sql(domainType).getCount(), Long.class);
return accessStrategy.count(domainType);
}
@Override
public <T> T findById(Object id, Class<T> domainType) {
String findOneSql = sql(domainType).getFindOne();
MapSqlParameterSource parameter = createIdParameterSource(id, domainType);
return operations.queryForObject(findOneSql, parameter, getEntityRowMapper(domainType));
return accessStrategy.findById(id, domainType);
}
@Override
public <T> boolean existsById(Object id, Class<T> domainType) {
String existsSql = sql(domainType).getExists();
MapSqlParameterSource parameter = createIdParameterSource(id, domainType);
return operations.queryForObject(existsSql, parameter, Boolean.class);
return accessStrategy.existsById(id, domainType);
}
@Override
public <T> Iterable<T> findAll(Class<T> domainType) {
return operations.query(sql(domainType).getFindAll(), getEntityRowMapper(domainType));
return accessStrategy.findAll(domainType);
}
@Override
public <T> Iterable<T> findAllById(Iterable<?> ids, Class<T> domainType) {
String findAllInListSql = sql(domainType).getFindAllInList();
Class<?> targetType = getRequiredPersistentEntity(domainType).getRequiredIdProperty().getColumnType();
MapSqlParameterSource parameter = new MapSqlParameterSource( //
"ids", //
StreamSupport.stream(ids.spliterator(), false) //
.map(id -> convert(id, targetType)) //
.collect(Collectors.toList()) //
);
return operations.query(findAllInListSql, parameter, getEntityRowMapper(domainType));
}
@Override
public <T> Iterable<T> findAllByProperty(Object id, JdbcPersistentProperty property) {
Class<?> actualType = property.getActualType();
String findAllByProperty = sql(actualType).getFindAllByProperty(property.getReverseColumnName());
MapSqlParameterSource parameter = new MapSqlParameterSource(property.getReverseColumnName(), id);
return (Iterable<T>) operations.query(findAllByProperty, parameter, getEntityRowMapper(actualType));
return accessStrategy.findAllById(ids, domainType);
}
@Override
@ -240,30 +139,6 @@ public class JdbcEntityTemplate implements JdbcEntityOperations { @@ -240,30 +139,6 @@ public class JdbcEntityTemplate implements JdbcEntityOperations {
change.executeWith(interpreter);
publisher.publishEvent(new AfterDelete(specifiedId, optionalEntity, change));
}
void doDelete(Object rootId, PropertyPath propertyPath) {
JdbcPersistentEntity<?> rootEntity = context.getRequiredPersistentEntity(propertyPath.getOwningType());
JdbcPersistentProperty referencingProperty = rootEntity.getRequiredPersistentProperty(propertyPath.getSegment());
Assert.notNull(referencingProperty, "No property found matching the PropertyPath " + propertyPath);
String format = sql(rootEntity.getType()).createDeleteByPath(propertyPath);
HashMap<String, Object> parameters = new HashMap<>();
parameters.put("rootId", rootId);
operations.update(format, parameters);
}
void doDelete(Object id, Class<?> domainType) {
String deleteByIdSql = sql(domainType).getDeleteById();
MapSqlParameterSource parameter = createIdParameterSource(id, domainType);
operations.update(deleteByIdSql, parameter);
}
private <T> AggregateChange createChange(T instance) {
@ -286,110 +161,4 @@ public class JdbcEntityTemplate implements JdbcEntityOperations { @@ -286,110 +161,4 @@ public class JdbcEntityTemplate implements JdbcEntityOperations {
jdbcEntityDeleteWriter.write(null, aggregateChange);
return aggregateChange;
}
private <T> MapSqlParameterSource createIdParameterSource(Object id, Class<T> domainType) {
return new MapSqlParameterSource("id",
convert(id, getRequiredPersistentEntity(domainType).getRequiredIdProperty().getColumnType()));
}
private <S> MapSqlParameterSource getPropertyMap(final S instance, JdbcPersistentEntity<S> persistentEntity) {
MapSqlParameterSource parameters = new MapSqlParameterSource();
persistentEntity.doWithProperties((PropertyHandler<JdbcPersistentProperty>) property -> {
if (!property.isEntity()) {
Object value = persistentEntity.getPropertyAccessor(instance).getProperty(property);
Object convertedValue = convert(value, property.getColumnType());
parameters.addValue(property.getColumnName(), convertedValue, JdbcUtil.sqlTypeFor(property.getColumnType()));
}
});
return parameters;
}
private <S, ID> ID getIdValueOrNull(S instance, JdbcPersistentEntity<S> persistentEntity) {
EntityInformation<S, ID> entityInformation = new BasicJdbcPersistentEntityInformation<>(persistentEntity);
ID idValue = entityInformation.getId(instance);
return isIdPropertySimpleTypeAndValueZero(idValue, persistentEntity) ? null : idValue;
}
private <S> void setIdFromJdbc(S instance, KeyHolder holder, JdbcPersistentEntity<S> persistentEntity) {
JdbcPersistentEntityInformation<S, ?> entityInformation = new BasicJdbcPersistentEntityInformation<>(
persistentEntity);
try {
getIdFromHolder(holder, persistentEntity).ifPresent(it -> {
Class<?> targetType = persistentEntity.getRequiredIdProperty().getType();
Object converted = convert(it, targetType);
entityInformation.setId(instance, converted);
});
} catch (NonTransientDataAccessException e) {
throw new UnableToSetId("Unable to set id of " + instance, e);
}
}
private <S> Optional<Object> getIdFromHolder(KeyHolder holder, JdbcPersistentEntity<S> persistentEntity) {
try {
// MySQL just returns one value with a special name
return Optional.ofNullable(holder.getKey());
} catch (InvalidDataAccessApiUsageException e) {
// Postgres returns a value for each column
return Optional.ofNullable(holder.getKeys().get(persistentEntity.getIdColumn()));
}
}
private <V> V convert(Object from, Class<V> to) {
if (from == null) {
return null;
}
JdbcPersistentEntity<?> persistentEntity = context.getPersistentEntity(from.getClass());
Object id = persistentEntity == null ? null : persistentEntity.getIdentifierAccessor(from).getIdentifier();
return conversions.convert(id == null ? from : id, to);
}
private <S, ID> boolean isIdPropertySimpleTypeAndValueZero(ID idValue, JdbcPersistentEntity<S> persistentEntity) {
JdbcPersistentProperty idProperty = persistentEntity.getIdProperty();
return idValue == null //
|| idProperty == null //
|| (idProperty.getType() == int.class && idValue.equals(0)) //
|| (idProperty.getType() == long.class && idValue.equals(0L));
}
@SuppressWarnings("unchecked")
private <S> JdbcPersistentEntity<S> getRequiredPersistentEntity(Class<S> domainType) {
return (JdbcPersistentEntity<S>) context.getRequiredPersistentEntity(domainType);
}
private SqlGenerator sql(Class<?> domainType) {
return sqlGeneratorSource.getSqlGenerator(domainType);
}
private <T> EntityRowMapper<T> getEntityRowMapper(Class<T> domainType) {
return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), conversions, context, this);
}
<T> void doDeleteAll(Class<T> domainType, PropertyPath propertyPath) {
operations.getJdbcOperations()
.update(sql(propertyPath == null ? domainType : propertyPath.getOwningType().getType())
.createDeleteAllSql(propertyPath));
}
public NamedParameterJdbcOperations getOperations() {
return operations;
}
}

56
src/main/java/org/springframework/data/jdbc/mybatis/MyBatisContext.java

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.mybatis;
import java.util.Map;
/**
* {@link MyBatisContext} instances get passed to MyBatis mapped statements as arguments, making Ids, instances, domainType and other attributes available to the statements.
*
* All methods might return {@literal null} depending on the kind of values available on invocation.
* @author Jens Schauder
*/
public class MyBatisContext {
private final Object id;
private final Object instance;
private final Class domainType;
private final Map<String, Object> additonalValues;
public MyBatisContext(Object id, Object instance, Class domainType, Map<String, Object> additonalValues) {
this.id = id;
this.instance = instance;
this.domainType = domainType;
this.additonalValues = additonalValues;
}
public Object getId() {
return id;
}
public Object getInstance() {
return instance;
}
public Class getDomainType() {
return domainType;
}
public Object get(String key) {
return additonalValues.get(key);
}
}

144
src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java

@ -0,0 +1,144 @@ @@ -0,0 +1,144 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.mybatis;
import java.util.Collections;
import java.util.Map;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.data.jdbc.core.DataAccessStrategy;
import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty;
import org.springframework.data.mapping.PropertyPath;
/**
* {@link DataAccessStrategy} implementation based on MyBatis.
*
* Each method gets mapped to a statement. The name of the statement gets constructed as follows:
*
* The namespace is based on the class of the entity plus the suffix "Mapper". This is then followed by the method name separated by a dot.
*
* For methods taking a {@link PropertyPath} as argument, the relevant entity is that of the root of the path, and the path itself gets as dot separated String appended to the statement name.
*
* Each statement gets an instance of {@link MyBatisContext}, which at least has the entityType set.
*
* For methods taking a {@link PropertyPath} the entityTyoe if the context is set to the class of the leaf type.
*
* @author Jens Schauder
*/
public class MyBatisDataAccessStrategy implements DataAccessStrategy {
private static final String MAPPER_SUFFIX = "Mapper";
private final SqlSessionFactory sqlSessionFactory;
public MyBatisDataAccessStrategy(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
@Override
public <T> void insert(T instance, Class<T> domainType, Map<String, Object> additionalParameters) {
sqlSession().insert(mapper(domainType) + ".insert",
new MyBatisContext(null, instance, domainType, additionalParameters));
}
@Override
public <S> void update(S instance, Class<S> domainType) {
sqlSession().update(mapper(domainType) + ".update",
new MyBatisContext(null, instance, domainType, Collections.emptyMap()));
}
@Override
public void delete(Object id, Class<?> domainType) {
sqlSession().delete(mapper(domainType) + ".delete",
new MyBatisContext(id, null, domainType, Collections.emptyMap()));
}
@Override
public void delete(Object rootId, PropertyPath propertyPath) {
sqlSession().delete(mapper(propertyPath.getOwningType().getType()) + ".delete." + propertyPath.toDotPath(),
new MyBatisContext(rootId, null, propertyPath.getLeafProperty().getTypeInformation().getType(),
Collections.emptyMap()));
}
@Override
public <T> void deleteAll(Class<T> domainType) {
sqlSession().delete( //
mapper(domainType) + ".deleteAll", //
new MyBatisContext(null, null, domainType, Collections.emptyMap()) //
);
}
@Override
public <T> void deleteAll(PropertyPath propertyPath) {
Class baseType = propertyPath.getOwningType().getType();
Class leaveType = propertyPath.getLeafProperty().getTypeInformation().getType();
sqlSession().delete( //
mapper(baseType) + ".deleteAll." + propertyPath.toDotPath(), //
new MyBatisContext(null, null, leaveType, Collections.emptyMap()) //
);
}
@Override
public <T> T findById(Object id, Class<T> domainType) {
return sqlSession().selectOne(mapper(domainType) + ".findById",
new MyBatisContext(id, null, domainType, Collections.emptyMap()));
}
@Override
public <T> Iterable<T> findAll(Class<T> domainType) {
return sqlSession().selectList(mapper(domainType) + ".findAll",
new MyBatisContext(null, null, domainType, Collections.emptyMap()));
}
@Override
public <T> Iterable<T> findAllById(Iterable<?> ids, Class<T> domainType) {
return sqlSession().selectList(mapper(domainType) + ".findAllById",
new MyBatisContext(ids, null, domainType, Collections.emptyMap()));
}
@Override
public <T> Iterable<T> findAllByProperty(Object rootId, JdbcPersistentProperty property) {
return sqlSession().selectList(mapper(property.getOwner().getType()) + ".findAllByProperty." + property.getName(),
new MyBatisContext(rootId, null, property.getType(), Collections.emptyMap()));
}
@Override
public <T> boolean existsById(Object id, Class<T> domainType) {
return sqlSession().selectOne(mapper(domainType) + ".existsById",
new MyBatisContext(id, null, domainType, Collections.emptyMap()));
}
@Override
public long count(Class<?> domainType) {
return sqlSession().selectOne(mapper(domainType) + ".count");
}
private String mapper(Class<?> domainType) {
return domainType.getName() + MAPPER_SUFFIX;
}
private SqlSession sqlSession() {
return sqlSessionFactory.openSession();
}
}

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

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

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

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

49
src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java

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

86
src/test/java/org/springframework/data/jdbc/core/CascadingDataAccessStrategyUnitTests.java

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

2
src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java

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

18
src/test/java/org/springframework/data/jdbc/core/JdbcEntityTemplateIntegrationTests.java

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

299
src/test/java/org/springframework/data/jdbc/core/MyBatisDataAccessStrategyUnitTests.java

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

34
src/test/java/org/springframework/data/jdbc/mybatis/DummyEntity.java

@ -0,0 +1,34 @@ @@ -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;
}
}

23
src/test/java/org/springframework/data/jdbc/mybatis/DummyEntityMapper.java

@ -0,0 +1,23 @@ @@ -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 {
}

111
src/test/java/org/springframework/data/jdbc/mybatis/MyBatisHsqlIntegrationTests.java

@ -0,0 +1,111 @@ @@ -0,0 +1,111 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.mybatis;
import static org.assertj.core.api.Assertions.*;
import junit.framework.AssertionFailedError;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.data.jdbc.repository.JdbcRepositoryIdGenerationIntegrationTests.TestConfiguration;
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
import org.springframework.data.repository.CrudRepository;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.rules.SpringClassRule;
import org.springframework.test.context.junit4.rules.SpringMethodRule;
import org.springframework.transaction.annotation.Transactional;
/**
* Tests the integration with Mybatis.
*
* @author Jens Schauder
*/
@ContextConfiguration
@Transactional
public class MyBatisHsqlIntegrationTests {
@org.springframework.context.annotation.Configuration
@Import(TestConfiguration.class)
@EnableJdbcRepositories(considerNestedRepositories = true)
static class Config {
@Autowired JdbcRepositoryFactory factory;
@Bean
Class<?> testClass() {
return MyBatisHsqlIntegrationTests.class;
}
@Bean
SqlSessionFactoryBean createSessionFactory(EmbeddedDatabase db) {
Configuration configuration = new Configuration();
configuration.getTypeAliasRegistry().registerAlias("MyBatisContext", MyBatisContext.class);
configuration.getTypeAliasRegistry().registerAlias(DummyEntity.class);
configuration.addMapper(DummyEntityMapper.class);
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(db);
sqlSessionFactoryBean.setConfiguration(configuration);
return sqlSessionFactoryBean;
}
}
@ClassRule public static final SpringClassRule classRule = new SpringClassRule();
@Rule public SpringMethodRule methodRule = new SpringMethodRule();
@Autowired SqlSessionFactory sqlSessionFactory;
@Autowired DummyEntityRepository repository;
@Test // DATAJDBC-123
public void mybatisSelfTest() {
SqlSession session = sqlSessionFactory.openSession();
session.selectList("org.springframework.data.jdbc.mybatis.DummyEntityMapper.findById");
}
@Test // DATAJDBC-123
public void myBatisGetsUsedForInsertAndSelect() {
DummyEntity entity = new DummyEntity(null, "some name");
DummyEntity saved = repository.save(entity);
assertThat(saved.id).isNotNull();
DummyEntity reloaded = repository.findById(saved.id).orElseThrow(AssertionFailedError::new);
assertThat(reloaded).isNotNull().extracting(e -> e.id, e -> e.name);
}
interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {
}
}

18
src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java

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

16
src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java

@ -19,6 +19,8 @@ import org.junit.Test; @@ -19,6 +19,8 @@ import org.junit.Test;
import org.mockito.stubbing.Answer;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.core.DefaultDataAccessStrategy;
import org.springframework.data.jdbc.core.SqlGeneratorSource;
import org.springframework.data.jdbc.mapping.event.AfterDelete;
import org.springframework.data.jdbc.mapping.event.AfterSave;
import org.springframework.data.jdbc.mapping.event.BeforeDelete;
@ -26,6 +28,7 @@ import org.springframework.data.jdbc.mapping.event.BeforeSave; @@ -26,6 +28,7 @@ import org.springframework.data.jdbc.mapping.event.BeforeSave;
import org.springframework.data.jdbc.mapping.event.Identifier;
import org.springframework.data.jdbc.mapping.event.JdbcEvent;
import org.springframework.data.jdbc.mapping.model.DefaultNamingStrategy;
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
import org.springframework.data.repository.CrudRepository;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
@ -44,8 +47,17 @@ public class SimpleJdbcRepositoryEventsUnitTests { @@ -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);
}

91
src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java

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

18
src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java

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

2
src/test/resources/logback.xml

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

1
src/test/resources/org.springframework.data.jdbc.mybatis/MyBatisHsqlIntegrationTests-hsql.sql

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

22
src/test/resources/org/springframework/data/jdbc/mybatis/DummyEntityMapper.xml

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