Add support for MySQL and Postgres testing.

* Introduce Testcontainers as the mechanism to test against real data stores.
* Alter CI to handle running Testcontainers in reduced privilege mode.
* Run the data store specific test cases ONLY for the baseline Java 8 test execution.

See #2256.
This commit is contained in:
Greg L. Turnquist
2021-07-22 16:57:58 -05:00
parent 183e3e5b62
commit a4ef56d83c
9 changed files with 698 additions and 9 deletions

View File

@@ -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<Object[]> employees = repository.genericObjectsFromResultSet();
assertThat(employees).containsExactly( //
new Object[] { 3, "Fanny" }, //
new Object[] { 4, "Gabriel" });
}
@Test // #2256
void testEntityListFromResultSet() {
List<Employee> employees = repository.entityListFromResultSet();
assertThat(employees).containsExactly( //
new Employee(3, "Fanny"), //
new Employee(4, "Gabriel"));
}
@Test // #2256
void testNamedOutputParameter() {
List<Employee> 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<Employee> 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<Employee> 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<Employee, Integer> {
@Procedure(value = "get_employees")
Object[] genericSingleObjectFromResultSet();
@Procedure(value = "get_employees")
List<Object[]> genericObjectsFromResultSet();
@Procedure(value = "get_employees")
List<Employee> entityListFromResultSet();
@Procedure(value = "get_employees", outputParameterName = "p_employees")
List<Employee> namedOutputParameter();
@Procedure(value = "get_single_employee")
Employee singleEntityFromResultSet();
@Procedure(value = "get_single_employee")
List<Employee> entityListFromSingleRowResultSet();
@Procedure(value = "get_employees_count")
Integer noResultSet();
@Procedure(name = "get_employees_mysql")
List<Employee> 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;
}
}
}

View File

@@ -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<Object[]> employees = repository.genericObjectsFromResultSet();
assertThat(employees).containsExactly( //
new Object[] { new BigDecimal("3"), "Fanny" }, //
new Object[] { new BigDecimal("4"), "Gabriel" });
}
@Test // 2256
void testEntityListFromResultSet() {
List<Employee> employees = repository.entityListFromResultSet();
assertThat(employees).containsExactly( //
new Employee(3, "Fanny"), //
new Employee(4, "Gabriel"));
}
@Test // 2256
void testNamedOutputParameter() {
List<Employee> 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<Employee> 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<Employee> 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<Employee, Integer> {
@Procedure(value = "get_employees", refCursor = true)
Object[] genericSingleObjectFromResultSet();
@Procedure(value = "get_employees", refCursor = true)
List<Object[]> genericObjectsFromResultSet();
@Procedure(value = "get_employees", refCursor = true)
List<Employee> entityListFromResultSet();
@Procedure(value = "get_employees", outputParameterName = "p_employees", refCursor = true)
List<Employee> namedOutputParameter();
@Procedure(value = "get_single_employee", refCursor = true)
Employee singleEntityFromResultSet();
@Procedure(value = "get_single_employee", refCursor = true)
List<Employee> entityListFromSingleRowResultSet();
@Procedure(value = "get_employees_count")
Integer noResultSet();
@Procedure(name = "get_employees_postgres", refCursor = true)
List<Employee> 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;
}
}
}

View File

@@ -7,10 +7,15 @@
</encoder>
</appender>
<logger name="org.springframework.data" level="error" />
<logger name="org.springframework.data" level="error"/>
<!-- Uncomment these sections to debug -->
<!-- <logger name="org.springframework.data.jpa" level="trace" />-->
<!-- <logger name="org.springframework.jdbc" level="debug" />-->
<!-- <logger name="org.testcontainers" level="debug" />-->
<root level="error">
<appender-ref ref="console" />
<appender-ref ref="console"/>
</root>
</configuration>

View File

@@ -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;;

View File

@@ -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$;;