数据访问对象模式(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模式核心结构 业务层(Service) DAO接口(契约) JDBC实现 MyBatis实现 JPA实现 数据库(MySQL / PostgreSQL / Oracle) 业务层只依赖DAO接口,不关心底层实现方式

DAO模式的好处,我总结了几点

  • 解耦:业务层不碰SQL,数据访问层不碰业务逻辑
  • 可测试:可以Mock DAO接口,单元测试好写
  • 可替换:从MySQL换到PostgreSQL?从JDBC换到MyBatis?只改实现层
  • 统一管理:所有数据访问集中在一层,连接池、事务、缓存都好控制

避坑指南:我曾经在一个项目里看到有人把DAO接口和实现写在同一个包里,结果想换实现时发现到处都new了具体实现类。记住,业务层只依赖接口,不要依赖实现。依赖注入是你的好朋友。

什么时候不该用DAO?

说实话,不是所有场景都需要DAO。如果你的项目就几张表,业务逻辑也简单,直接上MyBatis的Mapper就够了,没必要再包一层。

但一旦项目上了规模,或者你预感到将来可能要换数据库、换ORM框架,那DAO模式就是你的护身符。

嗯,我个人的经验是:中小型项目直接用MyBatis的Mapper接口当DAO用,大型项目或者需要多数据源的项目,老老实实加一层DAO抽象。


公众号:蓝海资料掘金营,微信deep3321