diff --git a/Jenkinsfile b/Jenkinsfile index 0f34e477b..906d2af6c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -24,13 +24,16 @@ pipeline { } options { timeout(time: 30, unit: 'MINUTES') } environment { + DOCKER_HUB = credentials('hub.docker.com-springbuildmaster') ARTIFACTORY = credentials('02bd1690-b54f-4c9f-819d-a77cb7a9822c') } steps { script { docker.withRegistry('', 'hub.docker.com-springbuildmaster') { - docker.image('adoptopenjdk/openjdk8:latest').inside('-v $HOME:/tmp/jenkins-home') { - sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -s settings.xml clean dependency:list test -Dsort -Dbundlor.enabled=false -U -B' + docker.image('adoptopenjdk/openjdk8:latest').inside('-u root -v /var/run/docker.sock:/var/run/docker.sock -v /usr/bin/docker:/usr/bin/docker -v $HOME:/tmp/jenkins-home') { + sh "docker login --username ${DOCKER_HUB_USR} --password ${DOCKER_HUB_PSW}" + sh 'PROFILE=all-dbs ci/test.sh' + sh "ci/clean.sh" } } } diff --git a/ci/clean.sh b/ci/clean.sh new file mode 100755 index 000000000..7b38a05c9 --- /dev/null +++ b/ci/clean.sh @@ -0,0 +1,6 @@ +#!/bin/bash -x + +set -euo pipefail + +MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" \ + ./mvnw -s settings.xml clean -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-jpa diff --git a/ci/test.sh b/ci/test.sh new file mode 100755 index 000000000..ffb33bccf --- /dev/null +++ b/ci/test.sh @@ -0,0 +1,10 @@ +#!/bin/bash -x + +set -euo pipefail + +mkdir -p /tmp/jenkins-home/.m2/spring-data-jpa +chown -R 1001:1001 . + +MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" \ + ./mvnw -s settings.xml \ + -P${PROFILE} clean dependency:list test -Dsort -U -B -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-jpa \ No newline at end of file diff --git a/pom.xml b/pom.xml index c5902fb8f..1c1562144 100644 --- a/pom.xml +++ b/pom.xml @@ -23,9 +23,12 @@ 2.7.9 5.5.3.Final - 0.10.3 - org.hibernate + 8.0.23 + 42.2.19 2.6.0-SNAPSHOT + 0.10.3 + + org.hibernate spring.data.jpa @@ -34,6 +37,43 @@ + + all-dbs + + + + org.apache.maven.plugins + maven-surefire-plugin + + + mysql-test + test + + test + + + + **/MySql*IntegrationTests.java + + + + + postgres-test + test + + test + + + + **/Postgres*IntegrationTests.java + + + + + + + + hibernate-next @@ -116,6 +156,18 @@ + + + + org.testcontainers + testcontainers-bom + ${testcontainers} + pom + import + + + + @@ -187,6 +239,40 @@ test + + + mysql + mysql-connector-java + ${mysql-connector-java} + test + + + + org.testcontainers + mysql + test + + + org.slf4j + jcl-over-slf4j + + + + + + + org.postgresql + postgresql + ${postgresql} + test + + + + org.testcontainers + postgresql + test + + org.threeten threetenbp @@ -325,6 +411,7 @@ + default-test @@ -333,7 +420,7 @@ - unit-tests + unit-test test @@ -345,7 +432,7 @@ - integration-tests + integration-test test @@ -359,6 +446,8 @@ **/*UnitTests.java **/OpenJpa* **/EclipseLink* + **/MySql* + **/Postgres* -javaagent:${settings.localRepository}/org/springframework/spring-instrument/${spring}/spring-instrument-${spring}.jar @@ -367,7 +456,7 @@ - eclipselink-tests + eclipselink-test test diff --git a/src/test/java/org/springframework/data/jpa/repository/procedures/MySqlStoredProcedureIntegrationTests.java b/src/test/java/org/springframework/data/jpa/repository/procedures/MySqlStoredProcedureIntegrationTests.java new file mode 100644 index 000000000..2bde92038 --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/procedures/MySqlStoredProcedureIntegrationTests.java @@ -0,0 +1,252 @@ +/* + * Copyright 2015-2021 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 + * + * https://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.jpa.repository.procedures; + +import static org.assertj.core.api.Assertions.*; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Properties; + +import javax.persistence.Entity; +import javax.persistence.EntityManagerFactory; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.NamedStoredProcedureQuery; +import javax.sql.DataSource; +import javax.transaction.Transactional; + +import org.hibernate.dialect.MySQL8Dialect; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.FilterType; +import org.springframework.core.io.ClassPathResource; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.jpa.repository.query.Procedure; +import org.springframework.jdbc.datasource.init.DataSourceInitializer; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; +import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.testcontainers.containers.MySQLContainer; + +import com.mysql.cj.jdbc.MysqlDataSource; + +/** + * Testcase to verify {@link org.springframework.jdbc.object.StoredProcedure}s work with MySQL. + * + * @author Gabriel Basilio + * @author Greg Turnquist + */ +@Transactional +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = MySqlStoredProcedureIntegrationTests.Config.class) +public class MySqlStoredProcedureIntegrationTests { + + @Autowired EmployeeRepositoryWithNoCursor repository; + + @Test // #2256 + void testGenericSingleObjectFromResultSet() { + + Object[] employee = repository.genericSingleObjectFromResultSet(); + + assertThat(employee).containsExactly( // + new Object[] { 3, "Fanny" }, // + new Object[] { 4, "Gabriel" }); + } + + @Test // #2256 + void testGenericObjectsFromResultSet() { + + List employees = repository.genericObjectsFromResultSet(); + + assertThat(employees).containsExactly( // + new Object[] { 3, "Fanny" }, // + new Object[] { 4, "Gabriel" }); + } + + @Test // #2256 + void testEntityListFromResultSet() { + + List employees = repository.entityListFromResultSet(); + + assertThat(employees).containsExactly( // + new Employee(3, "Fanny"), // + new Employee(4, "Gabriel")); + } + + @Test // #2256 + void testNamedOutputParameter() { + + List employees = repository.namedOutputParameter(); + + assertThat(employees).containsExactly( // + new Employee(3, "Fanny"), // + new Employee(4, "Gabriel")); + } + + @Test // #2256 + void testSingleEntityFromResultSet() { + + Employee employee = repository.singleEntityFromResultSet(); + + assertThat(employee).isEqualTo(new Employee(3, "Fanny")); + } + + @Test // #2256 + void testEntityListFromSingleRowResultSet() { + + List employees = repository.entityListFromSingleRowResultSet(); + + assertThat(employees).containsExactly(new Employee(3, "Fanny")); + } + + @Test // #2256 + void testNoResultSet() { + + int count = repository.noResultSet(); + + assertThat(count).isEqualTo(2); + } + + @Test // #2256 + void testEntityListFromNamedProcedure() { + + List employees = repository.entityListFromNamedProcedure(); + + assertThat(employees).containsExactly( // + new Employee(3, "Fanny"), // + new Employee(4, "Gabriel")); + } + + @Data + @Entity + @AllArgsConstructor + @NoArgsConstructor + @NamedStoredProcedureQuery(name = "get_employees_mysql", procedureName = "get_employees", + resultClasses = Employee.class) + public static class Employee { + + @Id @GeneratedValue private Integer id; + private String name; + } + + @Transactional + public interface EmployeeRepositoryWithNoCursor extends JpaRepository { + + @Procedure(value = "get_employees") + Object[] genericSingleObjectFromResultSet(); + + @Procedure(value = "get_employees") + List genericObjectsFromResultSet(); + + @Procedure(value = "get_employees") + List entityListFromResultSet(); + + @Procedure(value = "get_employees", outputParameterName = "p_employees") + List namedOutputParameter(); + + @Procedure(value = "get_single_employee") + Employee singleEntityFromResultSet(); + + @Procedure(value = "get_single_employee") + List entityListFromSingleRowResultSet(); + + @Procedure(value = "get_employees_count") + Integer noResultSet(); + + @Procedure(name = "get_employees_mysql") + List entityListFromNamedProcedure(); + } + + @EnableJpaRepositories(considerNestedRepositories = true, basePackageClasses = Config.class, + includeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = EmployeeRepositoryWithNoCursor.class)) + @EnableTransactionManagement + static class Config { + + private MySQLContainer MYSQL_CONTAINER; + + @Bean + public DataSource dataSource() { + + if (MYSQL_CONTAINER == null) { + + MYSQL_CONTAINER = new MySQLContainer<>("mysql:8.0.24") // + .withUsername("test") // + .withPassword("test") // + .withConfigurationOverride(""); + MYSQL_CONTAINER.start(); + } + + MysqlDataSource dataSource = new MysqlDataSource(); + dataSource.setUrl(MYSQL_CONTAINER.getJdbcUrl()); + dataSource.setUser("root"); + dataSource.setPassword(MYSQL_CONTAINER.getPassword()); + dataSource.setDatabaseName(MYSQL_CONTAINER.getDatabaseName()); + + return dataSource; + } + + @Bean + public AbstractEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) { + + LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean(); + factoryBean.setDataSource(dataSource); + factoryBean.setPersistenceUnitRootLocation("simple-persistence"); + factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); + factoryBean.setPackagesToScan(this.getClass().getPackage().getName()); + + Properties properties = new Properties(); + properties.setProperty("hibernate.hbm2ddl.auto", "create"); + properties.setProperty("hibernate.dialect", MySQL8Dialect.class.getCanonicalName()); + factoryBean.setJpaProperties(properties); + + return factoryBean; + } + + @Bean + PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { + return new JpaTransactionManager(entityManagerFactory); + } + + @Bean + DataSourceInitializer initializer(DataSource dataSource) { + + DataSourceInitializer initializer = new DataSourceInitializer(); + initializer.setDataSource(dataSource); + + ClassPathResource script = new ClassPathResource("scripts/mysql-stored-procedures.sql"); + ResourceDatabasePopulator populator = new ResourceDatabasePopulator(script); + populator.setSeparator(";;"); + initializer.setDatabasePopulator(populator); + + return initializer; + } + } +} diff --git a/src/test/java/org/springframework/data/jpa/repository/procedures/PostgresStoredProcedureIntegrationTests.java b/src/test/java/org/springframework/data/jpa/repository/procedures/PostgresStoredProcedureIntegrationTests.java new file mode 100644 index 000000000..ae6b42366 --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/procedures/PostgresStoredProcedureIntegrationTests.java @@ -0,0 +1,252 @@ +/* + * Copyright 2015-2021 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 + * + * https://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.jpa.repository.procedures; + +import static org.assertj.core.api.Assertions.*; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Properties; + +import javax.persistence.Entity; +import javax.persistence.EntityManagerFactory; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.NamedStoredProcedureQuery; +import javax.persistence.ParameterMode; +import javax.persistence.StoredProcedureParameter; +import javax.sql.DataSource; +import javax.transaction.Transactional; + +import org.hibernate.dialect.PostgreSQL91Dialect; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.postgresql.ds.PGSimpleDataSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.FilterType; +import org.springframework.core.io.ClassPathResource; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.jpa.repository.query.Procedure; +import org.springframework.jdbc.datasource.init.DataSourceInitializer; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; +import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.testcontainers.containers.PostgreSQLContainer; + +/** + * Testcase to verify {@link org.springframework.jdbc.object.StoredProcedure}s work with Postgres. + * + * @author Gabriel Basilio + * @author Greg Turnquist + */ +@Transactional +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = PostgresStoredProcedureIntegrationTests.Config.class) +public class PostgresStoredProcedureIntegrationTests { + + @Autowired EmployeeRepositoryWithRefCursor repository; + + @Test // 2256 + void testGenericSingleObjectFromResultSet() { + + Object[] employee = repository.genericSingleObjectFromResultSet(); + + assertThat(employee).containsExactly( // + new Object[] { new BigDecimal("3"), "Fanny" }, // + new Object[] { new BigDecimal("4"), "Gabriel" }); + } + + @Test // 2256 + void testGenericObjectsFromResultSet() { + + List employees = repository.genericObjectsFromResultSet(); + + assertThat(employees).containsExactly( // + new Object[] { new BigDecimal("3"), "Fanny" }, // + new Object[] { new BigDecimal("4"), "Gabriel" }); + } + + @Test // 2256 + void testEntityListFromResultSet() { + + List employees = repository.entityListFromResultSet(); + + assertThat(employees).containsExactly( // + new Employee(3, "Fanny"), // + new Employee(4, "Gabriel")); + } + + @Test // 2256 + void testNamedOutputParameter() { + + List employees = repository.namedOutputParameter(); + + assertThat(employees).containsExactly( // + new Employee(3, "Fanny"), // + new Employee(4, "Gabriel")); + } + + @Test // 2256 + void testSingleEntityFromResultSet() { + + Employee employee = repository.singleEntityFromResultSet(); + + assertThat(employee).isEqualTo(new Employee(3, "Fanny")); + } + + @Test // 2256 + void testEntityListFromSingleRowResultSet() { + + List employees = repository.entityListFromSingleRowResultSet(); + + assertThat(employees).containsExactly(new Employee(3, "Fanny")); + } + + @Test // 2256 + void testNoResultSet() { + + int count = repository.noResultSet(); + + assertThat(count).isEqualTo(2); + } + + @Test // 2256 + void testEntityListFromNamedProcedure() { + + List employees = repository.entityListFromNamedProcedure(); + + assertThat(employees).containsExactly( // + new Employee(3, "Fanny"), // + new Employee(4, "Gabriel")); + } + + @Data + @Entity + @AllArgsConstructor + @NoArgsConstructor + @NamedStoredProcedureQuery(name = "get_employees_postgres", procedureName = "get_employees", + parameters = { @StoredProcedureParameter(mode = ParameterMode.REF_CURSOR, type = void.class) }, + resultClasses = Employee.class) + public static class Employee { + + @Id @GeneratedValue private Integer id; + private String name; + } + + @Transactional + public interface EmployeeRepositoryWithRefCursor extends JpaRepository { + + @Procedure(value = "get_employees", refCursor = true) + Object[] genericSingleObjectFromResultSet(); + + @Procedure(value = "get_employees", refCursor = true) + List genericObjectsFromResultSet(); + + @Procedure(value = "get_employees", refCursor = true) + List entityListFromResultSet(); + + @Procedure(value = "get_employees", outputParameterName = "p_employees", refCursor = true) + List namedOutputParameter(); + + @Procedure(value = "get_single_employee", refCursor = true) + Employee singleEntityFromResultSet(); + + @Procedure(value = "get_single_employee", refCursor = true) + List entityListFromSingleRowResultSet(); + + @Procedure(value = "get_employees_count") + Integer noResultSet(); + + @Procedure(name = "get_employees_postgres", refCursor = true) + List entityListFromNamedProcedure(); + } + + @EnableJpaRepositories(considerNestedRepositories = true, + includeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = EmployeeRepositoryWithRefCursor.class)) + @EnableTransactionManagement + static class Config { + + private PostgreSQLContainer POSTGRESQL_CONTAINER; + + @Bean + public DataSource dataSource() { + + if (POSTGRESQL_CONTAINER == null) { + + POSTGRESQL_CONTAINER = new PostgreSQLContainer<>("postgres:9.6.12") // + .withUsername("postgres"); + POSTGRESQL_CONTAINER.start(); + } + + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setUrl(POSTGRESQL_CONTAINER.getJdbcUrl()); + dataSource.setUser(POSTGRESQL_CONTAINER.getUsername()); + dataSource.setPassword(POSTGRESQL_CONTAINER.getPassword()); + + return dataSource; + } + + @Bean + public AbstractEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) { + + LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean(); + factoryBean.setDataSource(dataSource); + factoryBean.setPersistenceUnitRootLocation("simple-persistence"); + factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); + factoryBean.setPackagesToScan(this.getClass().getPackage().getName()); + + Properties properties = new Properties(); + properties.setProperty("hibernate.hbm2ddl.auto", "create"); + properties.setProperty("hibernate.dialect", PostgreSQL91Dialect.class.getCanonicalName()); + factoryBean.setJpaProperties(properties); + + return factoryBean; + } + + @Bean + PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { + return new JpaTransactionManager(entityManagerFactory); + } + + @Bean + DataSourceInitializer initializer(DataSource dataSource) { + + DataSourceInitializer initializer = new DataSourceInitializer(); + initializer.setDataSource(dataSource); + + ClassPathResource script = new ClassPathResource("scripts/postgres-stored-procedures.sql"); + ResourceDatabasePopulator populator = new ResourceDatabasePopulator(script); + populator.setSeparator(";;"); + initializer.setDatabasePopulator(populator); + + return initializer; + } + } +} diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml index ad5cbef50..2bb9893fd 100644 --- a/src/test/resources/logback.xml +++ b/src/test/resources/logback.xml @@ -7,10 +7,15 @@ - + + + + + + - + \ No newline at end of file diff --git a/src/test/resources/scripts/mysql-stored-procedures.sql b/src/test/resources/scripts/mysql-stored-procedures.sql new file mode 100644 index 000000000..b696e3772 --- /dev/null +++ b/src/test/resources/scripts/mysql-stored-procedures.sql @@ -0,0 +1,27 @@ +CREATE TABLE employee +( + ID int NOT NULL, + name varchar(50) NOT NULL, + primary key (ID) +);; + +INSERT INTO employee (ID, NAME) VALUES (3, 'Fanny');; +INSERT INTO employee (ID, NAME) VALUES (4, 'Gabriel');; + +DROP PROCEDURE IF EXISTS get_employees;; +CREATE PROCEDURE get_employees() +BEGIN + SELECT * FROM employee; +END;; + +DROP PROCEDURE IF EXISTS get_employees_count;; +CREATE PROCEDURE get_employees_count(OUT employees INT) +BEGIN + SELECT COUNT(*) into employees FROM employee; +END;; + +DROP PROCEDURE IF EXISTS get_single_employee;; +CREATE PROCEDURE get_single_employee() +BEGIN + SELECT * FROM employee WHERE employee.ID = 3; +END;; diff --git a/src/test/resources/scripts/postgres-stored-procedures.sql b/src/test/resources/scripts/postgres-stored-procedures.sql new file mode 100644 index 000000000..9d8226ee7 --- /dev/null +++ b/src/test/resources/scripts/postgres-stored-procedures.sql @@ -0,0 +1,45 @@ +CREATE TABLE employee +( + ID numeric NOT NULL, + name text COLLATE pg_catalog."default" NOT NULL, + CONSTRAINT employee_pkey PRIMARY KEY (ID) +);; + +INSERT INTO employee (ID, NAME) VALUES (3, 'Fanny');; +INSERT INTO employee (ID, NAME) VALUES (4, 'Gabriel');; + +CREATE OR REPLACE FUNCTION get_employees() + RETURNS refcursor + LANGUAGE 'plpgsql' +AS +$BODY$ +DECLARE + ref refcursor; +BEGIN + OPEN ref FOR SELECT * FROM employee; + RETURN ref; +END; +$BODY$;; + +CREATE OR REPLACE FUNCTION get_employees_count() + RETURNS integer + LANGUAGE 'plpgsql' +AS +$BODY$ +BEGIN + RETURN (SELECT COUNT(*) FROM employee); +END; +$BODY$;; + +CREATE OR REPLACE FUNCTION get_single_employee() + RETURNS refcursor + LANGUAGE 'plpgsql' +AS +$BODY$ +DECLARE + ref refcursor; +BEGIN + OPEN ref FOR SELECT * FROM employee WHERE employee.ID = 3; + RETURN ref; +END; +$BODY$;;