Phát triển ứng dụng Java với JDBC Utility và Kiến trúc Ba lớp

Trong phát triển ứng dụng Java, việc tương tác với cơ sở dữ liệu là một nhiệm vụ phổ biến. JDBC (Java Database Connectivity) cung cấp một API để thực hiện các thao tác cơ sở dữ liệu. Tuy nhiên, việc quản lý kết nối và tài nguyên JDBC thủ công có thể trở nên lặp đi lặp lại và dễ xảy ra lỗi. Bài viết này giới thiệu một lớp tiện ích JDBC tùy chỉnh để đơn giản hóa việc quản lý kết nối và tài nguyên, đồng thời thảo luận về kiến trúc ba lớp (three-tier architecture) như một mô hình phát triển ứng dụng hiệu quả.

Lớp Tiện Ích JDBC

Các bước cơ bản khi sử dụng JDBC bao gồm đăng ký driver, thiết lập kết nối và đóng các tài nguyên đã sử dụng. Để tránh lặp lại các bước này trong mỗi lớp, chúng ta có thể tạo một lớp tiện ích JDBC chuyên dụng.

Lớp tiện ích này sẽ bao gồm các phương thức tĩnh để xử lý việc đăng ký driver, lấy kết nối và đóng tài nguyên một cách an toàn. Cấu trúc của lớp tiện ích thường bao gồm phương thức khởi tạo riêng tư để ngăn chặn việc tạo thể hiện và các phương thức tĩnh để truy cập chức năng.


import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class DatabaseUtil {

    private static final String DB_DRIVER = "com.mysql.cj.jdbc.Driver";
    private static final String DB_URL = "jdbc:mysql://localhost:3306/mydatabase"; // Thay thế bằng URL database của bạn
    private static final String DB_USER = "dbuser"; // Thay thế bằng tên người dùng database
    private static final String DB_PASSWORD = "dbpassword"; // Thay thế bằng mật khẩu database

    // Khối tĩnh để đăng ký driver khi lớp được tải
    static {
        try {
            Class.forName(DB_DRIVER);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("Lỗi khi đăng ký driver cơ sở dữ liệu", e);
        }
    }

    // Phương thức để lấy kết nối đến cơ sở dữ liệu
    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
    }

    // Phương thức để đóng các tài nguyên JDBC
    public static void closeResources(Statement stmt, Connection conn, ResultSet rs) {
        closeQuietly(rs);
        closeQuietly(stmt);
        closeQuietly(conn);
    }

    public static void closeResources(Statement stmt, Connection conn) {
        closeResources(stmt, conn, null);
    }

    private static void closeQuietly(AutoCloseable resource) {
        if (resource != null) {
            try {
                resource.close();
            } catch (Exception e) {
                // Ghi log lỗi thay vì ném ngoại lệ để tránh làm gián đoạn quá trình đóng tài nguyên
                System.err.println("Lỗi khi đóng tài nguyên: " + e.getMessage());
            }
        }
    }
}
    

Ví dụ Kiểm Thử Lớp Tiện Ích

Dưới đây là một ví dụ về cách sử dụng lớp tiện ích để thực hiện cập nhật dữ liệu và quản lý giao dịch.


import org.junit.jupiter.api.Test;
import java.sql.Connection;
import java.sql.PreparedStatement;

public class DatabaseUtilTest {

    @Test
    public void testUpdateRecord() {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        try {
            connection = DatabaseUtil.getConnection();
            connection.setAutoCommit(false); // Bắt đầu giao dịch

            String sql = "UPDATE users SET password = ? WHERE username = ?";
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, "newSecurePassword");
            preparedStatement.setString(2, "testuser");

            int rowsAffected = preparedStatement.executeUpdate();

            if (rowsAffected > 0) {
                System.out.println("Cập nhật bản ghi thành công.");
                connection.commit(); // Cam kết giao dịch
            } else {
                System.out.println("Không tìm thấy bản ghi để cập nhật.");
                connection.rollback(); // Hoàn tác giao dịch
            }
        } catch (Exception e) {
            if (connection != null) {
                try {
                    connection.rollback(); // Hoàn tác giao dịch nếu có lỗi
                } catch (SQLException ex) {
                    System.err.println("Lỗi khi hoàn tác giao dịch: " + ex.getMessage());
                }
            }
            System.err.println("Lỗi trong quá trình cập nhật: " + e.getMessage());
        } finally {
            DatabaseUtil.closeResources(preparedStatement, connection); // Đóng tài nguyên
        }
    }
}
    

