Skip to content

Commit b926a82

Browse files
author
bretislav.wajtr
committed
#2 JDBI implementation
1 parent 80d58ed commit b926a82

7 files changed

Lines changed: 413 additions & 5 deletions

File tree

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ With that conditions in respect, following frameworks were compared:
2424
* **jOOQ** (see [implementation](src/main/java/com/clevergang/dbtests/repository/impl/jooq/JooqDataRepositoryImpl.java))
2525
* **MyBatis** (see [implementation](src/main/java/com/clevergang/dbtests/repository/impl/mybatis/MyBatisDataRepositoryImpl.java) and [mapper](src/main/resources/mybatis/mappers/DataRepositoryMapper.xml))
2626
* **EBean** (see [implementation](src/main/java/com/clevergang/dbtests/repository/impl/ebean/EBeanDataRepositoryImpl.java))
27+
* **JDBI (version 2.77)** (see [implementation](src/main/java/com/clevergang/dbtests/repository/impl/jdbi/JDBIDataRepositoryImpl.java))
2728

2829
I tried to find optimal (== most readable) implementation in every framework, but comments are welcomed! There are lot of comments in the code explaining why I chose such implementation and some FIXMEs on places which I do not like, but which cannot be implemented differently or which I have troubles to improve...
2930

@@ -77,7 +78,7 @@ Please note that following remarks are very subjective and does not have to nece
7778
#### What would I choose
7879

