跳转至

深入理解 Java 中的 DAO 模式

简介

在 Java 开发中,数据访问对象(Data Access Object,简称 DAO)模式是一种广泛应用的设计模式。它主要用于将业务逻辑和数据访问逻辑分离,使得代码结构更加清晰,易于维护和扩展。通过使用 DAO 模式,我们可以将数据库操作封装在独立的类中,业务层只需调用这些类的方法,而无需关心底层数据库的具体实现细节。

目录

  1. DAO 模式基础概念
  2. DAO 模式使用方法
  3. 常见实践
  4. 最佳实践
  5. 小结
  6. 参考资料

DAO 模式基础概念

什么是 DAO 模式

DAO 模式是一种数据访问抽象层,它为业务逻辑提供了一个统一的接口来访问各种数据源,如关系型数据库、文件系统或其他持久化存储。其核心思想是将数据访问逻辑从业务逻辑中分离出来,这样当数据源发生变化(例如从 MySQL 数据库切换到 Oracle 数据库)时,只需要修改 DAO 层的代码,而不会影响到业务逻辑层。

DAO 模式的组成部分

  • DAO 接口:定义了对特定数据对象进行操作的方法签名,如插入、查询、更新和删除等操作。它为业务层提供了统一的访问接口,不涉及具体的实现细节。
  • DAO 实现类:实现了 DAO 接口中定义的方法,负责具体的数据访问操作,包括与数据库建立连接、执行 SQL 语句等。不同的数据源(如不同的数据库)可能有不同的实现类。
  • 数据传输对象(DTO):也称为值对象(VO),用于在不同层之间传递数据。它通常是一个简单的 JavaBean,包含了数据的属性及其 getter 和 setter 方法。

DAO 模式使用方法

示例项目结构

假设我们有一个简单的用户管理系统,使用 MySQL 数据库存储用户信息。项目结构如下:

src/
├── dao/
│   ├── UserDAO.java
│   └── UserDAOImpl.java
├── dto/
│   └── UserDTO.java
├── service/
│   └── UserService.java
└── Main.java

定义 DTO

首先,定义 UserDTO 类,用于在不同层之间传递用户信息。

package dto;

public class UserDTO {
    private int id;
    private String username;
    private String password;

    // getters and setters
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    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;
    }
}

定义 DAO 接口

接下来,定义 UserDAO 接口,声明对用户数据的操作方法。

package dao;

import dto.UserDTO;

import java.util.List;

public interface UserDAO {
    void insertUser(UserDTO user);
    UserDTO getUserById(int id);
    List<UserDTO> getAllUsers();
    void updateUser(UserDTO user);
    void deleteUser(int id);
}

实现 DAO 接口

然后,实现 UserDAO 接口的 UserDAOImpl 类,这里使用 JDBC 连接 MySQL 数据库进行数据操作。

package dao;

