Có lẽ chúng ta vẫn thường sử dụng jdbc, hoặc các thư viện được xây dựng trên các giao diện của jdbc. Tuy nhiên jdbc có một nhược điểm là tất cả các phương thức của nó đề ở dạng đồng bộ, nghĩa là khi chúng ta thực hiện một câu lệnh truy vấn thì chúng ta sẽ chờ để nhận về ngay kết quả. Ưu điểm của phương pháp này là nó tương đối dễ hiểu và dễ sử dụng, tuy nhiên nó cũng có nhược điểm là làm cho một phần chương trình của chúng ta hay chính xác hơn là một thread trong chương trình của chúng ta bị block dẫn đến khả năng phục vụ một lượng lớn người dùng tại một thời điểm bị hạn chế tương ứng số lượng thread hữu hạn mà chúng ta tạo ra mặc dù tài nguyên máy chủ thì còn rất nhiều. Về bản chất thì các driver ví dụ như mysql-connector-j vẫn chỉ là lập trình socket đơn thuần, mà socket thì không nhất thiết phải block, chính vì vậy chúng ta có thể sử dụng reactive programming để sử dụng sql theo cách bất đồng bộ. Trong bài này Dũng sẽ cùng các bạn đi tìm hiểu spring-r2dbc (Reactive Relational Database Connectivity) một thư viện cầu nối giữa tầng ứng dụng và thư viện reactive cho sql ở dưới nhé.

Chuẩn bị

Bạn sẽ cần tạo một cơ sở dữ liệu với mã nguồn như sau:

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

Sau đó bạn hãy tạo một bảng với mã nguồn:

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;

Khởi tạo module

Chúng ta sẽ khởi tạo module có tên r2dbc

Cấu hình dự án

Chúng ta sẽ cập nhật tập tin r2dbc/pom.xml 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>r2dbc</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-r2dbc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>io.asyncer</groupId>
            <artifactId>r2dbc-mysql</artifactId>
            <version>${r2dbc.version}</version>
        </dependency>
    </dependencies>
</project>

So với jdbc trước đó thì bây giờ chúng ta không còn sử dụng thư viện driver mysql-connector-j nữa, thay vào đó chúng ta sẽ sử dụng thư viện r2dbc-mysql, và chúng ta cũng bổ sung cả thư viện spring-r2dbc nữa.
Về bên trong phần socket thì r2dbc-mysql vẫn sử dụng netty-reactor để tận dụng phần socket client cũng như các lớp thành phần sẵn có cho Reactive của netty.

Cài đặt lớp cấu hình

Lớp cấu hình R2dbcConfig của chúng ta sẽ có mã nguồn như sau:

package vn.techmaster.r2dbc.config;

import io.asyncer.r2dbc.mysql.MySqlConnectionConfiguration;
import io.asyncer.r2dbc.mysql.MySqlConnectionFactory;
import io.r2dbc.spi.ConnectionFactory;
import lombok.Setter;
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.r2dbc.core.DatabaseClient;

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

    @Value("${jdbc.host}")
    private String jdbcHost;

    @Value("${jdbc.port}")
    private int jdbcPort;

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

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

    @Value("${jdbc.database}")
    private String jdbcDatabase;

    @Bean
    public ConnectionFactory connectionFactory() {
        return MySqlConnectionFactory.from(
            MySqlConnectionConfiguration.builder()
                .host(jdbcHost)
                .port(jdbcPort)
                .user(jdbcUsername)
                .password(jdbcPassword)
                .database(jdbcDatabase)
                .build()
        );
    }

    @Bean
    public DatabaseClient databaseClient(ConnectionFactory connectionFactory) {
        return DatabaseClient.create(connectionFactory);
    }
}

Ở đây chúng ta có ConnectionFactory thay vì DataSource, DatabaseClient thay vì JdbcTemplate. Trên thực tế thì đối với kiểu lập trình Reactive này nó có thể tận dụng được rất tốt tài nguyên trên một connection duy nhất vậy nên cũng có thể không cần thiết phải tạo ra nhiều connnection và việc sử dụng connection pool là không cần thiết.
Mã nguồn của tập tin application.properties chứa các thông số cấu hình sẽ như sau:

jdbc.host=localhost
jdbc.port=3306
jdbc.database=mastering_spring_boot
jdbc.username=root
jdbc.password=12345678

Cài đặt lớp repository

Trước khi cài đặt lớp UserRepository bạn có thể copy mã nguồn của lớp User từ bài jdbc sang:

package vn.techmaster.r2dbc.entity;

import lombok.Data;

import java.time.LocalDateTime;

