Sau khi đã tạo được một trang web đơn giản rồi thì giờ là lúc chúng ta cần tìm hiểu đến spring jdbc để kết nối cơ sở dữ liệu. Trong tương lai chúng ta sẽ kết hợp jdbc và webmvc để tạo ra một website hoàn chỉnh. Trong phần này Dũng sẽ cùng các bạn khởi tạo và sử dụng một số chức năng của Spring jdbc nhé.

Khởi tạo module

Chúng ta sẽ cần khởi tạo một module có tên jdbc.

Chuẩn bị

Chúng ta sẽ cần cài đặt mysql server trên máy tính của mình, bạn có thể tải và cài đặt mysql server phiên bản cộng đồng tại đây: https://dev.mysql.com/downloads/mysql/.
Sau đó chúng ta cũng cần tạo một cơ sở dữ liệu có tên mastering_spring_boot với script như sau:

CREATE SCHEMA `mastering_spring_boot` DEFAULT CHARACTER SET utf8 COLLATE utf8_bin ;

Tiếp theo chúng ta sẽ tạo một bằng có tên users với script như sau:

use mastering_spring_boot;

CREATE TABLE IF NOT EXISTS users (
    `id` bigint unsigned NOT NULL AUTO_INCREMENT,
    `username` varchar(60) COLLATE utf8mb4_unicode_520_ci NOT NULL,
    `email` varchar(120) COLLATE utf8mb4_unicode_520_ci NOT NULL,
    `password` varchar(60) COLLATE utf8mb4_unicode_520_ci NOT NULL,
    `display_name` varchar(120) COLLATE utf8mb4_unicode_520_ci,
    `status` varchar(25) COLLATE utf8mb4_unicode_520_ci NOT NULL,
    `created_at` datetime NOT NULL,
    `updated_at` datetime NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `key_username` (`username`),
    UNIQUE KEY `key_email` (`email`),
    INDEX `index_status` (`status`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;

Phần chuẩn bị đã xong, chúng ta có thể chuyển qua bước cấu hình dự án.

Cấu hình dự án

Để sử dụng được spring-jdbc chúng ta sẽ cần cập nhật tập tin jdbc/pom.xml để bổ sung các phụ thuộc như sau:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>vn.techmaster</groupId>
        <artifactId>mastering-spring-boot</artifactId>
        <version>1.0.0</version>
    </parent>

    <artifactId>jdbc</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-dbcp2</artifactId>
            <version>${dbcp2.version}</version>
        </dependency>
    </dependencies>
</project>

Ở đây chúng ta đã bổ sung các thư viện sau:

  1. spring-jdbc: Thư viện hỗ trợ chúng ta kết nối đến cơ sở dữ liệu. Về bản chất thì nó wrap lại thư viện jdbc của java core.
  2. Mysql-connector-j: Thư viện cài đặt việc kết nối với cơ sở dữ liệu mysql. Về bản chất thì đây là một thư viện socket client để kết nối đến máy chủ mysql.
  3. common-dbcp2: Là một thư viện connection pool. Nếu bạn chưa biết connection pool là gì bạn có thể tham khảo cuốn làm chủ các mẫu thiết kế kinh điển trong lập trình nhé.

Sử dụng JDBC đơn thuần

Để có thể hiểu rõ hơn về spring-jdbc chúng ta sẽ cần hiểu sâu một chút về jdbc thuần của java core. Để xoá, thêm và lấy ra một bản ghi trong bảng users mà chúng ta đã tạo, chúng ta có thể cài đặt với mã nguồn như sau:

package vn.techmaster.jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public class OriginalJdbcStartup {

    public static void main(String[] args) throws Exception {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection connection = DriverManager.getConnection(
            "jdbc:mysql://localhost:3306/mastering_spring_boot",
            "root",
            "12345678"
        );

        String deleteSQL = "DELETE FROM users WHERE username = 'tvd12'";
        try (PreparedStatement preparedStatement = connection.prepareStatement(deleteSQL)) {
            preparedStatement.executeUpdate();
        }

        String insertSQL = "INSERT INTO users (username, email, password, display_name, status, created_at, updated_at) " +
            "VALUES (?, ?, ?, ?, ?, NOW(), NOW())";

        try (PreparedStatement preparedStatement = connection.prepareStatement(insertSQL)) {
            preparedStatement.setString(1, "tvd12");
            preparedStatement.setString(2, "ta.van.dung@techmaster.vn");
            preparedStatement.setString(3, "12345678");
            preparedStatement.setString(4, "Dzung");
            preparedStatement.setString(5, "ACTIVATED");

            int rowsInserted = preparedStatement.executeUpdate();
            if (rowsInserted > 0) {
                System.out.println("A new user was inserted successfully!");
            }
        }
        String fetchSQL = "SELECT * FROM users";

        try (PreparedStatement preparedStatement = connection.prepareStatement(fetchSQL);
             ResultSet resultSet = preparedStatement.executeQuery()) {

            while (resultSet.next()) {
                long id = resultSet.getLong("id");
                String username = resultSet.getString("username");
                String email = resultSet.getString("email");
                String displayName = resultSet.getString("display_name");
                String status = resultSet.getString("status");
                System.out.printf("ID: %d, Username: %s, Email: %s, Display Name: %s, Status: %s%n",
                    id, username, email, displayName, status);
            }
        }
        connection.close();
    }
}

Trong mã nguồn này chúng ta sẽ làm một số việc như sau:

  1. Khởi tạo một kết nối.
  2. Xoá đi bản ghi có username là tvd12 để tránh việc chúng ta sẽ ghi một bản ghi cũng có username bằng tvd12 bên dưới.
  3. Thêm một bản ghi.
  4. Lấy ra một bản ghi và hiển thị.
  5. Đóng kết nối.
    Connection là một giao diện tiêu chuẩn của jdbc, nó cung cấp các hàm để tạo ra các đối tượng thực thi câu lệnh sql ví dụ hàm prepareStatement. Nó cũng cung cấp hàm close để đóng lại kết nối, tuy nhiên chính hàm close này lại gây ra rất nhiều vấn đề. Trong thực tế sẽ hiếm khi nào chúng ta thực sự đóng lại kết nối mà thường giữ lại để tái sử dụng để làm tăng hiệu năng, vì chi phí khởi tạo cho một connection là lớn dễ làm cho cả chương trình của chúng ta và máy chủ sql đạt 100% CPU. Đây là nguyên nhân mà chúng ta phải sử dụng connection pool.
    PreparedStatement cũng là một giao diện tiêu chuẩn của jdbc, nó cung cấp các hàm để xây dựng câu lệnh sql và sau đó thì các driver ví dụ như mysql sẽ cài đặt việc gửi đi câu lệnh sql cho máy chủ để thực thi thật sự.
    Ở mỗi bước chúng cần chú ý rằng chúng ta cần đóng lại các đối tượng mà chúng ta đã mở ví dụ:
try (PreparedStatement preparedStatement = connection.prepareStatement(deleteSQL)) {
    preparedStatement.executeUpdate();
}

Cú pháp này tương đối ngắn gọn, tuy nhiên nó là viết tắt của cú pháp đầy đủ:

PreparedStatement preparedStatement = connection.prepareStatement(deleteSQL)
try {
    preparedStatement.executeUpdate();
} finally {
    preparedStatement.close();
}

Việc đóng lại các đối tượng đã mở sẽ giúp chúng ta giải phóng các tài nguyên từ cả mysql client (chương trình của chúng ta) lẫn mysql server.

Sử dụng Spring JDBC

Như đã nói ở trên thì spring jdbc cũng chỉ là một thư viện wrap lại của java jdbc, nó cung cấp cho chúng ta một số cách tiện lợi hơn để thực thi câu lệnh. Đầu tiên chúng ta sẽ cần khai báo một tập tin có tên application.properties thế này:

jdbc.driverClassName=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mastering_spring_boot
jdbc.username=root
jdbc.password=12345678

Sau đó chúng ta cài đặt một lớp cấu hình để tạo kết nối với cơ sở dữ liệu có tên AppConfig:

package vn.techmaster.jdbc.config;

import lombok.Setter;
import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;

@Setter
@Configuration
@PropertySource("classpath:application.properties")
public class AppConfig {

    @Value("${jdbc.driverClassName}")
    private String jdbcDriverClassName;

    @Value("${jdbc.url}")
    private String jdbcUrl;

    @Value("${jdbc.username}")
    private String jdbcUsername;

    @Value("${jdbc.password}")
    private String jdbcPassword;

    @Bean
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName(jdbcDriverClassName);
        dataSource.setUrl(jdbcUrl);
        dataSource.setUsername(jdbcUsername);
        dataSource.setPassword(jdbcPassword);
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

Ở đây chúng ta chỉ định cho spring đọc tập tin application.properties trong classpath. Classpath là một khái niệm quan trọng của java, bạn có thể tìm hiểu thêm về classpath tại đây. Sau đó chúng ta sẽ gán các giá trị từ tập tin application.properties vào các biến thông qua việc sử dụng @Value annotation. Chúng ta cần sử dụng tập tin cấu hình thay vì fix cứng giá trị để có thể tuỳ chỉnh thông tin kết nối cho từng môi trường triển khai khác nhau.
Tiếp theo chúng ta sẽ có hàm để khởi tạo DataSource ở dạng singleton. DataSource là một giao diện cơ sở của thư viện java.sql, nó đóng gói lại việc khởi tạo các Connection, và ở đây chúng ta đang sử dụng BasicDataSource của thư viện dbcp2, để tạo ra connection pool và tái sử dụng được các connection.
Chúng ta cũng sẽ cần khởi tạo một đối tượng JdbcTemplate từ DataSource để giao tiếp với cơ sở dữ liệu. Lớp này về cơ bản cũng là một lớp wrap lại một số hàm của java jdbc để chúng ta gọi cho tiện hơn mà thôi.
Spring jdbc là một thư viện khá đơn giản, nó sẽ không tự ánh xạ tư từ đối tượng ResultSet sang đối tượng java cho chúng ta, chính vì vậy chúng ta phải khởi tạo một lớp UserRowMapper như sau:

package vn.techmaster.jdbc.mapper;

import org.springframework.jdbc.core.RowMapper;
import vn.techmaster.jdbc.entity.User;
import vn.techmaster.jdbc.entity.UserStatus;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class UserRowMapper implements RowMapper<User> {

    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
        User user = new User();
        user.setId(rs.getInt("id"));
        user.setUsername(rs.getString("username"));
        user.setEmail(rs.getString("email"));
        user.setPassword(rs.getString("password"));
        user.setDisplayName(rs.getString("display_name"));
        user.setStatus(UserStatus.valueOf(rs.getString("status")));
        user.setCreatedAt(
            LocalDateTime.parse(
                rs.getString("created_at"),
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
            )
        );
        user.setCreatedAt(
            LocalDateTime.parse(
                rs.getString("updated_at"),
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
            )
        );
        return user;
    }
}

Có lớp mapper rồi, giờ là lúc chúng ta có thể cài đặt lớp UserRepository để tương tác với cơ sở dữ liệu với mã nguồn như sau:

package vn.techmaster.jdbc.repository;

import lombok.AllArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import vn.techmaster.jdbc.entity.User;
import vn.techmaster.jdbc.mapper.UserRowMapper;

import java.time.format.DateTimeFormatter;

@Repository
@AllArgsConstructor
public class UserRepository {

    private JdbcTemplate jdbcTemplate;

    public void save(User user) {
        String sql = "INSERT INTO " +
            "users (username, email, password, display_name, status, created_at, updated_at) " +
            "VALUES (?, ?, ?, ?, ?, ?, ?)";
        jdbcTemplate.update(
            sql,
            user.getUsername(),
            user.getEmail(),
            user.getPassword(),
            user.getDisplayName(),
            user.getStatus().toString(),
            user.getCreatedAt().format(
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
            ),
            user.getUpdatedAt().format(
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
            )
        );
    }

    public User findByUsername(String username) {
        String sql = "SELECT * FROM users WHERE username = ?";
        try {
            return jdbcTemplate.queryForObject(sql, new UserRowMapper(), username);
        } catch (Exception e) {
            return null;
        }
    }
}

Ở đây chúng ta có hai hàm:

  1. Hàm update để lưu thông tin người dùng vào cơ sở dữ liệu.
  2. Hàm findByUsername để tìm kiếm thông tin người dùng.
    Bây giờ đến phần quan trọng nhất, chung ta sẽ ghép nối mọi thứ lại với nhau thông qua lớp SpringJdbcStartup với mã nguồn như sau:
package vn.techmaster.jdbc;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import vn.techmaster.jdbc.entity.User;
import vn.techmaster.jdbc.entity.UserStatus;
import vn.techmaster.jdbc.repository.UserRepository;

import java.time.LocalDateTime;

public class SpringJdbcStartup {

    public static void main(String[] args) {
        ApplicationContext context =
            new AnnotationConfigApplicationContext(
                "vn.techmaster.jdbc"
            );
        UserRepository userRepository = context.getBean(
            UserRepository.class
        );

        // Fetch user by ID
        User user = userRepository.findByUsername("tvd12");
        if (user == null) {
            user = new User();
            user.setUsername("tvd12");
            user.setEmail("ta.van.dung@techmaster.vn");
            user.setPassword("hash password");
            user.setDisplayName("Dzung");
            user.setStatus(UserStatus.ACTIVATED);
            LocalDateTime now = LocalDateTime.now();
            user.setCreatedAt(now);
            user.setUpdatedAt(now);
            userRepository.save(user);
        }
        User fetchedUser = userRepository.findByUsername("tvd12");
        System.out.println("Fetched User: " + fetchedUser);
    }
}

Ở đây chúng ta đang lấy ra thông tin người dùng với username là tvd12 để kiểm tra sự tồn tại, nếu không có thì chúng ta thêm 1 bản ghi thông tin người dùng vào cơ sở dữ liệu. Tiếp theo chúng ta lấy ra thông tin người dùng và hiển thị.
Về cơ bản thì chúng ta sẽ thấy mã nguồn sử dụng trở nên trong sáng hơn so với việc phải nhìn thấy mấy đối tượng của thư viện như ResultSet, PreparedStatement, nó sẽ giúp chúng ta giảm thiểu công sức để tìm hiểu cũng như cài đặt mã nguồn xử lý nghiệp vụ trong dự án thực tế.

Kết luận

Như vậy chúng ta đã cùng nhau:

  1. Tìm hiểu về cách cấu hình một dự án sử dụng spring jdbc.
  2. Sử dụng JDBC đơn thuần.
  3. Sử dụng spring jdbc để truy xuất cơ sở dữ liệu.

Sách tham khảo

Để hiểu rõ về connection pool các bạn có thể tham khảo cuốn làm chủ các mẫu thiết kế kinh điển trong lập trình và đừng quên nhập mã giảm giá Tech10 để được giảm giá 10% nhé.


Cám ơn bạn đã quan tâm đến bài viết|video này. Để nhận được thêm các kiến thức bổ ích bạn có thể:

  1. Đọc các bài viết của TechMaster trên facebook: https://www.facebook.com/techmastervn
  2. Xem các video của TechMaster qua Youtube: https://www.youtube.com/@TechMasterVietnam nếu bạn thấy video/bài viết hay bạn có thể theo dõi kênh của TechMaster để nhận được thông báo về các video mới nhất nhé.
  3. Chat với techmaster qua Discord: https://discord.gg/yQjRTFXb7a
  4. Đăng ký khoá học java spring fullstack https://java.techmaster.vn/ của techmaster nhé.