import dto.UserDTO;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class UserDAOImpl implements UserDAO {
    private static final String URL = "jdbc:mysql://localhost:3306/mydb";
    private static final String USER = "root";
    private static final String PASSWORD = "password";

    @Override
    public void insertUser(UserDTO user) {
        String sql = "INSERT INTO users (username, password) VALUES (?,?)";
        try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setString(1, user.getUsername());
            pstmt.setString(2, user.getPassword());
            pstmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    @Override
    public UserDTO getUserById(int id) {
        String sql = "SELECT * FROM users WHERE id =?";
        UserDTO user = null;
        try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setInt(1, id);
            ResultSet rs = pstmt.executeQuery();
            if (rs.next()) {
                user = new UserDTO();
                user.setId(rs.getInt("id"));
                user.setUsername(rs.getString("username"));
                user.setPassword(rs.getString("password"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return user;
    }

    @Override
    public List<UserDTO> getAllUsers() {
        String sql = "SELECT * FROM users";
        List<UserDTO> users = new ArrayList<>();
        try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
             PreparedStatement pstmt = conn.prepareStatement(sql);
             ResultSet rs = pstmt.executeQuery()) {
            while (rs.next()) {
                UserDTO user = new UserDTO();
                user.setId(rs.getInt("id"));
                user.setUsername(rs.getString("username"));
                user.setPassword(rs.getString("password"));
                users.add(user);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return users;
    }

    @Override
    public void updateUser(UserDTO user) {
        String sql = "UPDATE users SET username =?, password =? WHERE id =?";
        try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setString(1, user.getUsername());
            pstmt.setString(2, user.getPassword());
            pstmt.setInt(3, user.getId());
            pstmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void deleteUser(int id) {
        String sql = "DELETE FROM users WHERE id =?";
        try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setInt(1, id);
            pstmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

在业务层使用 DAO

最后,在业务层 UserService 类中使用 UserDAO 来处理用户相关的业务逻辑。

package service;

import dao.UserDAO;
import dao.UserDAOImpl;
import dto.UserDTO;

public class UserService {
    private UserDAO userDAO = new UserDAOImpl();

    public void registerUser(UserDTO user) {
        userDAO.insertUser(user);
    }

    public UserDTO getUser(int id) {
        return userDAO.getUserById(id);
    }

    public List<UserDTO> getAllRegisteredUsers() {
        return userDAO.getAllUsers();
    }

    public void updateRegisteredUser(UserDTO user) {
        userDAO.updateUser(user);
    }

    public void deleteRegisteredUser(int id) {
        userDAO.deleteUser(id);
    }
}

测试代码

Main 类中测试我们的 DAO 模式实现。

package;

import service.UserService;
import dto.UserDTO;

public class Main {
    public static void main(String[] args) {
        UserService userService = new UserService();

        // 插入用户
        UserDTO newUser = new UserDTO();
        newUser.setUsername("testUser");
        newUser.setPassword("testPassword");
        userService.registerUser(newUser);

        // 获取用户
        UserDTO retrievedUser = userService.getUser(newUser.getId());
        System.out.println("Retrieved User: " + retrievedUser.getUsername());

        // 获取所有用户
        System.out.println("All Users: " + userService.getAllRegisteredUsers());

        // 更新用户
        retrievedUser.setPassword("newPassword");
        userService.updateRegisteredUser(retrievedUser);

        // 删除用户
        userService.deleteRegisteredUser(retrievedUser.getId());
    }
}

常见实践

事务管理

在 DAO 实现类中,对于涉及多个数据库操作的业务逻辑,需要进行事务管理,以确保数据的一致性。例如,在一个转账操作中,可能涉及到从一个账户扣款并向另一个账户存款两个操作,这两个操作必须要么都成功,要么都失败。可以使用 JDBC 的 Connection 对象的 setAutoCommit(false)commit()rollback() 方法来实现事务管理。

@Override
public void transferMoney(int fromAccountId, int toAccountId, double amount) {
    String sql1 = "UPDATE accounts SET balance = balance -? WHERE id =?";
    String sql2 = "UPDATE accounts SET balance = balance +? WHERE id =?";
    try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD)) {
        conn.setAutoCommit(false);
        try (PreparedStatement pstmt1 = conn.prepareStatement(sql1);
             PreparedStatement pstmt2 = conn.prepareStatement(sql2)) {
            pstmt1.setDouble(1, amount);
            pstmt1.setInt(2, fromAccountId);
            pstmt1.executeUpdate();

            pstmt2.setDouble(1, amount);
            pstmt2.setInt(2, toAccountId);
            pstmt2.executeUpdate();

            conn.commit();
        } catch (SQLException e) {
            conn.rollback();
            e.printStackTrace();
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

数据库连接池

为了提高数据库访问的性能和资源利用率,通常会使用数据库连接池。常见的数据库连接池有 C3P0、DBCP 和 HikariCP 等。以 HikariCP 为例,配置和使用如下:

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import java.sql.Connection;
import java.sql.SQLException;

public class DatabaseUtil {
    private static final HikariDataSource dataSource;

    static {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
        config.setUsername("root");
        config.setPassword("password");
        dataSource = new HikariDataSource(config);
    }

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

在 DAO 实现类中使用连接池获取连接:

@Override
public void insertUser(UserDTO user) {
    String sql = "INSERT INTO users (username, password) VALUES (?,?)";
    try (Connection conn = DatabaseUtil.getConnection();
         PreparedStatement pstmt = conn.prepareStatement(sql)) {
        pstmt.setString(1, user.getUsername());
        pstmt.setString(2, user.getPassword());
        pstmt.executeUpdate();
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

最佳实践

接口隔离原则

在定义 DAO 接口时,应遵循接口隔离原则,将不同类型的数据访问操作分离到不同的接口中。例如,对于用户管理系统,可以定义 UserReadDAO 接口用于查询操作,UserWriteDAO 接口用于插入、更新和删除操作。这样可以避免业务层依赖不需要的方法,提高代码的可维护性和灵活性。

依赖注入

在业务层中,通过依赖注入的方式获取 DAO 对象,而不是在业务层类中直接实例化 DAO 实现类。这样可以方便地进行单元测试,并且在需要切换不同的 DAO 实现(例如从测试环境切换到生产环境)时,只需要修改依赖注入的配置,而不需要修改业务层代码。可以使用 Spring 框架等依赖注入框架来实现这一功能。

异常处理

在 DAO 实现类中,应该对数据库操作可能抛出的异常进行适当处理。一般来说,将数据库特定的异常(如 SQLException)转换为自定义的业务异常,然后抛给业务层处理。这样可以使业务层专注于业务逻辑,而不需要了解底层数据库的异常细节。

public class DataAccessException extends RuntimeException {
    public DataAccessException(String message) {
        super(message);
    }

    public DataAccessException(String message, Throwable cause) {
        super(message, cause);
    }
}

@Override
public void insertUser(UserDTO user) {
    String sql = "INSERT INTO users (username, password) VALUES (?,?)";
    try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
         PreparedStatement pstmt = conn.prepareStatement(sql)) {
        pstmt.setString(1, user.getUsername());
        pstmt.setString(2, user.getPassword());
        pstmt.executeUpdate();
    } catch (SQLException e) {
        throw new DataAccessException("Failed to insert user", e);
    }
}

小结

DAO 模式在 Java 开发中扮演着重要的角色,它通过将数据访问逻辑与业务逻辑分离,提高了代码的可维护性、可扩展性和可测试性。在实际应用中,我们需要根据项目的需求和特点,合理地运用 DAO 模式,并结合事务管理、数据库连接池等技术,遵循接口隔离、依赖注入和异常处理等最佳实践,来构建高效、稳定的应用程序。

参考资料

  • 《Effective Java》 - Joshua Bloch
  • 《Java EE Design Patterns》 - Deepak Alur, John Crupi, Dan Malks