@Data
public class User {
    private Long id;
    private String username;
    private String email;
    private String password;
    private String displayName;
    private UserStatus status;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}
package vn.techmaster.r2dbc.entity;

public enum UserStatus {
    ACTIVATED,
    INACTIVATED
}

Sau đó bạn có thể tạo ra lớp UserRowMapper để chuyển đổi dữ liệu dạng thô sang dạng lớp java thuần:

package vn.techmaster.r2dbc.mapper;

import io.r2dbc.spi.Readable;
import org.springframework.stereotype.Component;
import vn.techmaster.r2dbc.entity.User;
import vn.techmaster.r2dbc.entity.UserStatus;

import java.time.LocalDateTime;

@Component
public class UserRowMapper {

    public User mapRow(Readable rs) {
        User user = new User();
        user.setId(rs.get("id", Long.class));
        user.setUsername(rs.get("username", String.class));
        user.setEmail(rs.get("email", String.class));
        user.setPassword(rs.get("password", String.class));
        user.setDisplayName(rs.get("display_name", String.class));
        user.setStatus(UserStatus.valueOf(rs.get("status", String.class)));
        user.setCreatedAt(rs.get("created_at", LocalDateTime.class));
        user.setCreatedAt(rs.get("updated_at", LocalDateTime.class));
        return user;
    }
}

So với jdbc thì thư viện r2dbc đã cung cấp cho chúng ta hàm get tương đối dễ hiểu và nhiều loại chuyển đổi hơn ví dụ như có thể chuyển đổi trực tiếp từ dữ liệu sql về LocalDateTime mà không cần dùng hàm parse qua string nữa.
Và bây giờ chúng ta sẽ có mã nguồn của lớp UserRepository như sau:

package vn.techmaster.r2dbc.repository;

import lombok.AllArgsConstructor;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Mono;
import vn.techmaster.r2dbc.entity.User;
import vn.techmaster.r2dbc.mapper.UserRowMapper;

@Repository
@AllArgsConstructor
public class UserRepository {

    private final DatabaseClient databaseClient;
    private final UserRowMapper userRowMapper;

    public Mono<Long> save(User user) {
        String sql = "INSERT INTO " +
            "users (username, email, password, display_name, status, created_at, updated_at) " +
            "VALUES (?, ?, ?, ?, ?, ?, ?)";
        return databaseClient.sql(sql)
            .bind(0, user.getUsername())
            .bind(1, user.getEmail())
            .bind(2, user.getPassword())
            .bind(3, user.getDisplayName())
            .bind(4, user.getStatus().toString())
            .bind(5, user.getCreatedAt())
            .bind(6, user.getUpdatedAt())
            .fetch()
            .rowsUpdated();
    }

    public Mono<User> findByUsername(String username) {
        String sql = "SELECT * FROM users WHERE username = ?";
        try {
            return databaseClient.sql(sql)
                .bind(0, username)
                .map(userRowMapper::mapRow)
                .first();
        } catch (Exception e) {
            return null;
        }
    }
}

Ở đây một lần nữa chúng ta lại nhìn thấy đặc sản của Netty Reactive đó là lớp proxy Mono. Về cơ bản vẫn là việc chúng ta tạo câu truy vấn mẫu, truyền tham số vào cho mẫu và gọi thực thi. Tuy nhiên ở đây truy vấn sẽ không được thực thi ngay.

Khởi chạy chương trình

Để khởi chạy chương trình chúng ta sẽ cần cài đặt lớp SpringR2dbcStartup với mã nguồn như sau:

package vn.techmaster.r2dbc;

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

import java.time.LocalDateTime;

public class SpringR2dbcStartup {

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

        // Fetch user by ID
        User user = userRepository
            .findByUsername("tvd12")
            .block();
        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).block();
        }
        User fetchedUser = userRepository
            .findByUsername("tvd12")
            .block();
        System.out.println("Fetched User: " + fetchedUser);
    }
}

Ở đây chúng ta sẽ gọi hàm block để thực thi truy vấn và chờ lấy kết quả và chúng ta sẽ nhận được là:

Fetched User: User(id=1, username=tvd12, email=ta.van.dung@techmaster.vn, password=hash password, displayName=Dzung, status=ACTIVATED, createdAt=2024-06-21T14:30:13, updatedAt=null)

Trên thực tế thì không nhất thiết phải gọi block, chúng ta có thể sử dụng hàm subscribe như sau:

userRepository
    .findByUsername("tvd12")
    .doOnNext(System.out::println)
    .subscribe();

Tổng kết

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

  1. Tìm hiểu về spring r2dbc.
  2. Sử dụng r2dbc để truy vấn cơ sở dữ liệu.

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