Skip to content

Commit 4eac21d

Browse files
author
bretislav.wajtr
committed
Finished implementation of MyBatis version + added asserts to the scenarios (for better verification of results)
1 parent a3b68f1 commit 4eac21d

18 files changed

Lines changed: 508 additions & 110 deletions

README.md

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
# Java repository layer frameworks comparison
22

3-
Comparison of usage of non-JPA SQL mapping (persistence) frameworks for Java (Jooq, Spring JDBCTemplate etc.).
3+
Comparison of usage of non-JPA SQL mapping (persistence) frameworks for Java (Jooq, Spring JDBCTemplate, MyBatis etc.).
44

5-
We are not comparing performance (that'll be maybe added later), but rather how are these frameworks used for everyday tasks.
5+
We are not comparing performance, but rather how are these frameworks used for everyday tasks.
66

7-
We prepared some common scenarios, which you typically need to implement data-centric application, and then we implemented these scenarios using various non-JPA DB layer frameworks.
7+
We prepared some common scenarios, which you typically need to implement data-centric application, and then we implemented these scenarios using various non-JPA DB layer frameworks. This project should serve
8+
- as point of reference when deciding for SQL mapping framework
9+
- as a template of common framework usage scenarios (see scenarios below)
10+
- to document best practices of such common usages (**comments are welcomed!**)
811

912
## Frameworks compared
1013

1114
* Spring JDBCTemplate (see [implementation](src/main/java/com/clevergang/dbtests/repository/impl/jdbctemplate/JDBCDataRepositoryImpl.java))
1215
* jOOQ (see [implementation](src/main/java/com/clevergang/dbtests/repository/impl/jooq/JooqDataRepositoryImpl.java))
1316

14-
We tried to find optimal (== most readable) implementation in every framework, but comments are welcomed! There are lot of comments explaining why we chose to such implementation and some FIXMEs on places which we do not like, but which cannot be implemented differently or which we do not know how to improve...
17+
We tried to find optimal (== most readable) implementation in every framework, but comments are welcomed! There are lot of comments explaining why we chose to such implementation and some FIXMEs on places which we do not like, but which cannot be implemented differently or which we have troubles to improve...
1518

1619
## Scenarios implemented
1720

@@ -49,11 +52,6 @@ Well, we were trying to "stick with standard" in our projects so we used JPA in
4952

5053
So we dropped JPA completely, started using JDBCTemplate and discovered that we can deliver apps sooner (which was kind of surprising), they are a lot faster (thanks to effective use of DB) and much more robust... This was really relaxing and we do not plan to return to JPA at all... (yes, even for CRUD applications!)
5154

52-
This project aims to explore other options in the SQL mapping area than just JDBCTemplate. It should serve us
53-
54-
- as point of reference when deciding for SQL mapping framework
55-
- as a template of common DB usage scenarios
56-
- to document best practices of such common usages (**comments are welcomed!**)
57-
55+
This project aims to explore other options in the SQL mapping area than just JDBCTemplate.
5856

5957
**Use code in the repository as you like (MIT License)**

build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ description = 'Comparison and common patterns for non-JPA SQL mapping frameworks
1717

1818
repositories {
1919
jcenter()
20+
maven { url "https://jitpack.io" }
2021
}
2122

2223
// We need Java8 for this project
@@ -32,6 +33,9 @@ dependencies {
3233
compile "org.apache.commons:commons-lang3:3.4"
3334
compile "org.apache.commons:commons-collections4:4.1"
3435

36+
// required so mybatis works with java8 datetime classes (LocalDate)
37+
compile 'com.github.javaplugs:mybatis-types:0.3'
38+
3539
testCompile "org.springframework.boot:spring-boot-starter-test"
3640
}
3741

sql-updates/create-script.sql

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ CREATE TABLE Company (
1818

1919
CREATE TABLE Department (
2020
pid SERIAL PRIMARY KEY,
21-
company_pid INTEGER NOT NULL REFERENCES Company (pid) ON DELETE CASCADE,
21+
company_pid INTEGER NOT NULL REFERENCES Company (pid) ON DELETE NO ACTION,
2222
name TEXT NOT NULL
2323
);
2424

2525
CREATE TABLE Employee (
2626
pid SERIAL PRIMARY KEY,
27-
department_pid INTEGER NOT NULL REFERENCES Department (pid) ON DELETE CASCADE,
27+
department_pid INTEGER NOT NULL REFERENCES Department (pid) ON DELETE NO ACTION,
2828
name TEXT NOT NULL,
2929
surname TEXT NOT NULL,
3030
email TEXT,
@@ -40,8 +40,8 @@ CREATE TABLE Project (
4040
);
4141

4242
CREATE TABLE ProjectEmployee (
43-
project_pid INTEGER REFERENCES Project,
44-
employee_pid INTEGER REFERENCES Employee,
43+
project_pid INTEGER REFERENCES Project ON DELETE NO ACTION,
44+
employee_pid INTEGER REFERENCES Employee ON DELETE NO ACTION,
4545
PRIMARY KEY (project_pid, employee_pid)
4646
);
4747

src/main/java/com/clevergang/dbtests/DbTestsApplication.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
package com.clevergang.dbtests;
22

3+
import org.apache.ibatis.session.ExecutorType;
4+
import org.apache.ibatis.session.SqlSession;
5+
import org.apache.ibatis.session.SqlSessionFactory;
36
import org.jooq.conf.RenderNameStyle;
47
import org.jooq.conf.Settings;
58
import org.jooq.conf.StatementType;
9+
import org.mybatis.spring.SqlSessionFactoryBean;
10+
import org.mybatis.spring.SqlSessionTemplate;
611
import org.springframework.beans.factory.annotation.Qualifier;
712
import org.springframework.boot.SpringApplication;
813
import org.springframework.boot.autoconfigure.SpringBootApplication;
914
import org.springframework.context.annotation.Bean;
1015
import org.springframework.context.annotation.Configuration;
1116
import org.springframework.context.annotation.Primary;
17+
import org.springframework.core.io.ClassPathResource;
18+
19+
import javax.sql.DataSource;
1220

1321
/**
1422
* This whole thing is based on Spring Boot as we plan to use these frameworks with SpringBoot
@@ -17,6 +25,10 @@
1725
@Configuration
1826
public class DbTestsApplication {
1927

28+
/**
29+
* JOOQ CONFIGURATIONS
30+
**/
31+
2032
@Bean
2133
@Primary
2234
public Settings jooqSettings() {
@@ -35,6 +47,37 @@ public Settings jooqStaticStatementSettings() {
3547
return ret;
3648
}
3749

50+
/**
51+
* MYBATIS CONFIGURATIONS
52+
**/
53+
54+
@Bean
55+
@Primary
56+
@SuppressWarnings("SpringJavaAutowiringInspection")
57+
public SqlSession myBatisDefaultSession(SqlSessionFactory sqlSessionFactory) {
58+
return new SqlSessionTemplate(sqlSessionFactory);
59+
}
60+
61+
@Bean
62+
@Qualifier("batch-operations")
63+
@SuppressWarnings("SpringJavaAutowiringInspection")
64+
public SqlSession myBatisBatchOperationstSession(DataSource dataSource) throws Exception {
65+
/*
66+
NOTE: Unfortunately, in MyBatis it's not possible to execute batch and non-batch operations in single SqlSession.
67+
To support this scenario, we have to create completely new SqlSessionFactoryBean and completely new
68+
SqlSession. Surprisingly, this does not necessarily mean that the batch and non-batch operations will be
69+
executed in different transactions (as we would expect) - we tested this configuration using scenario 8.
70+
and it turned out that the bot non-batch and batch operations were run using same connection and in same transaction.
71+
I guess this has something to do with how connection is obtained by MyBatis from dataSource...
72+
*/
73+
74+
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
75+
sqlSessionFactoryBean.setDataSource(dataSource);
76+
sqlSessionFactoryBean.setConfigLocation(new ClassPathResource("mybatis/mybatis-config.xml"));
77+
78+
return new SqlSessionTemplate(sqlSessionFactoryBean.getObject(), ExecutorType.BATCH);
79+
}
80+
3881
public static void main(String[] args) {
3982
SpringApplication.run(DbTestsApplication.class, args);
4083
}

src/main/java/com/clevergang/dbtests/Scenarios.java

Lines changed: 81 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
import org.apache.commons.lang3.RandomStringUtils;
77
import org.slf4j.Logger;
88
import org.slf4j.LoggerFactory;
9-
import org.springframework.util.StopWatch;
109

1110
import java.math.BigDecimal;
1211
import java.time.LocalDate;
13-
import java.util.*;
12+
import java.util.ArrayList;
13+
import java.util.Collection;
14+
import java.util.List;
15+
import java.util.Map;
1416
import java.util.stream.Collectors;
1517

1618
import static java.util.stream.Collectors.toList;
@@ -43,6 +45,10 @@ public Scenarios(DataRepository repository) {
4345
public void fetchSingleEntityScenario(Integer companyPid) {
4446
Company company = repository.findCompany(companyPid);
4547

48+
// check some post conditions
49+
assert company != null;
50+
assert company.getPid().equals(companyPid);
51+
assert company.getName().equals("CleverGang");
4652
logger.info("Fetched result: {}", company);
4753
}
4854

@@ -55,6 +61,9 @@ public void fetchSingleEntityScenario(Integer companyPid) {
5561
public void fetchListOfEntitiesScenario(Integer employeeMinSalary) {
5662
List<Employee> employees = repository.employeesWithSalaryGreaterThan(employeeMinSalary);
5763

64+
// check some post conditions
65+
assert employees != null;
66+
assert employees.size() == 3;
5867
logger.info("Fetched result: {}", employees);
5968
}
6069

@@ -66,8 +75,14 @@ public void saveNewEntityScenario() {
6675
project.setName("TestProject");
6776
project.setDate(LocalDate.now());
6877

69-
repository.insertProject(project);
70-
}
78+
// SCENARIO CODE STARTS HERE
79+
Integer newPid = repository.insertProject(project);
80+
81+
// check some post conditions
82+
assert newPid != null;
83+
assert newPid > 2;
84+
logger.info("Scenario three, pid of inserted entity: {}", newPid);
85+
}
7186

7287
/**
7388
* 4. Batch insert multiple entities of same type and return generated keys
@@ -81,17 +96,20 @@ public void saveNewEntityScenario() {
8196
public void batchInsertMultipleEntitiesScenario() {
8297
// create a list of thousand products
8398
List<Project> projects = new ArrayList<>();
84-
StopWatch watch = new StopWatch();
85-
watch.start();
8699
for (int i = 0; i < 1000; i++) {
87100
Project project = new Project();
88101
project.setName(RandomStringUtils.randomAlphabetic(10));
89102
project.setDate(LocalDate.now());
90103
projects.add(project);
91104
}
92-
repository.insertProjects(projects);
93-
watch.stop();
94-
logger.info("Total time {} ms", watch.getTotalTimeMillis());
105+
106+
// SCENARIO CODE STARTS HERE
107+
List<Integer> newPids = repository.insertProjects(projects);
108+
109+
// check some postconditions
110+
Integer projectsCount = repository.getProjectsCount();
111+
assert projectsCount == 1002;
112+
logger.info("Scenario 4. output {}", newPids);
95113
}
96114

97115
/**
@@ -103,22 +121,29 @@ public void batchInsertMultipleEntitiesScenario() {
103121
*/
104122
public void updateCompleteEntityScenario() {
105123
// Imagine that this object comes from UI edit dialog, which is typical scenario
106-
Employee employeeToUpdate = performSomeEmployeeRecordUpdateInUI();
107-
108-
// SCENARIO CODE STARTS HERE - update the employee in DB
124+
Employee employeeToUpdate = performSomeEmployeeRecordModificationsInUI(1);
109125

126+
// SCENARIO CODE STARTS HERE
110127
repository.updateEmployee(employeeToUpdate);
111128

129+
// check some post conditions
130+
Employee updatedEmployee = repository.findEmployee(1);
131+
assert employeeToUpdate.getPid().equals(updatedEmployee.getPid());
132+
assert employeeToUpdate.getDepartmentPid().equals(updatedEmployee.getDepartmentPid());
133+
assert employeeToUpdate.getName().equals(updatedEmployee.getName());
134+
assert employeeToUpdate.getSurname().equals(updatedEmployee.getSurname());
135+
assert employeeToUpdate.getEmail().equals(updatedEmployee.getEmail());
136+
assert employeeToUpdate.getSalary().equals(updatedEmployee.getSalary());
112137
}
113138

114-
private Employee performSomeEmployeeRecordUpdateInUI() {
139+
private Employee performSomeEmployeeRecordModificationsInUI(Integer employeePid) {
115140
Employee employeeToUpdate = new Employee();
116-
employeeToUpdate.setPid(1);
117-
employeeToUpdate.setDepartmentPid(1);
118-
employeeToUpdate.setName("Curt");
119-
employeeToUpdate.setSurname("Odegaard");
120-
employeeToUpdate.setEmail("curt.odegaard@updated.com"); // <-- this is updated value
121-
employeeToUpdate.setSalary(new BigDecimal("15000")); // <-- this is updated value
141+
employeeToUpdate.setPid(employeePid);
142+
employeeToUpdate.setDepartmentPid(6);
143+
employeeToUpdate.setName("Curt1");
144+
employeeToUpdate.setSurname("Odegaard1");
145+
employeeToUpdate.setEmail("curt.odegaard@updated.com1"); // <-- this is updated value
146+
employeeToUpdate.setSalary(new BigDecimal("15000.00")); // <-- this is updated value
122147
return employeeToUpdate;
123148
}
124149

@@ -128,13 +153,17 @@ private Employee performSomeEmployeeRecordUpdateInUI() {
128153
public void fetchManyToOneRelationScenario() {
129154
Department softwareDevelopmentDepartment = repository.findDepartment(3);
130155

156+
// SCENARIO CODE STARTS HERE
131157
// Getting Company for Department (many-to-one relation) in JPA is quite easy. You typically have
132158
// @ManyToOne relation defined in the Department entity class, so once you have instance of Department,
133159
// you just call department.getCompany() and JPA does the magic for you (typically one or more lazy selects
134160
// are executed). We don't have any such magical call here, but non-JPA approach is quite straightforward
135161
// too: we have company_pid, so just ask DataRepository for the record:
136162
Company company = repository.findCompany(softwareDevelopmentDepartment.getCompanyPid());
137163

164+
// check some post conditions
165+
assert company.getName().equals("CleverGang");
166+
assert company.getPid().equals(1);
138167
logger.info("Department {} is in the {} company", softwareDevelopmentDepartment.getName(), company.getName());
139168
}
140169

@@ -144,13 +173,16 @@ public void fetchManyToOneRelationScenario() {
144173
public void fetchOneToManyRelationScenario() {
145174
Company company = repository.findCompany(1);
146175

176+
// SCENARIO CODE STARTS HERE
147177
// For one-to-many relations the situation is quite similar to many-to-one relations (scenario six). In JPA this
148178
// is "easy" - you define @OneToMany relation in the Company entity and then you just call getDepartments() method ->
149179
// a lazy select is issued and Departments are fetched from DB. However you can also use EAGER FetchType strategy which
150180
// causes the relation to load along with the primary entity - this behavior is (by our opinion) the source of all evil
151181
// in JPA... So, in non-JPA approach, we don't have any "eager" loads, just explicit calls for data:
152182
List<Department> departments = repository.findDepartmentsOfCompany(company);
153183

184+
// check some post conditions
185+
assert departments.size() == 4;
154186
logger.info("There are {} departments in {} company", departments.size(), company.getName());
155187
}
156188

@@ -176,11 +208,16 @@ public void updateCompleteOneToManyRelationScenario() {
176208

177209

178210
// SCENARIO CODE STARTS HERE - update departments in DB
179-
180211
updateDepartments(company, newDepartments);
181212

182-
// now check the results
213+
// check some post conditions
183214
List<Department> departmentsForCompany = repository.findDepartmentsOfCompany(company);
215+
assert departmentsForCompany.size() == 5;
216+
assert departmentsForCompany.get(0).getName().equals("Back office");
217+
assert departmentsForCompany.get(1).getName().equals("IT Department Updated");
218+
assert departmentsForCompany.get(2).getName().equals("Software Development");
219+
assert departmentsForCompany.get(3).getName().equals("New department 1");
220+
assert departmentsForCompany.get(4).getName().equals("New department 2");
184221
logger.info("State of database at the end of scenario eight: {}", departmentsForCompany);
185222
}
186223

@@ -191,6 +228,7 @@ private void updateDepartments(Company company, List<Department> newDepartments)
191228

192229
// first get current departments
193230
List<Department> currentDepartments = repository.findDepartmentsOfCompany(company);
231+
logger.info("Company {} current departments {}", company.getName(), currentDepartments);
194232

195233
// now determine which departments were deleted
196234
Collection<Integer> newPIDs = CollectionUtils.collect(newDepartments, Department::getPid);
@@ -222,13 +260,10 @@ private List<Department> createNewDepartmentsList(Company company) {
222260
departments.add(new Department(company.getPid(), "New department 2"));
223261

224262
// update one item
225-
Optional<Department> itDepartment = departments.stream()
263+
departments.stream()
226264
.filter(it -> it.getName().equals("IT Department"))
227-
.findFirst();
228-
229-
if (itDepartment.isPresent()) {
230-
itDepartment.get().setName("IT Department Updated");
231-
}
265+
.findFirst()
266+
.ifPresent(it -> it.setName("IT Department Updated"));
232267

233268
return departments;
234269
}
@@ -244,6 +279,13 @@ private List<Department> createNewDepartmentsList(Company company) {
244279
public void executeComplexSelectScenario() {
245280
List<ProjectsWithCostsGreaterThanOutput> projectsWithCostsGreaterThan = repository.getProjectsWithCostsGreaterThan(70000);
246281

282+
// check some post conditions
283+
assert projectsWithCostsGreaterThan != null;
284+
assert projectsWithCostsGreaterThan.size() == 2;
285+
assert projectsWithCostsGreaterThan.get(0).getCompanyName().equals("CleverGang");
286+
assert projectsWithCostsGreaterThan.get(0).getCompanyCost().equals(new BigDecimal("72000.00"));
287+
assert projectsWithCostsGreaterThan.get(1).getCompanyName().equals("Supersoft");
288+
assert projectsWithCostsGreaterThan.get(1).getCompanyCost().equals(new BigDecimal("13000.00"));
247289
logger.info("executeComplexSelectScenario output: {}", projectsWithCostsGreaterThan);
248290
}
249291

@@ -253,6 +295,14 @@ public void executeComplexSelectScenario() {
253295
public void callStoredProcedureScenario() {
254296
RegisterEmployeeOutput output = repository.callRegisterEmployee("Bretislav", "Wajtr", "bretislav.wajtr@test.com", new BigDecimal(40000), "MyDepartment", "MyCompany");
255297

298+
// check some post conditions
299+
assert output != null;
300+
assert output.getEmployeePid() != null;
301+
assert output.getEmployeePid() > 10;
302+
assert output.getDepartmentPid() != null;
303+
assert output.getDepartmentPid() > 7;
304+
assert output.getCompanyPid() != null;
305+
assert output.getCompanyPid() > 3;
256306
logger.info("callStoredProcedureScenario output: {}", output);
257307
}
258308

@@ -272,7 +322,10 @@ public void callStoredProcedureScenario() {
272322
public void executeSimpleStaticStatementScenario() {
273323
Company output = repository.findCompanyUsingSimpleStaticStatement(1);
274324

275-
logger.info("Output: {}", output);
325+
// check some post conditions
326+
assert output != null;
327+
assert output.getName().equals("CleverGang");
328+
logger.info("Output of scenario 11: {}", output);
276329
}
277330
}
278331

0 commit comments

Comments
 (0)