Kiến Trúc Ba Lớp

Kiến trúc ba lớp là một mô hình thiết kế phần mềm phổ biến nhằm mục đích tách biệt các mối quan tâm (separation of concerns) trong ứng dụng. Nó chia ứng dụng thành ba lớp logic:

  • Lớp Trình Bày (Presentation Layer / Web Layer): Chịu trách nhiệm xử lý giao tiếp với người dùng. Nó nhận yêu cầu, đóng gói dữ liệu, gọi các phương thức của lớp Dịch vụ và trả về kết quả cho người dùng.
  • Lớp Dịch Vụ (Service Layer): Chứa logic nghiệp vụ của ứng dụng. Lớp này điều phối các tác vụ, tương tác với lớp Truy cập Dữ liệu (DAO) và xử lý các quy tắc kinh doanh.
  • Lớp Truy Cập Dữ Liệu (Data Access Layer / DAO Layer): Chịu trách nhiệm tương tác trực tiếp với cơ sở dữ liệu. Nó sử dụng JDBC hoặc các công nghệ ORM để thực hiện các thao tác CRUD (Create, Read, Update, Delete).

Lợi ích của kiến trúc này bao gồm:

  • Giảm sự phụ thuộc (Decoupling): Giảm sự phụ thuộc giữa các lớp, giúp dễ dàng thay đổi hoặc cập nhật một lớp mà không ảnh hưởng đến các lớp khác.
  • Khả năng bảo trì (Maintainability): Dễ dàng xác định và sửa lỗi trong từng lớp.
  • Khả năng mở rộng (Scalability): Dễ dàng mở rộng chức năng bằng cách thêm hoặc sửa đổi các lớp.

Ví dụ Minh Họa: Xác Thực Người Dùng

1. Lớp Đối Tượng Người Dùng (User Object)

Định nghĩa một đối tượng để lưu trữ thông tin người dùng.


public class User {
    private String username;
    private String password;

    public User() {}

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    // Getters and Setters
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }

    @Override
    public String toString() {
        return "User{" +
               "username='" + username + '\'' +
               '}';
    }
}
    

2. Lớp Trình Bày (Web Layer)

Mô phỏng việc nhận dữ liệu đầu vào và gọi lớp Dịch vụ.


// Giả định đây là một lớp controller hoặc servlet
public class UserController {

    public void handleLoginRequest(String username, String password) {
        User loginUser = new User(username, password);

        UserService userService = new UserService();
        boolean isAuthenticated = userService.authenticateUser(loginUser);

        if (isAuthenticated) {
            System.out.println("Đăng nhập thành công!");
        } else {
            System.out.println("Tên đăng nhập hoặc mật khẩu không đúng.");
        }
    }
}
    

3. Lớp Dịch Vụ (Service Layer)

Chứa logic nghiệp vụ cho việc xác thực.


import java.util.Objects;

public class UserService {

    public boolean authenticateUser(User userToAuthenticate) {
        if (userToAuthenticate == null || userToAuthenticate.getUsername() == null || userToAuthenticate.getPassword() == null) {
            throw new IllegalArgumentException("Thông tin người dùng không hợp lệ.");
        }

        UserDAO userDAO = new UserDAO();
        User foundUser = userDAO.findUserByCredentials(userToAuthenticate.getUsername(), userToAuthenticate.getPassword());

        // Kiểm tra xem người dùng có tồn tại và mật khẩu khớp không
        return foundUser != null;
    }
}
    

4. Lớp Truy Cập Dữ Liệu (DAO Layer)

Tương tác với cơ sở dữ liệu để tìm người dùng.


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

public class UserDAO {

