ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

从raw JDBC到MyBatis:样板代码如何拖垮开发效率

从raw JDBC到MyBatis:样板代码如何拖垮开发效率 很多人第一次接触样板代码这个词是在写 Java 的时候。不管是早年用 raw JDBC 连数据库还是后来切换到 MyBatis总有一大段结构固定、内容重复、改了这行忘那行的代码横在业务逻辑前面。这些代码就是 boilerplate code——计算机编程里那些必须写、但几乎不改的胶水代码。今天这篇就想把 raw JDBC 和 MyBatis 这两代数据库访问方式里的样板代码掰开揉碎聊一遍。对不同基础的读者都有用刚入门的朋友能看懂为什么前辈们那么讨厌写 JDBC用 MyBatis 写了一阵子但没深究过原理的朋友也能借此理解框架替我们省掉了什么、又带来了哪些新的负担。1. 样板代码是什么为什么说它是 productivity killer1.1 从定义说起那些不得不写的固定代码在计算机编程里boilerplate code 指的是在程序很多地方都必须出现、但内容和结构几乎完全一致的代码片段。它像印刷报纸时预先排好的固定版面你每次只需要改掉中间一小块内容周围一圈版式纹丝不动。以数据库访问为例。一个 Java 程序不管查什么表、查什么字段只要用最原始的 JDBC 方式代码骨架永远是同样的六步// 加载驱动 → 建立连接 → 创建 Statement → 执行 SQL → 处理结果集 → 关闭资源 Class.forName(com.mysql.cj.jdbc.Driver); Connection conn DriverManager.getConnection(url, user, password); Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(select * from user); while (rs.next()) { // 处理每一行数据 } rs.close(); stmt.close(); conn.close();这六步里真正跟业务相关的只有执行 SQL和处理结果集两步。其他四步在每一个 DAO 方法里都会原封不动地再出现一遍。你写 10 个查询就要把这四步复制粘贴 10 次你写 50 个增删改查那就是 200 次重复。这就是样板代码最直观的形态。它本身没有多难甚至可以说毫无技术含量但它的杀伤力恰恰来自没有技术含量却占据大量时间。一个 CRUD 项目里真正有业务价值的逻辑可能只占三成剩下七成时间都在跟连接、关闭、异常处理搏斗。生产效率就是这么被拖垮的——不是被某个复杂的算法拖垮而是被海量的、无脑的、机械的重复劳动淹没。1.2 样板代码的真正危害认知负担和注意力涣散如果说浪费时间还只是表面损失样板代码对开发者认知状态的破坏才更致命。心理学上有个概念叫注意力残留当你从一个任务切换到另一个任务时前一个任务的思维会残留在脑中干扰你对当前任务的专注。写代码也是同理。当你正在处理一条 SQL 的查询逻辑脑子里突然要切到连接有没有关异常有没有处理驱动加载会不会报 ClassNotFoundException——这些切换非常频繁每写一个 DAO 方法就要切换好几次。我见过很多刚转 Java 的同事在 JDBC 代码里栽跟头栽得最多的反而不是复杂的 SQL而是忘记关闭 ResultSet导致数据库连接池被耗尽异常发生时连接没释放线上服务三两下就挂了Class.forName在不同驱动版本里写错类名启动就报错这些问题都不是不会而是顾不上。人的工作记忆空间是有限的当你把大量脑力花在样板代码上时分给真正业务逻辑的注意力自然就少了。代码质量下降、Bug 增多、排查时间变长形成恶性循环。所以我说样板代码是 productivity killer它不是杀死你的程序而是杀死你的产出效率和写代码时的专注状态。理解了这个前提再看 raw JDBC 和 MyBatis 的对比会有完全不同的感觉。2. 亲手写一遍 raw JDBC每一步都是痛点2.1 一段更贴近真实项目的 JDBC 代码解剖理论讲完上一段真实的老式 JDBC 代码。假设你要写一个按 ID 查询用户的方法public User findById(Long id) throws SQLException { Connection conn null; PreparedStatement ps null; ResultSet rs null; User user null; try { conn DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD); String sql select id, name, email, created_at from user where id ?; ps conn.prepareStatement(sql); ps.setLong(1, id); rs ps.executeQuery(); if (rs.next()) { user new User(); user.setId(rs.getLong(id)); user.setName(rs.getString(name)); user.setEmail(rs.getString(email)); user.setCreatedAt(rs.getTimestamp(created_at)); } return user; } finally { if (rs ! null) rs.close(); if (ps ! null) ps.close(); if (conn ! null) conn.close(); } }这段代码我相信很多人看着眼熟。它已经是写对的版本了用了 PreparedStatement 防注入用了 finally 保证资源释放结果集和语句都在关闭。但即便写得再规范问题依然很明显。首先是冗长。一个真正有业务价值的操作就是select 一个用户把它转换成 User 对象大概 15 行代码能解决。但为了保证连接正确、资源释放、异常不吞你需要额外写三倍以上的代码。所有 DAO 方法都在重复这个模式而且这次是手动处理每个字段rs.getLong(id)、rs.getString(name)……你写第 50 个查询时这种体力活已经开始麻木。其次每写一个方法都要重复造轮子。网上有很多JDBC 工具类DBUtils之类的封装本质上是想把这套连接管理和结果集映射抽出来。但抽出来之后你会发现新的抽象又带来了新的学习成本和维护成本。2.2 六步连接模板每步里的雷你都踩过我们把 raw JDBC 的固定流程拆成六步逐个看你到底踩过多少坑步骤固定写法典型问题加载驱动Class.forName(com.mysql.jdbc.Driver)驱动类名随版本变化老代码经常 ClassNotFoundException建立连接DriverManager.getConnection(url, user, password)每次请求都新建物理连接性能极差创建声明conn.createStatement()/prepareStatement(sql)稍不注意就 SQL 拼接注入风险执行 SQLexecuteQuery(sql)/executeUpdate(sql)动态条件拼 SQL 极其痛苦处理结果while (rs.next())手动取值字段一变这里跟着全改释放资源rs.close(); stmt.close(); conn.close()漏一个连接池就慢慢消失关于资源释放我吃过一次大亏。在某次线上故障排查中数据库连接数一直飙升重启后几分钟又涨满。最后查下来某个查询方法的ResultSet在异常路径上没有关闭连接没有归还到连接池。更麻烦的是这种问题不是每次都复现要看那台机器的 GC 和并发情况。在 JDBC 时代这种问题每个团队几乎都遇到过一次——模板代码里面最不起眼的关闭资源四步恰恰是整个链路里最容易出事的地方。2.3 为什么说连接管理和异常处理是 JDBC 的两座大山如果只是重复JDBC 还有个更大的问题连接管理和异常处理的边界模糊。物理连接的创建和销毁是昂贵的。每次DriverManager.getConnection都要经过 TCP 握手、认证、分配资源一次几毫秒到几十毫秒不等。在高并发场景下每次请求都新建连接数据库很快就会被拖垮。所以有了连接池比如 C3P0、DBCP、后来的 HikariCP。但连接池的引入又带来新的样板代码——从池里借用连接、归还连接、处理连接失效……这些虽然比直连好但依然是每个 DAO 方法都要关心的细节。异常处理更痛苦。SQLException是 checked exceptionJava 编译器强制你处理。于是每个方法都要 try-catch或者往上层抛。问题是SQLException 的语义太粗了连接失败、SQL 语法错误、约束冲突、唯一键重复全是同一个异常类。你要么用getSQLState()去猜错误码要么在业务层再包一层自定义异常。不管怎么弄都是在本来就厚的样板代码上再摞一叠。这就是 raw JDBC 的核心困境它把数据库访问的技术复杂度完全暴露给开发者。你不仅要懂业务 SQL还要懂网络、连接池、异常处理、资源管理。对老手来说这也是一大堆心事对新手来说基本是劝退。3. MyBatis 到底解决了什么把 SQL 带回主舞台3.1 核心设计逻辑只关心 SQL 本身MyBatis 出现的背景就是受够了上面这一整套模板代码。它的核心思路用一句话就能概括让开发者只关心 SQL 本身连接管理、资源释放、参数设置、结果集映射这些琐事框架全部接管。这是一次思路上的转变。JDBC 的思维是你告诉我怎么连、怎么执行、怎么处理MyBatis 的思维是你只要告诉我 SQL 是什么、参数是什么、结果要映射成什么对象剩下的我来。拿上面按 ID 查询用户的例子在 MyBatis 里你会这样写select idfindById resultTypecom.example.User select id, name, email, created_at from user where id #{id} /select对应的 Java 接口方法只需要一行User findById(Param(id) Long id);六个步骤缩减成了两个核心表述SQL 长什么样、结果映射成什么。连接谁去管框架管。参数怎么传框架管。结果怎么转换成 User 对象框架管。你不用再写DriverManager.getConnection不用再写finally { rs.close(); }更不用手动一行行rs.getLong(id)。这块设计我特别想强调一点#{} 参数占位符。在 JDBC 里你写ps.setLong(1, id)在 MyBatis 里你只需要写#{id}。框架在背后帮你做了类型判断、PreparedStatement 参数填充、类型转换。动态 SQL 更是 JDBC 时代不敢想象的东西——if、where、foreach这些标签直接让根据条件拼 SQL这种事从地狱级痛苦变成了配置文件里的几百字节。3.2 三种写法的代码量对比同样功能的边界我们做一个量化对比。同样是按 ID 查用户并转成对象raw JDBC大约 30 行连接 预编译 执行 手动映射 资源释放MyBatis XML一个select标签 一个接口方法约 5 行MyBatis 注解一个接口方法上加Select(select ... from user where id #{id})约 3 行注意这是只算当前这个查询本身的代码。JDBC 那套连接管理代码在 30 行里占了大头但它会在每个 DAO 方法里重复。也就是说写 20 个查询JDBC 的总代码量不是 30 行乘以 20而是连接模板代码 每个查询的具体逻辑大部分是重复的机械代码MyBatis 则是每个查询只需要写真正跟业务相关的部分。几千行代码差距就是这么出来的。一个普通的后台管理系统如果有 100 张表、几百个 CRUD 操作用 MyBatis 比用 raw JDBC 少写的代码量是以万行计的。这不是夸张是实际经验。但我也要说句公道话MyBatis 并没有把代码完全消灭它只是把样板代码从 Java 代码里搬到了 XML 或注解里。你在 XML 里依然要写resultMap、要配置sql片段、要维护if分支。只是这些写起来比 Java 里的连接模板轻量得多而且集中在一起更容易维护。3.3 动态 SQL 和自动映射少掉的那些代码都去哪了如果你用过 MyBatis一定体会过动态 SQL 的爽感。举一个最常见的场景列表查询查询条件可能有用户名、可能有用状态、可能还要按创建时间过滤。在 raw JDBC 时代你要这样写String sql select * from user where 11; if (name ! null !name.isEmpty()) { sql and name name ; // 这里还带着注入隐患 } if (status ! null) { sql and status status; }这种字符串拼接写多了你会遇到两个经典问题一个是拼接条件过多时 SQL 混乱另一个是稍不留神就拼出一个语法错误。更别提有人习惯性地用字符串直接拼变量把 SQL 注入漏洞亲手送上线。在 MyBatis 里同样的逻辑是这样写的select idlistByCondition resultTypeUser select * from user where if testname ! null and name ! and name like concat(%, #{name}, %) /if if teststatus ! null and status #{status} /if /where /selectwhere标签会自动处理开头的 and 或 orif负责条件是否拼入 SQL。你不需要在 Java 代码里做任何字符串拼接也不会因为忘写空格或多写了 and 而报语法错误。这种体验上的提升比单纯少写代码更值得关注——它把容易出错的部分框架化了。自动映射也一样。table的created_at列自动对应 Java 对象的createdAt属性配好驼峰映射后不用一个个写 getter 和 setter。那几百行rs.getXxx()赋值代码直接就消失了。MyBatis 做的是列名到属性名的映射你只要保证两者对得上或者写一个resultMap兜底剩下全是体力活。4. MyBatis 也逃不掉的新样板代码问题4.1 XML 配置的负担mybatis-config.xml 和 mapper 的 boilerplate框架解决问题的方式是把问题转移到自己定义的配置层。MyBatis 也有自己的样板代码集中在 XML 配置里。首先是mybatis-config.xml。每个项目都要写一遍几乎雷同的设置数据源、事务管理器、mapper 扫描路径、驼峰映射开关、日志实现。这些配置不复杂但格式固定、不写不行。换个项目这些 XML 片段往往是从老项目里直接复制的。其次是 mapper XML 里每个语句的骨架。举个例子insert idinsertUser parameterTypeUser insert into user (id, name, email) values (#{id}, #{name}, #{email}) /insert这种insert固定结构写 100 个表就要写 100 遍。如果想再要主键回填还得多加几个子元素insert idinsertUser useGeneratedKeystrue keyPropertyid有经验的开发会写一个代码生成器自动生成 mapper 接口、XML、实体类。把样板代码从手写变成生成。这算是 MyBatis 社区应对样板代码的主流方式——既然这些代码结构固定就让工具去生成人只改生成后的业务部分。这引出一个观点完全消灭样板代码是不现实的你能做的是把样板代码交给工具和框架处理。MyBatis 把 JDBC 的连接样板消除了但引入了 XML 配置样板代码生成器把 XML 样板消除了但生成出来的代码仍然需要人工 review。每一层抽象都在把某些重复代码收编但永远有新的重复待处理。4.2 高频翻车点TypeHandler、二级缓存、param 索引从热搜词里能看到大家最关心的 MyBatis 问题集中在几个点TypeHandler、二级缓存、参数索引、初始化流程。这些都是框架替你干活但干得不对时非常难查的代表。TypeHandler的具体作业流程是查询时它把 JDBC 的ResultSet里的值转成 Java 类型写入时它把 Java 参数转成 JDBC 类型。默认的 TypeHandler 覆盖了常用类型但遇到枚举、JSON、自定义业务对象时你得自己写一个 TypeHandler。我第一次自定义 TypeHandler 的时候踩过一个坑setParameter和getResult里都要处理 null 值。结果查询正常、插入的时候总是报未知列类型。查了半天发现是没有在setParameter里处理parameter null的情况PreparedStatement 不知道该用什么类型去 setNull。这东西看起来不起眼但它确实是 MyBatis 定制化开发里很典型的隐藏问题。二级缓存更是重灾区。MyBatis 的二级缓存默认是 namespace 级别的也就是说同一个 mapper 里的查询结果会缓存下来。听起来很好但问题在于如果两个 mapper 操作了同一张表二级缓存会不同步。你删一条数据是用UserMapper的接口但查询缓存是放在UserMapper下的另一个 mapper 的更新操作不会自动通知它。结果就是用户删了数据列表还是旧数据。这是让很多人头疼的经典缓存一致性问题。param 索引是另一个高频问题。MyBatis 参数绑定有两种写法#{param1}、#{param2}或者Param(id)。在很多老代码里看到#{param1}这种写法它的问题在于一旦你调整了方法参数顺序SQL 里的引用全乱。更麻烦的是如果你的参数恰好是List或数组配合foreach时索引会更容易懵。写代码的时候我强烈建议明确使用Param注解别贪省事用默认索引——这个习惯能在半年后帮你省掉至少三个小时的排查时间。4.3 源码角度初始化流程里最容易被忽略的环节热搜词还提到了mybatis 中初始化工作流程和基于 XML 配置的初始化工作原理。这确实值得看一眼。MyBatis 启动初始化大致是读取mybatis-config.xml创建Configuration对象加载所有 mapper 映射文件解析每个select、insert、update、delete、resultMap为每个 SQL 语句创建MappedStatement注册到 Configuration 中根据配置创建Executor、SqlSession等核心对象这里面最容易忽略的环节是 MappedStatement 的注册顺序和 id 唯一性。MyBatis 里每个 SQL 语句的 id 是 namespace id 拼接的必须全局唯一。很多人在复制粘贴 XML 时忘了改 namespace或者两个 mapper 的select用了同一个 id启动时才会发现冲突。还有一点是 XML 中sql片段和include的组合。当你的系统有很多公共查询条件比如逻辑删除标记、租户隔离条件你会想把它们抽成sql片段。但抽多了以后片段之间相互引用会让整个 XML 变得难以阅读加上不同环境开发、测试、生产的数据源配置占位符如果不一致启动时也能直接报错。从源码层面理解初始化流程最大的价值是遇到启动时 DAO 方法不生效新版驱动下 MyBatis 连不上数据库这类问题时你能快速定位是配置解析的问题、驱动加载的问题还是 SQL 映射的问题。这比死记面试题要有用得多——面试题问初始化流程你背一遍就算了线上报错时你能不能顺着堆栈找到XMLConfigBuilder和XMLStatementBuilder才是真本事。5. 生产力之争raw JDBC 和 MyBatis 的选择建议5.1 实际选型不是谁更好而是你能 hold 住谁的复杂度现在回到标题里的 productivity killer。我的观点是真正杀死生产力的不是技术选型本身而是选型与需求规模不匹配。先看什么样的情况适合用 raw JDBC项目极小比如一个工具脚本、一个内部小报表只有两三个 SQL引入 MyBatis 反而显得重量级数据库操作极度简单没有复杂的动态条件、没有复杂的对象映射或者你在写一个性能极其敏感的批量处理程序希望绕开框架反射和映射带来的开销完全掌控 SQL 执行路径对于这类场景raw JDBC 的样板代码是可控的。写两个 DAO、每个方法四五十行半天搞定引入 MyBatis 反而要配 XML、配驼峰映射、配插件花费更多时间。这是杀鸡用牛刀的问题。不过即使这种情况我也建议至少用 Spring 的JdbcTemplate或 DbUtils 这类轻量封装把连接管理样板消掉一部分——裸 JDBC 连我自己都不想写第二次。而一旦你进入以下任一场景建议直接上 MyBatis业务表超过 20 张CRUD 操作几十上百个SQL 经常需要根据条件动态拼接团队多人协作需要一个相对统一的数据访问风格我刚才说过MyBatis 的 XML 本身也会有样板代码resultMap、sql 片段、分页 SQL 重复但这些大多是生成一次、改时再动。而 raw JDBC 的样板代码是每个方法都从头写一遍。代数量级完全不同。5.2 关于 MyBatis-Plus 和二三级缓存进阶开发者常纠结的问题很多团队现在直接用 MyBatis-Plus。它提供BaseMapper连 XML 里最简单的 CRUD 都帮你写好了public interface UserMapper extends BaseMapperUser { }然后你就可以用userMapper.selectById(id)、userMapper.selectList(wrapper)。连select都不用写了实体类的字段会自动映射成列名。这块相当于把 MyBatis 的新样板代码又收敛了一层。说实话对于普通后台管理系统的增删改查MyBatis-Plus 的体验确实好很多。但 MyBatis-Plus 也不是没有问题它的Wrapper链式写法虽然有文档但当查询条件复杂时可读性和动态性反而不如手写 XML 清晰。尤其是多表 join、子查询、复杂 group byMyBatis-Plus 的通用方法作用有限你还是得写自定义 SQL。我的经验是BaseMapper 负责 80% 的常规 CRUD自定义 SQL 负责剩下的 20% 复杂查询。这样既保留了框架的效率又没有丢掉 MyBatis 最强的动态 SQL 能力。二级缓存这块我的建议是保守使用。MyBatis 默认开启的是 SqlSession 级别的一级缓存二级缓存在多个 SqlSession 之间共享但一致性维护麻烦。除非你非常清楚自己的数据变更频率和缓存失效场景否则宁可不开。我见过不止一个项目因为开了二级缓存在逻辑删除 多表关联的组合操作里缓存错乱最后只能清掉重来。缓存是优化手段不是救命稻草业务正确性永远是第一位。5.3 不要神化框架也不要妖魔化样板代码最后说点掏心窝的话。很多人有个误解觉得用了 MyBatis 就不用写 SQL 了。这恰恰是反的——MyBatis 把你从怎么写连接代码的琐事里解放出来是为了让你把精力花在怎么写好 SQL上。SQL 性能优化、索引设计、事务边界这些依然是核心能力。MyBatis 解放的是生产力不是思维。样板代码本身也不全是坏的。它的存在其实保证了程序结构的一致性。JDBC 时代你复制粘贴 50 遍连接管理代码虽然烦但至少每个方法的结构都是规范的——只要最后一遍不漏资源。框架把这些样板收编之后程序结构确实变简洁了但对开发者的要求反而变了你得理解框架的约定、理解参数映射规则、理解缓存失效条件。你不再被样板代码困扰但你被框架概念困扰。所以不管你是刚入门在学 raw JDBC还是工作中天天写 MyBatis我给你的建议都是同一句话先把底层的样板代码亲手写一遍理解它每一步在干什么然后再去用框架。亲手写过Connection、PreparedStatement、ResultSet的完整生命周期之后你再看到 MyBatis 的SqlSessionTemplate、MappedStatement、Executor这些概念会觉得它们一点都不神秘——它们就是把你当年手写的那套流程做得更标准、更高效、更可维护而已。说句实在的我到现在都记得第一次用 MyBatis 时的感觉原来查一个用户真的只需要写一条 SQL。那种从生产线式的模板代码里解放出来的爽快感是驱动我持续研究其背后原理的直接动力。也希望读这篇文章的你能先踩一遍 raw JDBC 的泥泞再去感受框架的轻快——那样的体验会完全不同。
返回列表