资讯动态

从JDBC到DAO模式:手写Java数据库访问层的核心原理与最佳实践

发布时间:2026/10/20 15:10:40 来源:尧图企业网站定制
1. 为什么2025年还要啃JDBC以及DAO模式到底解决了什么问题先别急着说我守旧。现在Java生态里MyBatis、JPA遍地都是很多人写项目直接从Spring Boot的JdbcTemplate开始JDBC反而成了“听说过、没手写过”的黑盒。但我个人的看法是JDBC是Java访问关系型数据库的根。不管上面包了多少层框架预编译、事务、连接管理、ResultSet游标这些底层机制全部还是JDBC那套东西。你把JDBC亲手写扎实了再回头用MyBatis、JPA会突然明白很多“框架为什么这么设计”的答案。写这篇的起因是模拟项目X里需要搭一个老式的、不依赖Spring的数据库访问层。在Spring Boot主导的时代这个需求听起来有点返祖但真当你脱离框架保护直接用原生的JDBC去连数据库、做增删改查、处理事务你会立刻面对几个绕不开的问题连接谁管理SQL怎么拼才安全ResultSet怎么转成对象事务边界谁来控制而DAO设计模式恰好就是解决这些问题的思路基石。这篇文章我打算用一条线串下来先拆JDBC的核心组件再说DAO模式的结构设计然后给出一套完整可跑的代码实现最后把我在实操过程中踩过的坑和排查思路整理出来。目标是让一个只会“照着教程敲框架”的同学也能独立写出一层可维护、可扩展的数据库访问层。内容按真实项目开发的节奏来不搞教科书式的陈列。2. 先把JDBC那套核心组件吃透后面写代码才会顺2.1 JDBC的本质一套跨数据库的Java方言协议JDBC的全称是Java Database Connectivity它是一套由Java定义的标准接口具体怎么跟某个数据库通信由各数据库厂商自己实现的驱动包负责。你在代码里面向的是java.sql包下的接口真正干活的其实是mysql-connector-java、ojdbc、postgresql这些驱动。这套抽象最大的好处是你写的核心代码理论上可以无缝迁移到不同的数据库。当然理论和现实有差距——不同数据库的SQL方言差异、分页写法差异、特殊类型处理都会让“无缝迁移”变成美好的愿望。但至少在Java侧你不需要为Oracle学一套API为MySQL再学一套API这就是标准接口的价值。理解JDBC只要抓住一条清晰的执行链路加载并注册驱动让JVM知道mysql-connector这个jar里的Driver类存在并把自己注册到DriverManager。获取连接通过连接字符串、用户名、密码向数据库发起连接请求得到一个Connection对象。创建Statement有三种选择Statement、PreparedStatement、CallableStatement分别对应普通SQL、预编译SQL、存储过程调用。执行SQL并获取结果executeQuery返回ResultSetexecuteUpdate返回受影响行数。遍历结果集ResultSet内部是一个游标模型通过next()逐行移动。关闭资源Connection、Statement、ResultSet都是重量级资源不关闭会直接拖垮数据库连接池。我用一个生活化的类比来串这条链路驱动是快递公司的司机Connection是快递车辆Statement是运单模板ResultSet是车上的货品列表关闭资源就是车辆回场并完成交接。每一个环节都是固定的缺一环就出问题。2.2 连接字符串里的学问以及驱动加载到底要不要写Class.forName连接字符串JDBC URL是新手最容易懵的地方。以MySQL为例标准格式长这样jdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue拆开看就是协议头jdbc:mysql://主机、端口数据库名以及一堆参数。这里有几个参数值得好好说serverTimezoneMySQL 8.0之后必须设置否则你的应用服务器时区和数据库服务器时区不一致日期时间字段会差8小时。设置为Asia/Shanghai基本是标配。useSSLfalse本地开发和内网环境建议关闭SSL握手省去证书配置的麻烦。公网环境则需要仔细配置SSL证书。allowPublicKeyRetrievaltrueMySQL 8.0的用户认证插件是caching_sha2_password首次连接时需要向服务器获取公钥如果不开启这个参数会出现“Public Key Retrieval is not allowed”的报错。characterEncodingutf8统一字符集避免中文乱码。注意在连接层面配置utf8后建表时的字符集也要是utf8否则等于白配。至于Class.forName(com.mysql.cj.jdbc.Driver)这行代码很多人奇怪现在不写好像也能跑。这是因为JDBC 4.0之后引入了SPI机制驱动jar包里的META-INF/services/java.sql.Driver文件会被自动加载。所以我个人建议现代项目不用再显式写Class.forName但如果你在写一个需要严格兼容JDBC 3.0的存量项目还是要保留这行。这不是“完全没用”而是保留了对老版本Java环境的兼容。2.3 PreparedStatement为什么是唯一推荐以及它到底预编译了什么很多人习惯用Statement拼接SQL这在项目里是绝对的坏味道。我举个极简例子String sql SELECT * FROM user WHERE username username AND password password ;如果username传的是admin OR 11那SQL就变成了SELECT * FROM user WHERE username admin OR 11 AND password 这相当于绕过密码校验直接以admin身份登录。这就是经典的SQL注入。PreparedStatement解决这个问题的思路是先把SQL骨架发送给数据库数据库完成词法解析和语义分析生成执行计划然后再把参数当纯数据传进去。参数的填充是在数据库端完成的无论你传什么特殊字符都只是数据不会改变SQL结构。这么说可能有点抽象我用一个更形象的比喻PreparedStatement的SQL骨架就像一个带空格的申请表数据库已经知道这张表的格式了你往里填的内容再奇怪也改变不了表格本身的结构。而Statement则是把内容直接混进标题里内容里带着逗号、引号标题就可能变成另外一张表。所以我的习惯是凡是Statement一律不用。哪怕只是一个简单的查询也写PreparedStatement。这不仅是为了防注入还有一个实际好处同一个SQL骨架执行多次时数据库可以复用执行计划性能更好。2.4 ResultSet游标模型以及ResultSet映射为对象的三种方式ResultSet读数据是一个“游标逐行移动”的模型。初始状态下游标位于第一行之前必须调用一次next()才能移动到第一行。next()返回true表示还有下一行返回false表示遍历完毕。这是理解全部数据读取逻辑的前提。拿到一行数据后可以用getInt(id)、getString(username)、getDate(create_time)等方法来取值。这里有个小坑getDate返回的是java.sql.Date它只包含日期不包含时间如果数据库字段是datetime类型直接getDate会把时分秒丢掉正确做法是用getTimestamp拿到后转成java.util.Date或Java 8的LocalDateTime。把ResultSet转成Java对象我见过三种方式手动逐列get然后set到对象里代码最啰嗦但最可控。使用Apache Commons DbUtils的BeanProcessor或Spring的BeanPropertyRowMapper基于反射自动映射要求数据库列名和Java属性名能对上。使用ORM框架像MyBatis写一个ResultHandler或TypeHandler本质也是自动映射。在裸JDBC阶段最推荐的是第一种自己写个RowMapper接口统一行到对象的转换逻辑。这样代码是死板了点但每一步都透明排错特别方便。3. DAO设计模式怎么把数据库操作从业务逻辑里抽出来3.1 为什么你的第一版代码会越写越乱以及DAO的关键思路很多同学写第一个CRUD程序时都是直接在业务逻辑方法里写连接、写SQL、遍历结果集。比如一个注册功能先写一堆JDBC代码查用户名是否存在再写一堆JDBC代码插入新用户业务代码和数据访问代码混成一锅粥。这在功能少的时候没啥感觉等你需要支持新增用户时加一个“VIP会员”字段又要在十几个方法里改。DAOData Access Object模式的思路是把“数据访问”的所有细节都关在一个独立的门后面。业务层只需要知道有一个UserDao它有个findByUsername方法能返回一个User对象至于这个方法里是连了SQLite还是MySQL是裸JDBC还是别的什么业务层完全不关心。这样做的好处有三个职责分离业务层只关心“希望得到什么数据”DAO只关心“怎么从数据库拿数据”。可替换性数据库从MySQL换成PostgreSQL修改DAO实现类即可业务层一行不改。可测试性单元测试时可以给DAO提供一个假实现或内存数据库实现不需要真实数据库。3.2 DAO接口与DAO实现类的关系标准的DAO模式会先定义一个接口描述数据访问能力再写一个实现类实现具体逻辑。比如public interface UserDao { User findById(Integer id); User findByUsername(String username); ListUser findAll(); int insert(User user); int update(User user); int deleteById(Integer id); }接口定义了“能做什么”实现类回答“怎么做”。在小型项目中有人觉得接口是多余的直接类就行。但我建议还是保留接口理由很简单你对DAO的调用方式稳定之后缓存代理、事务增强这些横切逻辑可以在接口后面做装饰器这在后续扩展中特别有用。3.3 实体对象 vs 持久化对象一个解决字段映射混乱的概念在写DAO之前先要想清楚一个问题数据库里的一行对应Java里的什么答案是实体类。我习惯把实体类和数据库表严格对应表名、字段名、字段类型一一映射。例如user表对应User类字段id、username、password、email、create_time分别对应Java属性。这里有一个常见的命名纠结Java属性用驼峰createTime数据库字段用下划线create_time。在裸JDBC中你需要自己处理这块映射手动在RowMapper里写rs.getTimestamp(create_time)和user.setCreateTime(...)。虽然繁琐但也避免了框架自动映射时“猜字段名”的各种奇怪问题。实体类我只推荐存放数据本身像validate逻辑、业务规则不要塞在里面。它就是一个携带数据的POJO。对于DAO层的操作而言insert和update方法接收实体对象select方法返回实体对象这个对象在层间传递像外卖一样是各个模块间沟通的“数据载体”。4. 完整实操一个可复用的DAO层是怎么搭出来的4.1 定义基础行映射接口和工具类首先我定义一个RowMapper接口专门负责把ResultSet的一行转成对象FunctionalInterface public interface RowMapperT { T mapRow(ResultSet rs) throws SQLException; }然后是通用查询工具类。为了避免每一个DAO实现里反复写连接、PreparedStatement、遍历ResultSet、关闭资源我封装一个JdbcTemplate简易版不引Spring自己写裸工具public class JdbcUtils { private static String url; private static String user; private static String password; static { try (InputStream in JdbcUtils.class.getClassLoader().getResourceAsStream(db.properties)) { Properties props new Properties(); props.load(in); url props.getProperty(jdbc.url); user props.getProperty(jdbc.username); password props.getProperty(jdbc.password); Class.forName(props.getProperty(jdbc.driver)); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r ! null) { try { r.close(); } catch (Exception ignored) { } } } } }提示close方法里为什么用AutoCloseable可变参数因为Connection、Statement、ResultSet都实现了AutoCloseable。用可变参数调用方可以一行代码一股脑关闭所有资源简洁不少。同时注意关闭顺序和打开顺序相反ResultSet先关Statement其次Connection最后。然后是一个简易的执行模板public class SimpleJdbcTemplate { public static T T query(String sql, RowMapperT mapper, Object... params) { Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn JdbcUtils.getConnection(); ps conn.prepareStatement(sql); for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } rs ps.executeQuery(); if (rs.next()) { return mapper.mapRow(rs); } return null; } catch (SQLException e) { throw new RuntimeException(e); } finally { JdbcUtils.close(rs, ps, conn); } } public static T ListT queryList(String sql, RowMapperT mapper, Object... params) { Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn JdbcUtils.getConnection(); ps conn.prepareStatement(sql); for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } rs ps.executeQuery(); ListT list new ArrayList(); while (rs.next()) { list.add(mapper.mapRow(rs)); } return list; } catch (SQLException e) { throw new RuntimeException(e); } finally { JdbcUtils.close(rs, ps, conn); } } public static int update(String sql, Object... params) { Connection conn null; PreparedStatement ps null; try { conn JdbcUtils.getConnection(); ps conn.prepareStatement(sql); for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(e); } finally { JdbcUtils.close(ps, conn); } } }这个简易模板的思路是把“获取连接-执行SQL-处理结果-关闭资源”这个固定流程抽出来只把可变的部分SQL、参数、行映射留给调用方。它很像Spring的JdbcTemplate但又没有那一大堆依赖非常适合不引框架的项目。4.2 实体类、DAO接口、DAO实现类代码全解先定义实体类。以用户表为例public class User { private Integer id; private String username; private String password; private String email; private LocalDateTime createTime; // getter / setter / 构造方法 / toString省略 }然后是DAO接口public interface UserDao { User findById(Integer id); User findByUsername(String username); ListUser findAll(); int insert(User user); int update(User user); int deleteById(Integer id); }再写实现类public class UserDaoImpl implements UserDao { private static final String SQL_FIND_BY_ID SELECT id, username, password, email, create_time FROM user WHERE id ?; private static final String SQL_FIND_BY_USERNAME SELECT id, username, password, email, create_time FROM user WHERE username ?; private static final String SQL_FIND_ALL SELECT id, username, password, email, create_time FROM user; private static final String SQL_INSERT INSERT INTO user(username, password, email, create_time) VALUES(?, ?, ?, ?); private static final String SQL_UPDATE UPDATE user SET username ?, password ?, email ?, create_time ? WHERE id ?; private static final String SQL_DELETE DELETE FROM user WHERE id ?; private static final RowMapperUser USER_ROW_MAPPER rs - { User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setPassword(rs.getString(password)); user.setEmail(rs.getString(email)); Timestamp ts rs.getTimestamp(create_time); if (ts ! null) { user.setCreateTime(ts.toLocalDateTime()); } return user; }; Override public User findById(Integer id) { return SimpleJdbcTemplate.query(SQL_FIND_BY_ID, USER_ROW_MAPPER, id); } Override public User findByUsername(String username) { return SimpleJdbcTemplate.query(SQL_FIND_BY_USERNAME, USER_ROW_MAPPER, username); } Override public ListUser findAll() { return SimpleJdbcTemplate.queryList(SQL_FIND_ALL, USER_ROW_MAPPER); } Override public int insert(User user) { return SimpleJdbcTemplate.update(SQL_INSERT, user.getUsername(), user.getPassword(), user.getEmail(), user.getCreateTime()); } Override public int update(User user) { return SimpleJdbcTemplate.update(SQL_UPDATE, user.getUsername(), user.getPassword(), user.getEmail(), user.getCreateTime(), user.getId()); } Override public int deleteById(Integer id) { return SimpleJdbcTemplate.update(SQL_DELETE, id); } }这里有一个很值得学习的细节SQL语句定义成了static final常量。为什么因为SQL骨架是固定的放在方法里每次调用都要重新创建字符串和拼接放在常量区则只创建一次。更重要的是把SQL集中放在实现类顶部一眼就能看出这个DAO能操作哪些SQL维护时非常清晰。USER_ROW_MAPPER也被定义为静态常量因为它不依赖任何实例状态每个查询都复用同一份映射逻辑。这是一个很小的性能优化也提升了代码整洁度。时间字段用getTimestamp()获取再转LocalDateTime避免丢时分秒的问题。4.3 事务边界怎么控制以及为什么DAO方法不太适合承载事务上面讲的增删改查每个方法内部是独立的连接、独立的事务边界。这在单条SQL场景下没问题但遇到“一次操作要改多张表中间任何一步失败都要全部回滚”的场景就麻烦了。典型例子是转账扣款成功、入账失败必须把钱退回。事务的边界在DAO的单个方法里控制是控制不住的因为一个跨多表的业务操作通常要调用多个DAO方法。所以正确做法是在业务服务层开启事务让多个DAO方法在同一个Connection上执行。那怎么让DAO方法用同一个连接答案是让DAO方法不再内部建立和关闭连接而是接收外部传入的Connection。我给出一个事务处理的参考实现public class UserService { public void transfer(Integer fromUserId, Integer toUserId, BigDecimal amount) { Connection conn null; try { conn JdbcUtils.getConnection(); conn.setAutoCommit(false); AccountDao accountDao new AccountDaoImpl(conn); accountDao.decreaseBalance(fromUserId, amount); accountDao.increaseBalance(toUserId, amount); conn.commit(); } catch (SQLException e) { try { conn.rollback(); } catch (SQLException ex) { throw new RuntimeException(回滚失败, ex); } throw new RuntimeException(转账失败, e); } finally { try { conn.setAutoCommit(true); } catch (SQLException ignored) { } JdbcUtils.close(conn); } } }注意代码里对Connection做了特别处理——关闭Connection前先把autoCommit恢复为true。如果不恢复这个连接归还给连接池后下一次被复用时可能还停留在false状态导致后续操作全部自动开启事务、无法自动提交这是非常隐蔽的生产事故。手动把setAutoCommit(true)还原是经验之谈。这种方法的问题在于DAO构造时接收连接会让实现类在有事务和无事务场景下有两种表现。更优雅的解决方案是引入ThreadLocalConnection在同一线程内传递同一个Connection让DAO在内部获取连接时自动命中线程绑定的连接。这个方案进一步扩展就是Spring的DataSourceTransactionManager的原型。在项目初期我建议先从“传连接”这个最朴实的方式做起理解了它的痛点再去学ThreadLocal方案你会理解得更透彻。4.4 连接池为什么是刚需以及常见的连接池对比裸JDBC里每次getConnection都直接用DriverManager创建一个物理连接这在开发时没问题但生产环境很快会出事。数据库建立物理连接的流程包括TCP握手、身份验证、字符集协商是一个几十到几百毫秒的重操作。如果每次SQL都走一遍这个流程性能会断崖式下降。连接池的思路是在应用启动时预先创建一批连接放到池子里用的时候从池子取用完归还到池子而不是销毁。主流连接池我整理成一个对比表连接池易用性性能配套功能适合场景DBCP一般稳定基础老项目兼容C3P0一般一般基础老项目兼容HikariCP好用极佳监控、JMX新项目首选Druid好用优秀监控、SQL防注入、慢SQL日志需要监控/运维排查的团队我个人在裸JDBC阶段会直接选HikariCP配置量极小性能又是业界标杆。核心配置无非是以下几项HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/library); config.setUsername(root); config.setPassword(password); config.setDriverClassName(com.mysql.cj.jdbc.Driver); config.setMaximumPoolSize(10); config.setConnectionTimeout(30000);接入连接池后JdbcUtils.getConnection()的实现可以换成dataSource.getConnection()业务代码一行都不用改。连接池的引入是纯粹的底层升级对DAO层是透明的这就是DAO模式解耦的又一个收益。4.5 配置信息外置以及静态初始化块的加载时机在工程化层面不建议把数据库URL、用户名、密码硬编码在Java代码里。我使用的方案很简单src/main/resources/db.properties在JdbcUtils的静态初始化块中读取。这样环境切换开发、测试、生产只需替换配置文件不需要重新编译。配置内容类似jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password123456注意如果项目向Git仓库提交务必要把db.properties加入忽略名单。数据库密码一旦泄漏相当于把整个数据库暴露在公网。生产环境建议密码用环境变量注入不要直接写在仓库里。5. 实操路上常见的六个坑以及对应的排查思路5.1 数据库驱动找不到启动直接ClassNotFoundException这个问题最常见的原因是jar包没有正确引入。排查时依次检查pom.xml里是否写了对应版本的mysql-connector-java依赖。如果是非Maven项目检查WEB-INF/lib有没有把驱动jar打进去。如果用的是Tomcat驱动jar最好放在web应用的lib目录不要放在Tomcat的全局lib目录。另一个坑是版本兼容MySQL 5.x用com.mysql.jdbc.DriverMySQL 8.x必须用com.mysql.cj.jdbc.Driver。换数据库版本的同时记得检查驱动类名。5.2 中文乱码乱码是个多链路问题任何一个环节字符集不一致就会出现。检查顺序是数据库建表时字段是不是utf8表级字符集是不是utf8mb4。连接URL里是否带了characterEncodingutf8。页面或接口接收参数时HTTP请求本身的编码是否为UTF-8。我遇到过一个“诡异”的案例连接URL和数据库字符集都对但插进去仍然是问号。最后定位到是后端代码在向PreparedStatement传参时传入的String本身就是乱码从某个历史接口读出来的数据没转码。所以排查乱码时不要只盯着数据库和连接先用System.out.println把待插入的参数打出来看看。5.3 数据库表字段和Java关键字冲突有些字段喜欢用desc、order、group这类名字这些是SQL关键字直接写SELECT desc FROM table会直接报语法错误。解决方法是反引号包字段名比如SELECT desc FROM table。更推荐的是在设计表结构阶段就避免用关键字做字段名。5.4 大量连接未关闭数据库连接耗尽我接手过一个“跑一段时间就需要重启应用”的项目。排查时先看数据库的SHOW PROCESSLIST发现几百个Sleep连接挂着。根因是某段代码在return之前忘记关闭Connection异常路径下连接没释放。解决办法有两个层面代码层面强制全部走try-with-resources或finally关闭。运维层面数据库服务器配置wait_timeout让超时的空闲连接被自动回收但这只是治标。另一个常见场景是连接池的maximumPoolSize设置太小并发一高所有线程都在等待连接出现Connection is not available, request timed out。这时候调大连接池参数的同时也要检查业务代码有无连接泄漏。5.5 用getDate取datetime字段导致时间丢失这个问题我在前文提到过但值得当独立坑再说一次。rs.getDate(create_time)返回的是java.sql.Date它只有年月日。如果字段是datetime类型直接getDate时分秒全部丢失如果字段是timestamp类型getDate也会丢弃时间部分。必须使用rs.getTimestamp(create_time)再转换成LocalDateTime或java.util.Date。5.6 SQL注入的隐蔽变体LIKE拼接和ORDER BY参数化先解决LIKE注入PreparedStatement可以直接传参但SQL里写LIKE ?传入%张%这样的字符串。有人会下意识把%拼在SQL里那就又回到了Statement的老路。再看ORDER BYPreparedStatement的参数占位符不能用在表名、列名、排序方向这类结构位置。比如ORDER BY ?数据库不会把你传入的create_time当列名。如果确实需要动态排序正确做法是白名单校验只允许传预定集合内的排序字段比如allowedColumns.contains(orderBy)通过才拼接。这是我推荐的安全策略。6. 进阶思考DAO模式该怎么自然演进到框架思维6.1 从DAO到通用DAO再到泛型DAO当你写完UserDao、BookDao、OrderDao之后会很快发现这些DAO有大量重复结构都是findById、findAll、insert、update、deleteById。这时候可以抽一个BaseDaoT泛型接口和BaseDaoImplT泛型类把共性方法收进去子类只要补充特有的查询方法。泛型DAO的核心是反射在构造BaseDaoImpl时通过ParameterizedType拿到T的实际类型再结合注解或命名约定推导表名和主键列名。这让DAO层从“每张表写一个类”进化成“一张表补一个子类”。但注意泛型DAO的反射逻辑会提高代码复杂度项目表数量少于20张时我并不建议上这套老老实实每张表一个类反而直观。6.2 DAO之上再加一层Service是为了什么有了DAO之后Business Service层的价值体现在DAO方法粒度是单表操作而业务方法粒度是完整的用例流程。比如“用户注册”这个业务既要查重又要插入可能还要给用户发一封欢迎邮件——这在DAO层没法表达只有Service层能把多个DAO编排起来。另外Service层还有一个隐含作用事务边界。我在前文讲过事务跨越多个DAO方法时需要外层控制。Service层天然适合做这件事方法A调用userDao.insert和userLogDao.insert整个方法形成一个事务。这也是Service和DAO分层的一个重要理由。6.3 这个体系在MyBatis/JPA中对应什么位置最后说一个从JDBC到框架的映射关系理解这个对学习新框架特别有帮助PreparedStatement对应MyBatis的#{}占位符与JDBC typeHandler机制。ResultSet手动映射对应MyBatis的resultMap对应JPA的实体字段映射注解。自己写的SimpleJdbcTemplate对应Spring的JdbcTemplate。自己实现的DAO接口对应Spring Data JPA里的Repository接口。手工管理Connection事务对应Spring的Transactional注解与ThreadLocal绑定。你只有亲手踩过裸JDBC的坑才会真正明白MyBatis为什么要设计Mapper接口、为什么要引入TypeHandler也才会理解JPA为什么把实体和表映射的规则做得那么“固执”。框架是封装但封装的底子是JDBC。底子打牢了框架用起来才不会是空中楼阁。6.4 如果我要重新选择会怎么给新项目分层如果让我今天重新搭一个不依赖Spring但需要数据库访问的项目我的分层会是这样实体层POJO只存数据。工具层JdbcUtils提供连接与资源释放。模板层SimpleJdbcTemplate封装固定流程。DAO接口层定义数据访问契约。DAO实现层面向具体数据库的语言实现。Service层组合DAO方法控制事务边界。用一句话总结这一套架构的精髓把前提条件都抽出去让数据访问代码做减法让SQL和结果映射成为唯一的关注点。这看起来步骤多但每一层都清晰可测改一个地方不会牵连一片长期维护下来非常省心。最后一个小建议新手学这一套时一定不要只看不写。找一个小项目比如一个极简的图书管理系统用裸JDBCDAO实现完整CRUD和事务再尝试把HikariCP接进去最后对比框架做法。写的过程中你踩到的每一个坑都会成为日后和框架机制“对账”的最好素材。这套基本功别嫌啰嗦真到排查生产问题的时候你会感激自己当初啃过底层。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