    public User findUserByCredentials(String username, String password) {
        User user = null;
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;

        try {
            connection = DatabaseUtil.getConnection();
            String sql = "SELECT user_id, username, password FROM users WHERE username = ? AND password = ?";
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, username);
            preparedStatement.setString(2, password);

            resultSet = preparedStatement.executeQuery();

            if (resultSet.next()) {
                // Lấy thông tin từ ResultSet và tạo đối tượng User
                int userId = resultSet.getInt("user_id");
                String foundUsername = resultSet.getString("username");
                String foundPassword = resultSet.getString("password"); // Lưu ý: Không nên lấy mật khẩu đã mã hóa ra
                user = new User(foundUsername, foundPassword);
                // Có thể thêm trường user_id vào lớp User nếu cần
            }
        } catch (SQLException e) {
            System.err.println("Lỗi truy vấn cơ sở dữ liệu: " + e.getMessage());
            // Xử lý ngoại lệ hoặc ném lại ngoại lệ tùy thuộc vào yêu cầu
        } finally {
            DatabaseUtil.closeResources(preparedStatement, connection, resultSet);
        }
        return user; // Trả về null nếu không tìm thấy người dùng
    }
}
    

Sử dụng Connection Pool (Ví dụ với Druid)

Connection pool là một tập hợp các kết nối cơ sở dữ liệu được quản lý sẵn sàng để sử dụng. Nó giúp cải thiện hiệu suất bằng cách giảm chi phí tạo và đóng kết nối.

Các bước sử dụng Druid Connection Pool:

  1. Thêm thư viện Druid vào dự án của bạn (ví dụ: Maven dependency).
  2. Tạo một tệp cấu hình (ví dụ: `druid.properties`) để định nghĩa các thông số kết nối và cấu hình pool.
  3. Sử dụng lớp `DruidDataSourceFactory` để tạo một đối tượng `DataSource`.
  4. Lấy kết nối từ `DataSource` thay vì sử dụng `DriverManager` trực tiếp.

Tệp Cấu Hình (`druid.properties`):


# Thông số kết nối cơ sở dữ liệu
db.driverClassName=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/mydatabase
db.username=dbuser
db.password=dbpassword

# Thông số cấu hình Connection Pool
druid.initialSize=10
druid.maxActive=50
druid.maxWait=30000
druid.minIdle=5
druid.timeBetweenEvictionRunsMillis=60000
druid.minEvictableIdleTimeMillis=300000
druid.validationQuery=SELECT 1
druid.testWhileIdle=true
druid.testOnBorrow=false
druid.testOnReturn=false
druid.poolPreparedStatements=true
druid.maxPoolPreparedStatementPerConnectionSize=20
    

Ví dụ Sử Dụng Druid


import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.FileInputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;

public class DruidConnectionExample {

    private static DataSource dataSource;

    static {
        try {
            Properties properties = new Properties();
            // Nạp tệp cấu hình druid.properties từ classpath hoặc đường dẫn tuyệt đối
            properties.load(new FileInputStream("src/main/resources/druid.properties")); // Thay đổi đường dẫn nếu cần
            dataSource = DruidDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
            throw new RuntimeException("Lỗi khi khởi tạo Druid DataSource", e);
        }
    }

    public static Connection getDruidConnection() throws SQLException {
        return dataSource.getConnection();
    }

    public static void closeDruidResources(PreparedStatement stmt, Connection conn, ResultSet rs) {
        closeQuietly(rs);
        closeQuietly(stmt);
        closeDruidQuietly(conn); // Trả kết nối về pool thay vì đóng hoàn toàn
    }

    private static void closeDruidQuietly(Connection conn) {
        if (conn != null) {
            try {
                conn.close(); // Trả kết nối về pool
            } catch (SQLException e) {
                System.err.println("Lỗi khi trả kết nối về pool: " + e.getMessage());
            }
        }
    }

    private static void closeQuietly(AutoCloseable resource) {
        if (resource != null) {
            try {
                resource.close();
            } catch (Exception e) {
                System.err.println("Lỗi khi đóng tài nguyên: " + e.getMessage());
            }
        }
    }

    // Ví dụ sử dụng
    public static void main(String[] args) {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;

        try {
            connection = getDruidConnection();
            String sql = "UPDATE users SET password = ? WHERE username = ?";
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, "newPasswordFromPool");
            preparedStatement.setString(2, "testuser");

            int rowsAffected = preparedStatement.executeUpdate();

            if (rowsAffected > 0) {
                System.out.println("Cập nhật dữ liệu thành công bằng Druid.");
            } else {
                System.out.println("Không có bản ghi nào được cập nhật.");
            }

        } catch (SQLException e) {
            System.err.println("Lỗi SQL: " + e.getMessage());
        } finally {
            closeDruidResources(preparedStatement, connection, resultSet);
        }
    }
}
    

