diff --git a/build.gradle b/build.gradle index a2bbcb3..01405af 100644 --- a/build.gradle +++ b/build.gradle @@ -40,7 +40,10 @@ dependencies { 'com.github.javaplugs:mybatis-types:0.3', // required so mybatis works with java8 datetime classes (LocalDate) // jdbi dependencies - 'org.jdbi:jdbi:2.77' + 'org.jdbi:jdbi:2.77', + + // simpleflatmapper + 'org.simpleflatmapper:sfm-springjdbc:3.6' ) testCompile( diff --git a/src/main/java/com/clevergang/dbtests/repository/api/data/RegisterEmployeeInput.java b/src/main/java/com/clevergang/dbtests/repository/api/data/RegisterEmployeeInput.java new file mode 100644 index 0000000..a216020 --- /dev/null +++ b/src/main/java/com/clevergang/dbtests/repository/api/data/RegisterEmployeeInput.java @@ -0,0 +1,47 @@ +package com.clevergang.dbtests.repository.api.data; + +import java.math.BigDecimal; + +public class RegisterEmployeeInput { + + private final String name; + private final String surname; + private final String email; + private final BigDecimal salary; + private final String departmentName; + private final String companyName; + + + public RegisterEmployeeInput(String name, String surname, String email, BigDecimal salary, String departmentName, String companyName) { + this.name = name; + this.surname = surname; + this.email = email; + this.salary = salary; + this.departmentName = departmentName; + this.companyName = companyName; + } + + public String getName() { + return name; + } + + public String getSurname() { + return surname; + } + + public String getEmail() { + return email; + } + + public BigDecimal getSalary() { + return salary; + } + + public String getDepartmentName() { + return departmentName; + } + + public String getCompanyName() { + return companyName; + } +} diff --git a/src/main/java/com/clevergang/dbtests/repository/impl/jdbctemplate/JDBCWithSfmDataRepositoryImpl.java b/src/main/java/com/clevergang/dbtests/repository/impl/jdbctemplate/JDBCWithSfmDataRepositoryImpl.java new file mode 100644 index 0000000..44e166d --- /dev/null +++ b/src/main/java/com/clevergang/dbtests/repository/impl/jdbctemplate/JDBCWithSfmDataRepositoryImpl.java @@ -0,0 +1,279 @@ +package com.clevergang.dbtests.repository.impl.jdbctemplate; + +import com.clevergang.dbtests.repository.api.DataRepository; +import com.clevergang.dbtests.repository.api.data.Company; +import com.clevergang.dbtests.repository.api.data.Department; +import com.clevergang.dbtests.repository.api.data.Employee; +import com.clevergang.dbtests.repository.api.data.Project; +import com.clevergang.dbtests.repository.api.data.ProjectsWithCostsGreaterThanOutput; +import com.clevergang.dbtests.repository.api.data.RegisterEmployeeInput; +import com.clevergang.dbtests.repository.api.data.RegisterEmployeeOutput; +import org.simpleflatmapper.jdbc.spring.JdbcTemplateCrud; +import org.simpleflatmapper.jdbc.spring.JdbcTemplateMapperFactory; +import org.simpleflatmapper.jdbc.spring.SqlParameterSourceFactory; +import org.simpleflatmapper.map.property.RenameProperty; +import org.simpleflatmapper.util.CheckedConsumer; +import org.simpleflatmapper.util.ListCollector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.stereotype.Repository; + +import java.math.BigDecimal; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Spring JDBCTemplate with simpleflatmapper implementation of the DataRepository + * + */ +@Repository +public class JDBCWithSfmDataRepositoryImpl implements DataRepository { + private static final Logger logger = LoggerFactory.getLogger(JDBCWithSfmDataRepositoryImpl.class); + + private final NamedParameterJdbcTemplate jdbcTemplate; + + // Cruds provide insert, update, select and delete. + private final JdbcTemplateCrud companyCrud; + private final JdbcTemplateCrud projectCrud; + private final JdbcTemplateCrud employeeCrud; + private final JdbcTemplateCrud departmentCrud; + + // Spring row mappers implementation + private final RowMapper companyMapper; + private final RowMapper departmentMapper; + private final RowMapper employeeMapper; + + private final RowMapper projectsWithCostsGreaterThanOutputRowMapper; + private final RowMapper registerEmployeeOutputRowMapper; + + // Spring SqlParameterSource factory to create parameter source from RegisterEmployeeInput + private final SqlParameterSourceFactory registerEmployeeInputSqlParameterSourceFactory; + + @SuppressWarnings("SpringJavaAutowiringInspection") + @Autowired + public JDBCWithSfmDataRepositoryImpl(NamedParameterJdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + + JdbcOperations jdbcOperations = jdbcTemplate.getJdbcOperations(); + + JdbcTemplateMapperFactory mapperFactory = JdbcTemplateMapperFactory.newInstance(); + + companyCrud = + mapperFactory + .crud(Company.class, Integer.class) + .to(jdbcOperations, "company"); + employeeCrud = + mapperFactory + .crud(Employee.class, Integer.class) + .to(jdbcOperations, "employee"); + + departmentCrud = + mapperFactory + .crud(Department.class, Integer.class) + .to(jdbcOperations); // if table name is not specified will look for a matching one + + projectCrud = + JdbcTemplateMapperFactory + .newInstance() + // add alias from column datastarted to date property + .addAlias("datestarted", "date") + .crud(Project.class, Integer.class) + .to(jdbcOperations, "project"); + + companyMapper = + mapperFactory.newRowMapper(Company.class); + + departmentMapper = + mapperFactory.newRowMapper(Department.class); + employeeMapper = + mapperFactory.newRowMapper(Employee.class); + projectsWithCostsGreaterThanOutputRowMapper = + mapperFactory.newRowMapper(ProjectsWithCostsGreaterThanOutput.class); + registerEmployeeOutputRowMapper = + JdbcTemplateMapperFactory + .newInstance() + // add rules to rename id column name to pid in output object + .addColumnProperty( + k -> k.getName().contains("id"), + k -> new RenameProperty(k.getName().replace("id", "pid"))) + .newRowMapper(RegisterEmployeeOutput.class); + + registerEmployeeInputSqlParameterSourceFactory = + mapperFactory.newSqlParameterSourceFactory(RegisterEmployeeInput.class); + } + + @Override + public Company findCompany(Integer pid) { + logger.info("Finding Company by ID using companyCrud"); + + Company company = companyCrud.read(pid); + + logger.info("Found company: " + company); + + return company; + } + + @Override + public Company findCompanyUsingSimpleStaticStatement(Integer pid) { + String query; + query = "SELECT pid, address, name " + + "FROM company " + + "WHERE pid = " + pid; + + + Company company = jdbcTemplate.getJdbcOperations().queryForObject(query, companyMapper); + + logger.info("Found company: " + company); + + return company; + } + + @Override + public void removeProject(Integer pid) { + projectCrud.delete(pid); + } + + @Override + public Department findDepartment(Integer pid) { + return departmentCrud.read(pid); + } + + @Override + public List employeesWithSalaryGreaterThan(Integer minSalary) { + logger.info("Looking for employeesWithSalaryGreaterThan using JDBCTemplate"); + + String query = "SELECT * " + + " FROM employee" + + " WHERE salary > :salary"; + + MapSqlParameterSource params = new MapSqlParameterSource() + .addValue("salary", minSalary); + + // using BeanPropertyRowMapper is easier, but with much worse performance than custom RowMapper + return jdbcTemplate.query(query, params, employeeMapper); + } + + @Override + public Integer insertProject(Project project) { + return projectCrud.create(project, new CheckedConsumer() { + Integer key; + @Override + public void accept(Integer integer) throws Exception { + this.key = integer; + } + }).key; + } + + @Override + public List insertProjects(List projects) { + return projectCrud.create(projects, new ListCollector()).getList(); + } + + @Override + public void updateEmployee(Employee employeeToUpdate) { + logger.info("Updating employee using JDBC Template"); + employeeCrud.update(employeeToUpdate); + } + + @Override + public List getProjectsWithCostsGreaterThan(int totalCostBoundary) { + String query; + query = "WITH project_info AS (\n" + + " SELECT project.pid project_pid, project.name project_name, salary monthly_cost, company.name company_name\n" + + " FROM project\n" + + " JOIN projectemployee ON project.pid = projectemployee.project_pid\n" + + " JOIN employee ON projectemployee.employee_pid = employee.pid\n" + + " LEFT JOIN department ON employee.department_pid = department.pid\n" + + " LEFT JOIN company ON department.company_pid = company.pid\n" + + "),\n" + + "project_cost AS (\n" + + " SELECT project_pid, sum(monthly_cost) total_cost\n" + + " FROM project_info GROUP BY project_pid\n" + + ")\n" + + "SELECT project_name, total_cost, company_name, sum(monthly_cost) company_cost FROM project_info\n" + + " JOIN project_cost USING (project_pid)\n" + + "WHERE total_cost > :totalCostBoundary\n" + + "GROUP BY project_name, total_cost, company_name\n" + + "ORDER BY company_name"; + + MapSqlParameterSource params = new MapSqlParameterSource() + .addValue("totalCostBoundary", totalCostBoundary); + + return jdbcTemplate.query(query, params, projectsWithCostsGreaterThanOutputRowMapper); + } + + @Override + public Employee findEmployee(Integer pid) { + return employeeCrud.read(pid); + } + + @Override + public RegisterEmployeeOutput callRegisterEmployee(String name, String surname, String email, BigDecimal salary, String departmentName, String companyName) { + String query; + //noinspection SqlResolve + query = "SELECT employee_id, department_id, company_id FROM register_employee(\n" + + " _name := :name, \n" + + " _surname := :surname, \n" + + " _email := :email, \n" + + " _salary := :salary, \n" + + " _department_name := :departmentName, \n" + + " _company_name := :companyName\n" + + ")"; + + SqlParameterSource params = + registerEmployeeInputSqlParameterSourceFactory + .newSqlParameterSource( + new RegisterEmployeeInput(name, surname, email, salary, departmentName, companyName)); + + return jdbcTemplate.queryForObject(query, params, registerEmployeeOutputRowMapper); + } + + @Override + public Integer getProjectsCount() { + String query = "SELECT count(*) FROM project"; + return jdbcTemplate.getJdbcOperations().queryForObject(query, Integer.class); + } + + @Override + public List findDepartmentsOfCompany(Company company) { + String query = "SELECT pid, company_pid, name" + + " FROM department " + + " WHERE company_pid = :pid" + + " ORDER BY pid"; + + MapSqlParameterSource params = new MapSqlParameterSource() + .addValue("pid", company.getPid()); + + return jdbcTemplate.query(query, params, departmentMapper); + } + + @Override + public void deleteDepartments(List departmentsToDelete) { + List keys = departmentsToDelete.stream() + .map(Department::getPid) + .collect(Collectors.toList()); + + departmentCrud.delete(keys); + } + + @Override + public void updateDepartments(List departmentsToUpdate) { + departmentCrud.update(departmentsToUpdate); + } + + @Override + public void insertDepartments(List departmentsToInsert) { + departmentCrud.create(departmentsToInsert); + } + + @Override + public Project findProject(Integer pid) { + return projectCrud.read(pid); + } +} diff --git a/src/test/java/com/clevergang/dbtests/JdbcWithSfmTemplateScenariosTest.java b/src/test/java/com/clevergang/dbtests/JdbcWithSfmTemplateScenariosTest.java new file mode 100644 index 0000000..99b140e --- /dev/null +++ b/src/test/java/com/clevergang/dbtests/JdbcWithSfmTemplateScenariosTest.java @@ -0,0 +1,91 @@ +package com.clevergang.dbtests; + +import com.clevergang.dbtests.repository.impl.jdbctemplate.JDBCWithSfmDataRepositoryImpl; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.Rollback; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.annotation.Transactional; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest +@ContextConfiguration(classes = DbTestsApplication.class) +@Transactional +@Rollback +public class JdbcWithSfmTemplateScenariosTest { + + @Autowired + private JDBCWithSfmDataRepositoryImpl jdbcRepository; + + private Scenarios scenarios; + + @Before + public void setup() { + scenarios = new Scenarios(jdbcRepository); + } + + @Test + public void scenarioOne() { + scenarios.fetchSingleEntityScenario(1); + } + + @Test + public void scenarioTwo() { + scenarios.fetchListOfEntitiesScenario(25000); + } + + @Test + public void scenarioThree() { + scenarios.saveNewEntityScenario(); + } + + @Test + public void scenarioFour() { + scenarios.batchInsertMultipleEntitiesScenario(); + } + + @Test + public void scenarioFive() { + scenarios.updateCompleteEntityScenario(); + } + + @Test + public void scenarioSix() { + scenarios.fetchManyToOneRelationScenario(); + } + + @Test + public void scenarioSeven() { + scenarios.fetchOneToManyRelationScenario(); + } + + @Test + public void scenarioEight() { + scenarios.updateCompleteOneToManyRelationScenario(); + } + + @Test + public void scenarioNine() { + scenarios.executeComplexSelectScenario(); + } + + @Test + public void scenarioTen() { + scenarios.callStoredProcedureScenario(); + } + + @Test + public void scenarioEleven() { + scenarios.executeSimpleStaticStatementScenario(); + } + + @Test + public void scenarioTwelve() { + scenarios.removeSingleEntityScenario(); + } + +}