数据访问对象模式(DAO):实体类、DAO接口、DAO实现,以及JDBC与MyBatis中的DAO实践
DAO模式,说白了就是给数据库操作加一层「中间人」。
我刚开始做项目时,业务逻辑和SQL揉在一起写,那叫一个酸爽。后来接手一个老系统,改个查询条件要翻遍十几个文件,改完还担心漏了哪里。嗯,从那以后,我再也不敢把数据访问代码到处乱扔了。
什么是DAO模式?
DAO(Data Access Object)的核心思想很简单:把对数据源的访问封装起来,对外提供统一接口。业务层不需要知道数据存哪了、怎么存的,只管调用就行。
它由三部分组成:
- 实体类(Entity):对应数据库表结构,承载数据
- DAO接口:定义操作契约,比如增删改查
- DAO实现:真正干活的,写SQL、连数据库
核心价值:业务逻辑与数据访问解耦。换数据库?换ORM框架?只改DAO实现层,业务代码不动。
一个标准的DAO长什么样?
先看实体类。我习惯把字段名和数据库列名一一对应,省得后面映射时头疼。
// 实体类
public class User {
private Long id;
private String name;
private String email;
private Integer age;
// getter/setter 省略
}
然后是DAO接口。这里只定义「做什么」,不关心「怎么做」。
// DAO接口
public interface UserDao {
User findById(Long id);
List<User> findAll();
int save(User user);
int update(User user);
int deleteById(Long id);
}
最后是实现类。这里才是真刀真枪写SQL的地方。
// DAO实现(JDBC版)
public class UserDaoJdbcImpl implements UserDao {
private DataSource dataSource;
public UserDaoJdbcImpl(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public User findById(Long id) {
String sql = "SELECT * FROM users WHERE id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return mapRow(rs);
}
}
} catch (SQLException e) {
throw new RuntimeException("查询用户失败", e);
}
return null;
}
private User mapRow(ResultSet rs) throws SQLException {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
user.setAge(rs.getInt("age"));
return user;
}
// 其他方法实现类似...
}
我的习惯:每个DAO实现类都单独写一个mapRow方法,把ResultSet到实体类的转换集中管理。这样如果字段变了,只改一处就行。
JDBC中的DAO实践
用JDBC写DAO,最烦人的就是样板代码。获取连接、创建语句、处理结果集、关闭资源……每个方法都得来一遍。
我曾经在一个项目里统计过,一个简单的CRUD,JDBC代码里真正业务相关的只有30%,剩下70%都是重复的模板代码。
怎么优化?我一般会抽一个基类:
public abstract class BaseDao<T> {
protected DataSource dataSource;
protected T queryForObject(String sql, RowMapper<T> mapper, Object... args) {
// 封装了连接获取、参数设置、结果映射、资源释放
}
protected List<T> queryForList(String sql, RowMapper<T> mapper, Object... args) {
// 同上,返回列表
}
protected int executeUpdate(String sql, Object... args) {
// 封装更新操作
}
}
这样具体的DAO实现只需要关注SQL和映射逻辑,不用再写那些try-catch-finally了。
注意:JDBC的资源释放一定要放在finally块里,或者用try-with-resources。我曾经见过线上连接池被打满,就是因为某个查询异常了没关闭连接。嗯,血的教训。
MyBatis中的DAO实践
MyBatis把DAO模式玩得更彻底。它连实现类都帮你省了,你只需要写接口和XML映射文件(或者注解)。
先看接口:
public interface UserMapper {
User selectById(Long id);
List<User> selectAll();
int insert(User user);
int update(User user);
int deleteById(Long id);
}
再看XML映射:
<mapper namespace="com.example.dao.UserMapper">
<select id="selectById" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO users(name, email, age) VALUES(#{name}, #{email}, #{age})
</insert>
<!-- 其他映射省略 -->
</mapper>
你想想看,MyBatis帮你做了什么事?它把DAO实现层的「获取连接、创建语句、处理结果集」全部自动化了。你只需要写SQL和映射规则。
我个人更喜欢XML方式,虽然注解更简洁,但SQL复杂时,XML的可维护性明显更好。尤其是要调优SQL时,直接从XML里复制到数据库客户端就能跑,不用翻代码。
DAO模式的核心结构
下面这张图把DAO模式的整体脉络画出来了,你看一眼就明白:
DAO模式的好处,我总结了几点
- 解耦:业务层不碰SQL,数据访问层不碰业务逻辑
- 可测试:可以Mock DAO接口,单元测试好写
- 可替换:从MySQL换到PostgreSQL?从JDBC换到MyBatis?只改实现层
- 统一管理:所有数据访问集中在一层,连接池、事务、缓存都好控制
避坑指南:我曾经在一个项目里看到有人把DAO接口和实现写在同一个包里,结果想换实现时发现到处都new了具体实现类。记住,业务层只依赖接口,不要依赖实现。依赖注入是你的好朋友。
什么时候不该用DAO?
说实话,不是所有场景都需要DAO。如果你的项目就几张表,业务逻辑也简单,直接上MyBatis的Mapper就够了,没必要再包一层。
但一旦项目上了规模,或者你预感到将来可能要换数据库、换ORM框架,那DAO模式就是你的护身符。
嗯,我个人的经验是:中小型项目直接用MyBatis的Mapper接口当DAO用,大型项目或者需要多数据源的项目,老老实实加一层DAO抽象。
公众号:蓝海资料掘金营,微信deep3321