Bài Tập Thực Hành: Truy Vấn Dữ Liệu Thương Hiệu

Giả sử có một bảng `brands` với các cột `id`, `name`, `people`, và `status`. Chúng ta cần truy vấn tất cả thông tin thương hiệu và hiển thị chúng.

1. Lớp Đối Tượng Thương Hiệu (Brand Object)


public class Brand {
    private int id;
    private String name;
    private int employeeCount; // Sử dụng tên biến rõ ràng hơn
    private String status;

    public Brand() {}

    public Brand(int id, String name, int employeeCount, String status) {
        this.id = id;
        this.name = name;
        this.employeeCount = employeeCount;
        this.status = status;
    }

    // Getters and Setters
    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 int getEmployeeCount() { return employeeCount; }
    public void setEmployeeCount(int employeeCount) { this.employeeCount = employeeCount; }
    public String getStatus() { return status; }
    public void setStatus(String status) { this.status = status; }

    @Override
    public String toString() {
        return "Brand{" +
               "id=" + id +
               ", name='" + name + '\'' +
               ", employeeCount=" + employeeCount +
               ", status='" + status + '\'' +
               '}';
    }
}
    

2. Lớp Dịch Vụ (Brand Service Layer)

Điều phối việc truy vấn dữ liệu thương hiệu.


import java.util.List;
import java.util.ArrayList; // Cần import nếu chưa có

public class BrandService {

    // Phương thức kiểm tra hoạt động của DAO
    public void testBrandDao() throws Exception {
        BrandDAO brandDAO = new BrandDAO();
        List<Brand> allBrands = brandDAO.findAllBrands();

        if (allBrands != null && !allBrands.isEmpty()) {
            for (Brand brand : allBrands) {
                System.out.println(brand);
            }
        } else {
            System.out.println("Không tìm thấy thương hiệu nào.");
        }
    }
}
    

3. Lớp Truy Cập Dữ Liệu (Brand DAO Layer)

Thực hiện truy vấn dữ liệu từ bảng `brands` bằng cách sử dụng connection pool.


import java.io.FileInputStream;
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 java.util.Properties;
import javax.sql.DataSource;

import com.alibaba.druid.pool.DruidDataSourceFactory; // Giả định sử dụng Druid

public class BrandDAO {

    private DataSource getDataSource() throws Exception {
        Properties properties = new Properties();
        // Giả định tệp druid.properties nằm trong thư mục resources
        properties.load(new FileInputStream("src/main/resources/druid.properties"));
        return DruidDataSourceFactory.createDataSource(properties);
    }

    public List<Brand> findAllBrands() throws Exception {
        List<Brand> brandList = new ArrayList<>();
        DataSource dataSource = getDataSource();
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;

        try {
            connection = dataSource.getConnection();
            // Sử dụng CASE để chuyển đổi giá trị status thành chuỗi dễ đọc
            String sql = "SELECT id, name, people, CASE status WHEN 1 THEN 'Active' WHEN 0 THEN 'Inactive' ELSE 'Unknown' END AS formatted_status FROM brands";
            preparedStatement = connection.prepareStatement(sql);
            resultSet = preparedStatement.executeQuery();

            while (resultSet.next()) {
                int id = resultSet.getInt("id");
                String name = resultSet.getString("name");
                int people = resultSet.getInt("people");
                String status = resultSet.getString("formatted_status"); // Lấy giá trị đã định dạng

                brandList.add(new Brand(id, name, people, status));
            }
        } catch (SQLException e) {
            System.err.println("Lỗi khi truy vấn danh sách thương hiệu: " + e.getMessage());
            throw e; // Ném ngoại lệ để lớp dịch vụ có thể xử lý
        } finally {
            // Sử dụng phương thức đóng tài nguyên chung hoặc riêng của connection pool
            DruidConnectionExample.closeDruidResources(preparedStatement, connection, resultSet);
        }
        return brandList;
    }
}
    

Thẻ: jdbc Java Database Three-Tier Architecture connection pool

Đăng vào ngày 7 tháng 8 lúc 16:39