深入理解 Java 中的 DAO 模式
简介
在 Java 开发中,数据访问对象(Data Access Object,简称 DAO)模式是一种广泛应用的设计模式。它主要用于将业务逻辑和数据访问逻辑分离,使得代码结构更加清晰,易于维护和扩展。通过使用 DAO 模式,我们可以将数据库操作封装在独立的类中,业务层只需调用这些类的方法,而无需关心底层数据库的具体实现细节。
目录
- DAO 模式基础概念
- DAO 模式使用方法
- 常见实践
- 最佳实践
- 小结
- 参考资料
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