Trong bài blog này, chúng ta sẽ cùng nhau khám phá Spring JDBC, bắt đầu từ một ví dụ ứng dụng JDBC đơn giản, sau đó sẽ tìm hiểu cách JdbcTemplate
giúp chúng ta loại bỏ phần boiler-plate code (mã lặp khuôn mẫu) trong logic thao tác cơ sở dữ liệu, chẳng hạn như việc mở/đóng Connection, ResultSet, PreparedStatement, v.v.
Chúng ta sẽ sử dụng Spring Tool Suite (STS) để phát triển ứng dụng này, vì STS rất hữu ích cho các ứng dụng dựa trên Spring. Cấu trúc project cuối cùng của chúng ta sẽ trông như hình minh họa bên dưới.
Spring JDBC là gì?
Spring JDBC cung cấp một lớp trừu tượng (abstraction layer) trên JDBC API, giúp đơn giản hóa việc tương tác database bằng cách xử lý tự động các tác vụ như quản lý kết nối, xử lý lỗi và đóng tài nguyên. Trái tim của Spring JDBC là lớp JdbcTemplate
.
Các Dependency cần thiết cho Spring JDBC
Đầu tiên và quan trọng nhất, chúng ta cần bao gồm các thư viện Spring JDBC và driver của database vào file pom.xml
của Maven project. File pom.xml
của tôi sẽ trông như thế này:
<project xmlns="<https://maven.apache.org/POM/4.0.0>" xmlns:xsi="<https://www.w3.org/2001/XMLSchema-instance>"
xsi:schemaLocation="<https://maven.apache.org/POM/4.0.0> <https://www.maven.apache.org/xsd/maven-4.0.0.xsd>">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.samples</groupId>
<artifactId>SpringJDBCExample</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<!-- Generic properties -->
<java.version>1.6</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- Spring -->
<spring-framework.version>4.0.2.RELEASE</spring-framework.version>
<!-- Logging -->
<logback.version>1.0.13</logback.version>
<slf4j.version>1.7.5</slf4j.version>
</properties>
<dependencies>
<!-- Spring and Transactions -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<!-- Spring JDBC Support -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.0.5</version>
</dependency>
<!-- Logging with SLF4J & LogBack -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
Phần lớn file pom.xml
được STS tự động tạo ra. Tôi đã cập nhật phiên bản Spring Framework lên 4.0.2.RELEASE
(phiên bản ví dụ trong bài viết này). Quan trọng là chúng ta đã thêm các artifact
cần thiết: spring-jdbc
và mysql-connector-java
. spring-jdbc
chứa các lớp hỗ trợ Spring JDBC, và mysql-connector-java
là driver database. Tôi đang sử dụng MySQL cho mục đích kiểm thử, vì vậy tôi đã thêm MySQL JConnector
. Nếu bạn sử dụng một hệ quản trị cơ sở dữ liệu (RDBMS) khác, bạn nên thay đổi dependency
tương ứng.
Cấu hình Database cho Spring JDBC
Hãy tạo một bảng đơn giản mà chúng ta sẽ sử dụng trong ứng dụng cho các thao tác CRUD (Create, Read, Update, Delete):
CREATE TABLE `Employee` (
`id` int(11) unsigned NOT NULL,
`name` varchar(20) DEFAULT NULL,
`role` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Lớp Model cho Spring JDBC
Chúng ta sẽ sử dụng kiến trúc DAO Pattern (Data Access Object) cho các thao tác JDBC. Vì vậy, hãy tạo một Java Bean sẽ mô hình hóa bảng Employee
của chúng ta:
package com.journaldev.spring.jdbc.model;
public class Employee {
private int id;
private String name;
private String role;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
@Override
public String toString(){
return "{ID="+id+",Name="+name+",Role="+role+"}";
}
}
DAO Interface và Implementation (Sử dụng JDBC truyền thống)
Đối với DAO Pattern, trước tiên chúng ta sẽ có một interface
khai báo tất cả các hoạt động mà chúng ta muốn triển khai:
package com.journaldev.spring.jdbc.dao;
import java.util.List;
import com.journaldev.spring.jdbc.model.Employee;
//CRUD operations
public interface EmployeeDAO {
//Create
public void save(Employee employee);
//Read
public Employee getById(int id);
//Update
public void update(Employee employee);
//Delete
public void deleteById(int id);
//Get All
public List<Employee> getAll();
}
Và đây là phần triển khai sử dụng JDBC truyền thống:
package com.journaldev.spring.jdbc.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import javax.sql.DataSource;
import com.journaldev.spring.jdbc.model.Employee;
public class EmployeeDAOImpl implements EmployeeDAO {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public void save(Employee employee) {
String query = "insert into Employee (id, name, role) values (?,?,?)";
Connection con = null;
PreparedStatement ps = null;
try{
con = dataSource.getConnection();
ps = con.prepareStatement(query);
ps.setInt(1, employee.getId());
ps.setString(2, employee.getName());
ps.setString(3, employee.getRole());
int out = ps.executeUpdate();
if(out !=0){
System.out.println("Employee saved with id="+employee.getId());
}else System.out.println("Employee save failed with id="+employee.getId());
}catch(SQLException e){
e.printStackTrace();
}finally{
try {
ps.close();
con.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
@Override
public Employee getById(int id) {
String query = "select name, role from Employee where id = ?";
Employee emp = null;
Connection con = null;
PreparedStatement ps = null;
ResultSet rs = null;
try{
con = dataSource.getConnection();
ps = con.prepareStatement(query);
ps.setInt(1, id);
rs = ps.executeQuery();
if(rs.next()){
emp = new Employee();
emp.setId(id);
emp.setName(rs.getString("name"));
emp.setRole(rs.getString("role"));
System.out.println("Employee Found::"+emp);
}else{
System.out.println("No Employee found with id="+id);
}
}catch(SQLException e){
e.printStackTrace();
}finally{
try {
rs.close();
ps.close();
con.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
return emp;
}
@Override
public void update(Employee employee) {
String query = "update Employee set name=?, role=? where id=?";
Connection con = null;
PreparedStatement ps = null;
try{
con = dataSource.getConnection();
ps = con.prepareStatement(query);
ps.setString(1, employee.getName());
ps.setString(2, employee.getRole());
ps.setInt(3, employee.getId());
int out = ps.executeUpdate();
if(out !=0){
System.out.println("Employee updated with id="+employee.getId());
}else System.out.println("No Employee found with id="+employee.getId());
}catch(SQLException e){
e.printStackTrace();
}finally{
try {
ps.close();
con.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
@Override
public void deleteById(int id) {
String query = "delete from Employee where id=?";
Connection con = null;
PreparedStatement ps = null;
try{
con = dataSource.getConnection();
ps = con.prepareStatement(query);
ps.setInt(1, id);
int out = ps.executeUpdate();
if(out !=0){
System.out.println("Employee deleted with id="+id);
}else System.out.println("No Employee found with id="+id);
}catch(SQLException e){
e.printStackTrace();
}finally{
try {
ps.close();
con.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
@Override
public List<Employee> getAll() {
String query = "select id, name, role from Employee";
List<Employee> empList = new ArrayList<Employee>();
Connection con = null;
PreparedStatement ps = null;
ResultSet rs = null;
try{
con = dataSource.getConnection();
ps = con.prepareStatement(query);
rs = ps.executeQuery();
while(rs.next()){
Employee emp = new Employee();
emp.setId(rs.getInt("id"));
emp.setName(rs.getString("name"));
emp.setRole(rs.getString("role"));
empList.add(emp);
}
}catch(SQLException e){
e.printStackTrace();
}finally{
try {
rs.close();
ps.close();
con.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
return empList;
}
}
Các thao tác CRUD ở trên khá dễ hiểu. Điểm đáng chú ý là sự lặp lại của các khối try-catch-finally
để đảm bảo Connection
, PreparedStatement
và ResultSet
luôn được đóng. Việc này rất dễ gây lỗi nếu lập trình viên sơ ý, dẫn đến rò rỉ tài nguyên.
Cấu hình Spring Bean
Nếu bạn nhìn vào tất cả các lớp trên, chúng đều sử dụng JDBC API tiêu chuẩn và không có bất kỳ tham chiếu nào đến Spring JDBC framework. Các lớp của Spring JDBC framework bắt đầu có vai trò khi chúng ta tạo file cấu hình Spring Bean và định nghĩa các bean. Chúng ta sẽ tạo DataSource
trong file ngữ cảnh Spring Bean và thiết lập nó cho lớp triển khai DAO của chúng ta. File cấu hình Spring Bean của tôi trông như sau:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="<https://www.springframework.org/schema/beans>"
xmlns:xsi="<https://www.w3.org/2001/XMLSchema-instance>"
xsi:schemaLocation="<https://www.springframework.org/schema/beans> <https://www.springframework.org/schema/beans/spring-beans.xsd>">
<bean id="employeeDAO" class="com.journaldev.spring.jdbc.dao.EmployeeDAOImpl">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/TestDB" />
<property name="username" value="pankaj" />
<property name="password" value="pankaj123" />
</bean>
</beans>
Đầu tiên, chúng ta tạo một đối tượng DataSource
của lớp DriverManagerDataSource
. Lớp này cung cấp một triển khai cơ bản của DataSource
mà chúng ta có thể sử dụng. Chúng ta truyền URL database MySQL, username và password làm thuộc tính cho bean DataSource
. Sau đó, bean dataSource
được gán cho bean EmployeeDAOImpl
, và chúng ta đã sẵn sàng với triển khai Spring JDBC của mình. Việc triển khai này có tính khớp nối lỏng (loosely coupled) – nếu chúng ta muốn chuyển sang một triển khai khác hoặc di chuyển sang một server database khác, tất cả những gì chúng ta cần làm là thực hiện các thay đổi tương ứng trong cấu hình bean. Đây là một trong những lợi thế lớn mà Spring JDBC framework mang lại.
Lớp kiểm thử Spring JDBC
Hãy viết một lớp kiểm thử đơn giản để đảm bảo mọi thứ hoạt động tốt.
package com.journaldev.spring.jdbc.main;
import java.util.List;
import java.util.Random;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.journaldev.spring.jdbc.dao.EmployeeDAO;
import com.journaldev.spring.jdbc.model.Employee;
public class SpringMain {
public static void main(String[] args) {
//Get the Spring Context
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("spring.xml");
//Get the EmployeeDAO Bean
EmployeeDAO employeeDAO = ctx.getBean("employeeDAO", EmployeeDAO.class);
//Run some tests for JDBC CRUD operations
Employee emp = new Employee();
int rand = new Random().nextInt(1000);
emp.setId(rand);
emp.setName("Pankaj");
emp.setRole("Java Developer");
//Create
employeeDAO.save(emp);
//Read
Employee emp1 = employeeDAO.getById(rand);
System.out.println("Employee Retrieved::"+emp1);
//Update
emp.setRole("CEO");
employeeDAO.update(emp);
//Get All
List<Employee> empList = employeeDAO.getAll();
System.out.println(empList);
//Delete
employeeDAO.deleteById(rand);
//Close Spring Context
ctx.close();
System.out.println("DONE");
}
}
Tôi đang sử dụng lớp Random
để tạo số ngẫu nhiên cho id
của nhân viên. Khi chúng ta chạy chương trình trên, chúng ta sẽ nhận được output tương tự như sau:
Mar 25, 2014 12:54:18 PM org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@4b9af9a9: startup date [Tue Mar 25 12:54:18 PDT 2014]; root of context hierarchy
Mar 25, 2014 12:54:18 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [spring.xml]
Mar 25, 2014 12:54:19 PM org.springframework.jdbc.datasource.DriverManagerDataSource setDriverClassName
INFO: Loaded JDBC driver: com.mysql.jdbc.Driver
Employee saved with id=726
Employee Found::{ID=726,Name=Pankaj,Role=Java Developer}
Employee Retrieved::{ID=726,Name=Pankaj,Role=Java Developer}
Employee updated with id=726
[{ID=726,Name=Pankaj,Role=CEO}]
Employee deleted with id=726
Mar 25, 2014 12:54:19 PM org.springframework.context.support.ClassPathXmlApplicationContext doClose
INFO: Closing org.springframework.context.support.ClassPathXmlApplicationContext@4b9af9a9: startup date [Tue Mar 25 12:54:18 PDT 2014]; root of context hierarchy
DONE
Ví dụ Spring JdbcTemplate
Nếu bạn nhìn vào lớp triển khai DAO EmployeeDAOImpl
ở trên, có rất nhiều mã rườm rà nơi chúng ta phải tự mở và đóng Connection
, PreparedStatement
và ResultSet
. Điều này có thể dẫn đến rò rỉ tài nguyên nếu ai đó quên đóng tài nguyên đúng cách. Chúng ta có thể sử dụng lớp org.springframework.jdbc.core.JdbcTemplate
để tránh những lỗi này.
Spring JdbcTemplate
là lớp trung tâm trong gói Spring JDBC core và cung cấp rất nhiều phương thức để thực thi các truy vấn và tự động phân tích ResultSet
để lấy đối tượng hoặc danh sách các đối tượng. Tất cả những gì chúng ta cần làm là cung cấp các đối số dưới dạng mảng Object
và triển khai các Callback interface
như PreparedStatementSetter
và RowMapper
để ánh xạ các đối số hoặc chuyển đổi dữ liệu ResultSet
thành các đối tượng bean.
Hãy cùng xem một triển khai khác của EmployeeDAO
mà chúng ta sẽ sử dụng lớp Spring JdbcTemplate
để thực thi các loại truy vấn khác nhau:
package com.journaldev.spring.jdbc.dao;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import com.journaldev.spring.jdbc.model.Employee;
public class EmployeeDAOJDBCTemplateImpl implements EmployeeDAO {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public void save(Employee employee) {
String query = "insert into Employee (id, name, role) values (?,?,?)";
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
Object[] args = new Object[] {employee.getId(), employee.getName(), employee.getRole()};
int out = jdbcTemplate.update(query, args);
if(out !=0){
System.out.println("Employee saved with id="+employee.getId());
}else System.out.println("Employee save failed with id="+employee.getId());
}
@Override
public Employee getById(int id) {
String query = "select id, name, role from Employee where id = ?";
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
//using RowMapper anonymous class, we can create a separate RowMapper for reuse
Employee emp = jdbcTemplate.queryForObject(query, new Object[]{id}, new RowMapper<Employee>(){
@Override
public Employee mapRow(ResultSet rs, int rowNum)
throws SQLException {
Employee emp = new Employee();
emp.setId(rs.getInt("id"));
emp.setName(rs.getString("name"));
emp.setRole(rs.getString("role"));
return emp;
}});
return emp;
}
@Override
public void update(Employee employee) {
String query = "update Employee set name=?, role=? where id=?";
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
Object[] args = new Object[] {employee.getName(), employee.getRole(), employee.getId()};
int out = jdbcTemplate.update(query, args);
if(out !=0){
System.out.println("Employee updated with id="+employee.getId());
}else System.out.println("No Employee found with id="+employee.getId());
}
@Override
public void deleteById(int id) {
String query = "delete from Employee where id=?";
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
int out = jdbcTemplate.update(query, id);
if(out !=0){
System.out.println("Employee deleted with id="+id);
}else System.out.println("No Employee found with id="+id);
}
@Override
public List<Employee> getAll() {
String query = "select id, name, role from Employee";
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
List<Employee> empList = new ArrayList<Employee>();
List<Map<String,Object>> empRows = jdbcTemplate.queryForList(query);
for(Map<String,Object> empRow : empRows){
Employee emp = new Employee();
emp.setId(Integer.parseInt(String.valueOf(empRow.get("id"))));
emp.setName(String.valueOf(empRow.get("name")));
emp.setRole(String.valueOf(empRow.get("role")));
empList.add(emp);
}
return empList;
}
}
Các điểm quan trọng cần lưu ý trong đoạn mã trên khi sử dụng Spring JdbcTemplate
là:
- Sử dụng mảng
Object
để truyền các đối số choPreparedStatement
. Mặc dù chúng ta cũng có thể sử dụng triển khaiPreparedStatementSetter
, nhưng việc truyền mảngObject
có vẻ dễ sử dụng hơn. - Không có đoạn mã nào liên quan đến việc mở và đóng
Connection
,Statement
hoặcResultSet
. Tất cả những điều đó được lớp SpringJdbcTemplate
xử lý nội bộ. - Triển khai lớp ẩn danh
RowMapper
để ánh xạ dữ liệuResultSet
vào đối tượng beanEmployee
trong phương thứcqueryForObject()
. Bạn cũng có thể tạo một lớpRowMapper
riêng biệt để tái sử dụng. - Phương thức
queryForList()
trả về danh sách cácMap
, trong đóMap
chứa dữ liệu hàng được ánh xạ với khóa là tên cột và giá trị từ hàng database khớp với tiêu chí.
Để sử dụng triển khai Spring JdbcTemplate
, tất cả những gì chúng ta cần làm là thay đổi lớp employeeDAO
trong file cấu hình Spring Bean như sau:
<bean id="employeeDAO" class="com.journaldev.spring.jdbc.dao.EmployeeDAOJDBCTemplateImpl">
<property name="dataSource" ref="dataSource" />
</bean>
Khi bạn chạy lớp main
, đầu ra của triển khai Spring JdbcTemplate
sẽ tương tự như đầu ra đã thấy ở trên với triển khai JDBC thông thường.
Hy vọng qua những đoạn code và giải thích trên, bạn đã hình dung rõ hơn về cách Spring JDBC, đặc biệt là JdbcTemplate
, đơn giản hóa việc tương tác với database. Giờ thì, hãy cùng đúc kết lại những gì chúng ta đã học nhé.
Kết luận
Như vậy, chúng ta đã cùng nhau khám phá Spring JDBC, từ những vấn đề cố hữu khi sử dụng JDBC truyền thống đến giải pháp mạnh mẽ mà Spring Framework cung cấp thông qua JdbcTemplate
.
Việc nắm vững Spring JDBC và JdbcTemplate
là một kỹ năng nền tảng quan trọng cho bất kỳ nhà phát triển Java nào làm việc với các ứng dụng Spring cần tương tác với database. Kiến thức này không chỉ giúp bạn viết code hiệu quả hơn mà còn là bước đệm vững chắc để bạn khám phá các công nghệ quản lý dữ liệu cao cấp hơn trong Spring như Spring Data JPA hoặc các ORM framework như Hibernate.