7980
1. If project manager is ok with additional cost of a licence or the project uses one of open source databases (like PostgreSQL) then definitely go with **jOOQ**.
80-
2. If project uses Oracle, DB2, MSSQL or any other commercial database and additional cost for jOOQ licence is not acceptable, then go with **JDBCTemplate**
81+
2. If project uses Oracle, DB2, MSSQL or any other commercial database and additional cost for jOOQ licence is not acceptable, then go with **JDBCTemplate** (for me it wins over other choices for it's maturity and documentation).
8182

8283
#### Subjective pros/cons of each framework
8384

@@ -121,4 +122,13 @@ Please note that following remarks are very subjective and does not have to nece
121122
* Online documentation is quite weak (as of December 1, 2016). Lot of things are hidden in videos and you have to google for details or get into JavaDocs... However, JavaDoc is very good and I generally didn't have a problem to find what I needed in JavaDoc. Also the API is quite understandable... to sum it up, that weak online documentation is not such a big deal.
122123
* Logging could be better
123124
* Allows JPA OneToMany and ManyToOne relations modeling and possibility to "lazy fetch" these relations - actually I do not like this concept at all as it can lead to potentially very ineffective code. Per documentation and experiences of several people on internet EBean behaves better than full blown JPA implementation in this manner, but you can still be hit by the N+1 problem and all the performance traps, which lazy fetching brings...
124-
125+
126+
**JDBI (version 2.77)**
127+
* Pros
128+
* I like the fluent style of creating statements and binding parameters - I'd like to see something like that in JDBC Template
129+
* Code is generally more readable than jdbc template
130+
* Quite easy and understandable batch operations
131+
* Cons
132+
* Extremely weak logging :(
133+
* Very weak documentation (as of 5.12.2016, version 2.77)
134+
* I don't quite like the necessity to open&close handle for each DAO method -> it's little bit unclear for me if the handle should be opened for each method or if it's ok to open one handle per HTTP request... documentation is not much clear about this...

build.gradle

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ dependencies {
3737

3838
// MyBatis dependencies
3939
'org.mybatis.spring.boot:mybatis-spring-boot-starter:1.1.1',
40-
'com.github.javaplugs:mybatis-types:0.3' // required so mybatis works with java8 datetime classes (LocalDate)
40+
'com.github.javaplugs:mybatis-types:0.3', // required so mybatis works with java8 datetime classes (LocalDate)
41+
42+
// jdbi dependencies
43+
'org.jdbi:jdbi:2.77'
4144
)
4245

4346
testCompile(

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@
1212
import org.jooq.conf.StatementType;
1313
import org.mybatis.spring.SqlSessionFactoryBean;
1414
import org.mybatis.spring.SqlSessionTemplate;
15+
import org.skife.jdbi.v2.DBI;
16+
import org.skife.jdbi.v2.logging.SLF4JLog;
1517
import org.springframework.beans.factory.annotation.Qualifier;
1618
import org.springframework.boot.SpringApplication;
1719
import org.springframework.boot.autoconfigure.SpringBootApplication;
1820
import org.springframework.context.annotation.Bean;
1921
import org.springframework.context.annotation.Configuration;
2022
import org.springframework.context.annotation.Primary;
2123
import org.springframework.core.io.ClassPathResource;
24+
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
2225

2326
import javax.sql.DataSource;
2427

@@ -39,6 +42,7 @@ public Settings jooqSettings() {
3942
Settings ret = new Settings();
4043
ret.withRenderSchema(false);
4144
ret.setRenderFormatted(true);
45+
4246
ret.setRenderNameStyle(RenderNameStyle.AS_IS);
4347
return ret;
4448
}
@@ -106,6 +110,21 @@ public EbeanServer ebeanServer(ServerConfig serverConfig) {
106110
return EbeanServerFactory.create(serverConfig);
107111
}
108112

113+
/*
114+
* JDBI Configurations
115+
*/
116+
117+
@SuppressWarnings("SpringJavaAutowiringInspection")
118+
@Bean
119+
public DBI jdbiFactory(DataSource dataSource) {
120+
// note that for JDBI we have to wrap datasource with TransactionAwareDataSourceProxy otherwise JDBI won't respect
121+
// transactions created by spring
122+
TransactionAwareDataSourceProxy transactionAwareDataSourceProxy = new TransactionAwareDataSourceProxy(dataSource);
123+
DBI dbi = new DBI(transactionAwareDataSourceProxy);
124+
dbi.setSQLLog(new SLF4JLog()); // to enable SLF4J logging
125+
return dbi;
126+
}
127+
109128
public static void main(String[] args) {
110129
SpringApplication.run(DbTestsApplication.class, args);
111130
}

src/main/java/com/clevergang/dbtests/repository/api/DataRepository.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public interface DataRepository {
4141
/**
4242
* Should return full records of all Departments assigned to the Company (passed in as parameter).
4343
* @param company Full Company record
44-
* @return List of Departments
44+
* @return List of Departments, ordered by PID ascending
4545
*/
4646
List<Department> findDepartmentsOfCompany(Company company);
4747

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
package com.clevergang.dbtests.repository.impl.jdbi;
2+
3+
import com.clevergang.dbtests.repository.api.DataRepository;
4+
import com.clevergang.dbtests.repository.api.data.*;
5+
import org.skife.jdbi.v2.DBI;
6+
import org.skife.jdbi.v2.Handle;
7+
import org.skife.jdbi.v2.PreparedBatch;
8+
import org.skife.jdbi.v2.util.IntegerColumnMapper;
9+
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.stereotype.Repository;
11+
import org.springframework.util.Assert;
12+
13+
import java.math.BigDecimal;
14+
import java.sql.ResultSet;
15+
import java.sql.SQLException;
16+
import java.sql.Statement;
17+
import java.util.List;
18+
19+
/**
20+
* JDBI implementation of the DataRepository interface
21+
*
22+
* @author Bretislav Wajtr
23+
*/
24+
@Repository
25+
public class JDBIDataRepositoryImpl implements DataRepository {
26+
27+
private DBI dbi;
28+
29+
@Autowired
30+
public JDBIDataRepositoryImpl(DBI dbi) {
31+
this.dbi = dbi;
32+
}
33+
34+
@Override
35+
public Company findCompany(Integer pid) {
36+
try (Handle h = dbi.open()) {
37+
return h.createQuery("SELECT * FROM company WHERE pid = :pid")
38+
.bind("pid", pid)
39+
.map(Company.class)
40+
.first();
41+
}
42+
}
43+
44+
@Override
45+
public Company findCompanyUsingSimpleStaticStatement(Integer pid) {
46+
// I didn't find a way how to setup JDBI so the query is executed statically (using JDBC regular Statement
47+
// not PreparedStatement). However, JDBI is polite and provides way how to access the underlying JDBC connection
48+
// -> therefore we can actually fall back to JDBC way of doing things (which is not such a big deal since we need simple
49+
// Statements only rarely):
50+
try (Handle h = dbi.open()) {
51+
String query = "SELECT pid, address, name " +
52+
"FROM company " +
53+
"WHERE pid = " + pid;
54+
55+
Statement statement = h.getConnection().createStatement();
56+
ResultSet resultSet = statement.executeQuery(query);
57+
Company result = null;
58+
if (resultSet.next()) {
59+
result = new Company();
60+
result.setPid(resultSet.getInt("pid"));
61+
result.setAddress(resultSet.getString("address"));
62+
result.setName(resultSet.getString("name"));
63+
64+
}
65+
return result;
66+
} catch (SQLException e) {
67+
// just wrap checked exceptions as e
68+
throw new RuntimeException(e);
69+
}
70+
}
71+
72+
@Override
73+
public Department findDepartment(Integer pid) {
74+
try (Handle h = dbi.open()) {
75+
return h.createQuery("SELECT pid, name, company_pid companyPid FROM department WHERE pid = :pid")
76+
.bind("pid", pid)
77+
.map(Department.class)
78+
.first();
79+
}
80+
}
81+
82+
@Override
83+
public List<Department> findDepartmentsOfCompany(Company company) {
84+
Assert.notNull(company);
85+
Assert.notNull(company.getPid());
86+
87+
try (Handle h = dbi.open()) {
88+
return h.createQuery("SELECT pid, name, company_pid companyPid " +
89+
"FROM department " +
90+
"WHERE company_pid = :company_pid " +
91+
"ORDER BY pid ")
92+
.bind("company_pid", company.getPid())
93+
.map(Department.class)
94+
.list();
95+
}
96+
}
97+
98+
@Override
99+
public void deleteDepartments(List<Department> departmentsToDelete) {
100+
try (Handle h = dbi.open()) {
101+
PreparedBatch preparedBatch = h.prepareBatch("DELETE FROM department WHERE pid = :pid");
102+
103+
for (Department department : departmentsToDelete) {
104+
preparedBatch.bind("pid", department.getPid()).add();
105+
}
106+
107+
preparedBatch.execute();
108+
}
109+
}
110+
111+
@Override
112+
public void updateDepartments(List<Department> departmentsToUpdate) {
113+
try (Handle h = dbi.open()) {
114+
PreparedBatch preparedBatch = h.prepareBatch("UPDATE department SET company_pid = :company_pid, name = :name WHERE pid = :pid");
115+
for (Department department : departmentsToUpdate) {
116+
preparedBatch
117+
.bind("company_pid", department.getCompanyPid())
118+
.bind("name", department.getName())
119+
.bind("pid", department.getPid())
120+
.add();
121+
}
122+
preparedBatch.execute();
123+
}
124+
}
125+
126+
@Override
127+
public void insertDepartments(List<Department> departmentsToInsert) {
128+
try (Handle h = dbi.open()) {
129+
PreparedBatch preparedBatch = h.prepareBatch("INSERT INTO department (company_pid, name) VALUES (:company_pid, :name)");
130+
for (Department department : departmentsToInsert) {
131+
preparedBatch
132+
.bind("company_pid", department.getCompanyPid())
133+
.bind("name", department.getName())
134+
.add();
135+
}
136+
preparedBatch.execute();
137+
}
138+
}
139+
140+
@Override
141+
public Project findProject(Integer pid) {
142+
try (Handle h = dbi.open()) {
143+
return h.createQuery("SELECT pid, name, datestarted FROM project WHERE pid = :pid")
144+
.bind("pid", pid)
145+
.map((index, r, ctx) -> {
146+
Project project = new Project();
147+
project.setPid(r.getInt("pid"));
148+
project.setName(r.getString("name"));
149+
project.setDate(r.getDate("datestarted").toLocalDate());
150+
return project;
151+
})
152+
.first();
153+
}
154+
}
155+
156+
@Override
157+
public Integer insertProject(Project project) {
158+
try (Handle h = dbi.open()) {
159+
return h.createStatement("INSERT INTO project (name, datestarted) VALUES (:name, :datestarted)")
160+
.bind("name", project.getName())
161+
.bind("datestarted", project.getDate())
162+
.executeAndReturnGeneratedKeys(IntegerColumnMapper.WRAPPER)
163+
.first();
164+
}
165+
}
166+
167+
@Override
168+
public List<Integer> insertProjects(List<Project> projects) {
169+
try (Handle h = dbi.open()) {
170+
PreparedBatch preparedBatch = h.prepareBatch("INSERT INTO project (name, datestarted) VALUES (:name, :datestarted)");
171+
for (Project project : projects) {
172+
preparedBatch
173+
.bind("name", project.getName())
174+
.bind("datestarted", project.getDate())
175+
.add();
176+
}
177+
return preparedBatch.executeAndGenerateKeys(IntegerColumnMapper.WRAPPER).list();
178+
}
179+
}
180+
181+
@Override
182+
public List<ProjectsWithCostsGreaterThanOutput> getProjectsWithCostsGreaterThan(int totalCostBoundary) {
183+
try (Handle h = dbi.open()) {
184+
String query;
185+
query = "WITH project_info AS (\n" +
186+
" SELECT project.pid project_pid, project.name project_name, salary monthly_cost, company.name company_name\n" +
187+
" FROM project\n" +
188+
" JOIN projectemployee ON project.pid = projectemployee.project_pid\n" +
189+
" JOIN employee ON projectemployee.employee_pid = employee.pid\n" +
190+
" LEFT JOIN department ON employee.department_pid = department.pid\n" +
191+
" LEFT JOIN company ON department.company_pid = company.pid\n" +
192+
"),\n" +
193+
"project_cost AS (\n" +
194+
" SELECT project_pid, sum(monthly_cost) total_cost\n" +
195+
" FROM project_info GROUP BY project_pid\n" +
196+
")\n" +
197+
"SELECT project_name projectName, total_cost totalCost, company_name companyName, sum(monthly_cost) companyCost FROM project_info\n" +
198+
" JOIN project_cost USING (project_pid)\n" +
199+
"WHERE total_cost > :totalCostBoundary\n" +
200+
"GROUP BY project_name, total_cost, company_name\n" +
201+
"ORDER BY company_name";
202+
203+
return h.createQuery(query)
204+
.bind("totalCostBoundary", totalCostBoundary)
205+
.map(ProjectsWithCostsGreaterThanOutput.class)
206+
.list();
207+
}
208+
}
209+
210+
@Override
211+
public Employee findEmployee(Integer pid) {
212+
try (Handle h = dbi.open()) {
213+
return h.createQuery("SELECT pid, name, surname, email, department_pid departmentPid, salary FROM employee WHERE pid = :pid")
214+
.bind("pid", pid)
215+
.map(Employee.class)
216+
.first();
217+
}
218+
}
219+
220+
@Override
221+
public List<Employee> employeesWithSalaryGreaterThan(Integer minSalary) {
222+
try (Handle h = dbi.open()) {
223+
return h.createQuery("SELECT * FROM employee WHERE salary > :salary")
224+
.bind("salary", minSalary)
225+
.map(Employee.class)
226+
.list();
227+
}
228+
}
229+
230+
@Override
231+
public void updateEmployee(Employee employeeToUpdate) {
232+
Assert.notNull(employeeToUpdate);
233+
Assert.notNull(employeeToUpdate.getPid());
234+
235+
try (Handle h = dbi.open()) {
236+
h.createStatement(" UPDATE EMPLOYEE SET " +
237+
" department_pid = :department_pid, " +
238+
" name = :name," +
239+
" surname = :surname," +
240+
" email = :email," +
241+
" salary = :salary" +
242+
" WHERE pid = :pid")
243+
.bind("department_pid", employeeToUpdate.getDepartmentPid())
244+
.bind("name", employeeToUpdate.getName())
245+
.bind("surname", employeeToUpdate.getSurname())
246+
.bind("email", employeeToUpdate.getEmail())
247+
.bind("salary", employeeToUpdate.getSalary())
248+
.bind("pid", employeeToUpdate.getPid())
249+
.execute();
250+
}
251+
}
252+
253+
@Override
254+
public RegisterEmployeeOutput callRegisterEmployee(String name, String surname, String email, BigDecimal salary, String departmentName, String companyName) {
255+
try (Handle h = dbi.open()) {
256+
return h.createQuery("SELECT employee_id employeePid, department_id departmentPid, company_id companyPid FROM register_employee(" +
257+
" :name, \n" +
258+
" :surname, \n" +
259+
" :email, \n" +
260+
" :salary, \n" +
261+
" :departmentName, \n" +
262+
" :companyName\n" +
263+
")")
264+
.bind("name", name)
265+
.bind("surname", surname)
266+
.bind("email", email)
267+
.bind("salary", salary)
268+
.bind("departmentName", departmentName)
269+
.bind("companyName", companyName)
270+
.map(RegisterEmployeeOutput.class)
271+
.first();
272+
}
273+
}
274+
275+
@Override
276+
public Integer getProjectsCount() {
277+
try (Handle h = dbi.open()) {
278+
return h.createQuery("SELECT count(*) FROM project")
279+
.map(IntegerColumnMapper.WRAPPER)
280+
.first();
281+
}
282+
283+
}
284+
}

src/main/resources/application.properties

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,7 @@ logging.level.org.avaje.ebean.SUM=TRACE
2727
logging.level.org.avaje.ebean.QUERY=TRACE
2828
logging.level.org.avaje.ebean.BEAN=TRACE
2929
logging.level.org.avaje.ebean.COLL=TRACE
30-
logging.level.org.avaje.ebean.NATKEY=TRACE
30+
logging.level.org.avaje.ebean.NATKEY=TRACE
31+
32+
# logging settings for jdbi
33+
logging.level.org.skife.jdbi.v2=TRACE

0 commit comments

Comments
 (0)