首页 / 资讯中心 / 文章详情

从JDBC到自研ORM:反射与注解构建可复用数据访问层

从JDBC到自研ORM:反射与注解构建可复用数据访问层 ★ FEATURED ARTICLE
简介一份面向Java开发者的PDF教程聚焦利用Spring JDBC的设计理念手写定制ORM框架适合有一定基础、想深入理解持久层原理的工程师也适合为后续学习MyBatis做铺垫。教程从原生JDBC的典型操作入手逐步拆解加载驱动、建立连接、创建语句集、执行语句、获取结果集、关闭资源六个步骤并通过抽取mapperRow()方法把结果集到实体的映射逻辑独立出来减少重复代码。内容还延伸到Spring JdbcTemplate的二次开发讲解事务管理、参数绑定、异常处理、结果集映射、批处理等关键机制帮助读者理解ORM框架的设计思路同时为自定义轻量级ORM组件提供可落地的代码参考。资源包内仅包含1个PDF文档压缩后大小约940KB轻量易查阅目前已有178人学习下载对于想要掌握数据库访问层封装技巧的开发者具有一定参考价值。1. 基于 Spring JDBC 手写定制自己的 ORM 框架从六步 JDBC 到可复用的数据访问层基于 Spring JDBC 手写定制自己的 ORM 框架听起来像是重复造轮子但当你把 ResultSet 到对象的映射、SQL 拼接、分页这些散落代码收拢成统一接口后再去看 MyBatis 的会话和结果映射会突然通透很多。这份资源从 JDBC 六步操作讲起用反射和注解把表字段与实体属性绑定再设计 BaseDao、QueryRule、Page、ResultMsg 这一套基础组件沉淀成一个可以在小团队里直接用的轻量 ORM 骨架。它适合会用 MyBatis 但说不清底层原理的开发者也适合想统一团队数据访问层、降低学习成本的场景。跟着代码走一遍你收获的不只是一套源码更是一条从 JDBC 到 ORM 的完整推导路径。核心思路其实很朴素把不变的部分收进模板把变化的部分交给映射剩下的技术细节全是围绕这两件事展开的。2. 从 ResultSet 说起JDBC 六步模板里的重复到底在哪课程材料把第一步点得很直白ResultSet 是最熟悉的陌生人。每个 Java 服务端开发都写过 JDBC 查询但很少有人停下来数一数每一次查询里哪些代码是必须变的。我数下来其实只有两处SQL 本身以及 ResultSet 到对象的赋值。剩下的加载驱动、建立连接、创建语句集、执行、关闭全是固定的流程。Spring JdbcTemplate 的设计思想就是把固定流程收进模板把变化的部分交给 RowMapper 回调。2.1 六步 JDBC 模板哪一步是可以抽走的先看材料里那段原生 JDBC 查询。它把六步走得非常完整代码结构也代表了绝大多数老项目的真实状态private static ListMember select(String sql) { ListMember result new ArrayList(); Connection con null; PreparedStatement pstm null; ResultSet rs null; try { // 1、加载驱动类 Class.forName(com.mysql.cj.jdbc.Driver); // 2、建立连接 con DriverManager.getConnection( jdbc:mysql://127.0.0.1:3306/demo_db?characterEncodingUTF-8, root, 123456); // 3、创建语句集 pstm con.prepareStatement(sql); // 4、执行语句集 rs pstm.executeQuery(); // 5、获取结果集并封装对象 while (rs.next()) { Member instance new Member(); instance.setId(rs.getLong(id)); instance.setName(rs.getString(name)); instance.setAge(rs.getInt(age)); instance.setAddr(rs.getString(addr)); result.add(instance); } } catch (Exception e) { e.printStackTrace(); } finally { // 6、关闭结果集、语句集、连接 try { rs.close(); pstm.close(); con.close(); } catch (Exception e) { e.printStackTrace(); } } return result; }这里有一个很多人没注意的细节finally 块无条件调用 rs.close()可一旦第 2 步连接失败rs 还是 nullclose 直接抛空指针。原生 JDBC 的重复不只是代码多连异常处理都容易写错。把六步动作列成一张表责任就一目了然步骤动作是否需要每个业务都写1Class.forName 加载驱动否连接池初始化一次即可2DriverManager.getConnection否交给 DataSource3prepareStatement否模板方法统一处理4executeQuery否模板方法统一处理5ResultSet 取值并 set 到实体是这是 ORM 的核心价值6关闭 rs/pstm/con否模板方法统一处理结论很明确第 5 步才是每个 DAO 之间真正不同的地方也是后来所有 ORM 框架争夺的主战场。JdbcTemplate 的做法是连接和语句由模板统一管理映射交给 RowMapper 回调使用者只写 SQL 和 mapRow 方法。我一般会自己封装一个 JdbcTemplate 风格的基类把 getConnection、prepareStatement、executeQuery、close 全部收进去业务代码里永远看不到 Connection。这样即使不引入完整 ORM日常开发也能少掉一半样板代码。2.2 mapperRow() 分离只解决了第一个问题材料里的第一次改造是抽出一个 mapperRow() 方法把 ResultSet 的取值动作从查询方法里挪走private static Member mapperRow(ResultSet rs, int i) throws Exception { Member instance new Member(); instance.setId(rs.getLong(id)); instance.setName(rs.getString(name)); instance.setAge(rs.getInt(age)); instance.setAddr(rs.getString(addr)); return instance; }这个抽法解决了“查询方法看起来干净了”但没解决复用。换一个 Order 实体你要再写一个 Order 的 mapperRow换十张表就写十个。真正的问题从写重复代码变成了写重复的映射代码。Spring JdbcTemplate 用 RowMapper 接口解决这个问题它的签名也是 mapRow(ResultSet rs, int rowNum)只不过把返回值从 Member 换成泛型 T。使用者仍然要写每一行的取值逻辑但至少可以在不同 SQL 里共用同一个 RowMapper。再到 MyBatis干脆把这段也自动化了靠反射和结果映射规则完成赋值。2.3 反射加注解驱动让一张表变成一个实体映射材料里的第二次改造是核心转折不再为每个实体写 mapperRow而是通过反射读取类上的注解动态生成 SQL 和映射关系。先看实体类Entity Table(name t_member) public class Member implements Serializable { Id private Long id; private String name; private String addr; private Integer age; // getter/setter 省略 }类名和表名不一致时用 Table(name t_member) 指定字段名和列名一致时什么都不用加。这个约定在后面会对应到反射代码里。然后是一个通用的 select(Object condition)private static List? select(Object condition) throws Exception { ListObject result new ArrayList(); Class? entityClass condition.getClass(); Connection con null; PreparedStatement pstm null; ResultSet rs null; try { con DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD); // 根据列名找属性名、根据属性名找列名建立双向映射 MapString, String columnMapper new HashMap(); MapString, String fieldMapper new HashMap(); Field[] fields entityClass.getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); String fieldName field.getName(); Column column field.getAnnotation(Column.class); if (column ! null) { columnMapper.put(column.name(), fieldName); fieldMapper.put(fieldName, column.name()); } else { columnMapper.put(fieldName, fieldName); fieldMapper.put(fieldName, fieldName); } } Table table entityClass.getAnnotation(Table.class); StringBuilder sql new StringBuilder(select * from table.name()); StringBuilder where new StringBuilder( where 11 ); // 用传入对象的非空属性拼动态 where for (Field field : fields) { Object value field.get(condition); if (null value) continue; if (String.class field.getType()) { where.append( and ).append(fieldMapper.get(field.getName())) .append( ).append(value).append(); } else { where.append( and ).append(fieldMapper.get(field.getName())) .append( ).append(value); } } System.out.println(sql where.toString()); pstm con.prepareStatement(sql where.toString()); // 执行查询 rs pstm.executeQuery(); int columnCount rs.getMetaData().getColumnCount(); // 按列名反射赋值生成目标对象 while (rs.next()) { Object instance entityClass.getDeclaredConstructor().newInstance(); for (int i 1; i columnCount; i) { String columnName rs.getMetaData().getColumnName(i); Field field entityClass.getDeclaredField(columnMapper.get(columnName)); field.setAccessible(true); field.set(instance, rs.getObject(columnName)); } result.add(instance); } } finally { // 关闭资源生产环境建议 try-with-resources if (rs ! null) rs.close(); if (pstm ! null) pstm.close(); if (con ! null) con.close(); } return result; }提示源码实体类用了 Lombok Data我这里为排版省略了 getter/setter。在 IDEA 里跑 Demo 前先确认 Lombok 插件已安装否则编译会报找不到方法。这段代码把 ORM 的三个核心动作串起来了。第一读取实体 Class 上的 Table 和 Column 注解维护 columnMapper 和 fieldMapper 两个 MapcolumnMapper 负责数据库列名到实体字段名的正向查找fieldMapper 负责实体字段名到列名的反向查找。生成 where 时需要 fieldMapper映射结果时需要 columnMapper少了任何一边都别扭。第二遍历传入对象的字段取非空值拼进 where。注意这里为了演示直观把值直接拼进了 SQLString 加单引号、数值不加。这个写法有两个问题一是单引号内容如果包含引号或特殊字符SQL 直接崩二是存在注入风险。课程下半部分会改成占位符方式也就是 where 里只写 and column ?值统一放进参数列表。第三rs.getMetaData().getColumnCount() 是从数据库元数据拿到当前结果集有多少列然后逐列取列名再用 columnMapper 找到实体字段反射赋值。这里的 getColumnName 有个隐藏坑SQL 一旦用了别名它返回的可能是原始列名而不是别名映射就会落空第 5 章会展开。在我看来这一段已经不是一个工具方法而是一个最小可用的 ORM 引擎。它把“表结构信息”理解为 Class 上的注解把“一行数据”理解为字段反射赋值把“查询条件”理解为对象非空属性。后面所有框架的复杂设计本质上都是从这条链路生长出来的。3. 接口先行BaseDao、QueryRule 与分页模型怎么定反射版本跑通后你会发现单表查询已经能做得很通用。但自己用可以交给团队不行。材料里的自研框架没有一上来就写实现而是先把数据访问层的接口模型定死继承一个 BaseDaoT, PK查询条件用 QueryRule分页用 Page统一返回用 ResultMsg。这个顺序很关键先定契约再写实现团队里的人用起来才不需要猜。这份资源是系列第 1 部分重点恰好就是这套接口模型。3.1 BaseDao 接口为什么查询返回 List 原生 SQL 返回 ListBaseDao 的接口模型在材料里给了完整版本核心方法可以压缩成下面这些public interface BaseDaoT, PK { // 根据条件查询列表 ListT select(QueryRule queryRule) throws Exception; // 根据条件查询分页 Page? select(QueryRule queryRule, int pageNo, int pageSize) throws Exception; // 原生 SQL 查询返回 Map适合 join 或报表类 SQL ListMapString, Object selectBySql(String sql, Object... args) throws Exception; // 原生 SQL 分页 PageMapString, Object selectBySqlToPage(String sql, Object[] param, int pageNo, int pageSize) throws Exception; boolean delete(T entity) throws Exception; int deleteAll(ListT list) throws Exception; PK insertAndReturnId(T entity) throws Exception; boolean insert(T entity) throws Exception; int insertAll(ListT list) throws Exception; boolean update(T entity) throws Exception; }注意几个设计细节。select(QueryRule) 返回 List 语义是实体查询selectBySql 返回 ListMapString, Object语义是任意 SQL 查询。当查询结果没有对应实体时做成 Map 是最妥协也最灵活的方案。这比硬塞一个 T 要实在因为 join 出来的列名、类型五花八门实体很难覆盖。insertAndReturnId 返回 PK 而不是 boolean因为回填自增 ID 在很多业务里是刚需。delete 返回 boolean、deleteAll 返回 int是在语义上区分“是否成功”和“影响了几行”。这套接口设计整体偏实用没有过度抽象。方法声明全部 throws Exception作为教学演示可以接受但生产环境我更建议模仿 Spring把底层异常统一转换成 DataAccessException 之类的运行时异常这样业务代码不用到处 try catch事务回滚语义也更清晰。3.2 QueryRule用条件 DSL 代替散装字符串拼接QueryRule 是这套框架里我最喜欢的一个设计。它不直接拼 SQL而是把查询条件描述成结构化对象public final class QueryRule implements Serializable { public static final int ASC_ORDER 101; public static final int LIKE 1; public static final int IN 2; public static final int BETWEEN 4; public static final int EQ 5; public static final int GT 7; public static final int GE 8; public static final int AND 201; public static final int OR 202; private ListRule ruleList new ArrayList(); private QueryRule() {} public static QueryRule getInstance() { return new QueryRule(); } public QueryRule andEqual(String propertyName, Object value) { ruleList.add(new Rule(EQ, propertyName, new Object[]{value}).setAndOr(AND)); return this; } public QueryRule andLike(String propertyName, Object value) { ruleList.add(new Rule(LIKE, propertyName, new Object[]{value}).setAndOr(AND)); return this; } public QueryRule andIn(String propertyName, Object... values) { ruleList.add(new Rule(IN, propertyName, values).setAndOr(AND)); return this; } public QueryRule andBetween(String propertyName, Object... values) { ruleList.add(new Rule(BETWEEN, propertyName, values).setAndOr(AND)); return this; } public QueryRule addAscOrder(String propertyName) { ruleList.add(new Rule(ASC_ORDER, propertyName)); return this; } }每次调用 andXxx 都往 ruleList 里塞一个 RuleRule 里记录四样东西操作符类型、属性名、参数值、以及它是 AND 还是 OR。之后由一个 SQLBuilder 遍历 ruleList把属性名转成列名把操作符转成 SQL 片段把参数值收集进数组。业务层写代码时是链式调用到执行层才真正面对 PreparedStatement。这个设计最大的收益是把“条件要从哪来”和“条件怎么写进 SQL”解耦了。业务层不需要关心拼接顺序也不容易漏掉 where 11 这种历史包袱。我后来在项目里用类似方式封装过多表查询比在 service 里堆 if 拼字符串好维护太多。看一眼就理解为什么 if 拼字符串不好同样的 and name 某个值有人写单引号有人写双引号有人忘了转义。QueryRule 把操作符和参数分离这些问题从根上消失了。当然它也有代价复杂子查询比较难表达材料里用 queryRuleList 支持嵌套但太深的条件组合还是直接写 SQL 更稳。3.3 Page 与 ResultMsg统一分页和返回结构分页对象 Page 的核心字段不多start 代表当前页第一条数据在数据库中的偏移量pageSize 每页条数rows 当前页数据total 总记录数。它的关键计算都围绕这四个字段展开public class PageT implements Serializable { private int pageSize 20; private long start; private ListT rows; private long total; public Page() { this(0, 0, 20, new ArrayListT()); } public Page(long start, long totalSize, int pageSize, ListT rows) { this.pageSize pageSize; this.start start; this.total totalSize; this.rows rows; } public long getTotalPageCount() { return total % pageSize 0 ? total / pageSize : total / pageSize 1; } public long getPageNo() { return start / pageSize 1; } public static int getStartOfPage(int pageNo, int pageSize) { return (pageNo - 1) * pageSize; } }getStartOfPage 是分页 SQL 里 limit 偏移量的计算入口。比如第 2 页、每页 10 条偏移量是 10。Page 没有直接暴露 start 给前端而是通过 getPageNo 换算成页码符合前端表格组件的使用习惯。ResultMsg 就更简单了status、msg、data 三个字段是所有控制层返回的统一外壳public class ResultMsgT implements Serializable { private int status; // 状态码 private String msg; // 状态码解释 private T data; // 业务数据 // 多种构造函数与 getter/setter 省略 }这套东西严格说不是 ORM 的一部分但做框架时必须带上。后端接口如果没有统一返回结构前端处理成功失败就得猜监控也不好埋点。材料里专门提到 Page 能对接 Bootstrap Table说明字段命名就是照着前端组件设计的这个细节值得学。4. 从原理到落地注解解析、SQL 生成与结果集映射的完整链路前面的接口模型是骨架这一章把血和肉补上。以单表查询为目标把反射 ORM 的实现拆成三个可独立测试的环节字段映射、SQL 生成、结果集赋值。下面每个环节都是可以直接跑通的最小代码。4.1 建立字段与列的映射一张表对应两张 Map不管是生成 SQL 还是映射结果第一步都是把实体类信息读出来。常见做法是首次访问时解析一次把结果缓存住避免每次查询都反射一遍。核心解析逻辑如下MapString, String columnMapper new HashMap(); MapString, String fieldMapper new HashMap(); Field[] fields entityClass.getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); String fieldName field.getName(); Column column field.getAnnotation(Column.class); if (column ! null) { columnMapper.put(column.name(), fieldName); fieldMapper.put(fieldName, column.name()); } else { columnMapper.put(fieldName, fieldName); fieldMapper.put(fieldName, fieldName); } }columnMapper 的 key 是数据库列名value 是实体属性名fieldMapper 刚好相反。为什么要两份因为 SQL 生成方向是属性名到列名结果映射方向是列名到属性名两个方向用同一个 Map 都要做一次反向查找味道不对。这里还有个约定问题。如果实体字段名是 userId数据库列名是 user_id默认没写 Column 就映射不上。我一般会在框架里内置一个驼峰转下划线逻辑字段名默认转成下划线再去找列再放开 Column 覆盖。这样既省注解又能照顾特殊命名。4.2 动态 where 与参数绑定先能跑再防注入拿到映射关系后根据传入条件对象生成动态 where。教学版写法如下StringBuilder sql new StringBuilder(select * from table.name()); StringBuilder where new StringBuilder( where 11 ); ListObject args new ArrayList(); for (Field field : fields) { Object value field.get(condition); if (value null) { continue; } String dbColumn fieldMapper.get(field.getName()); if (String.class field.getType()) { where.append( and ).append(dbColumn).append( ?); } else { where.append( and ).append(dbColumn).append( ?); } args.add(value); }这段代码和材料里的版本有两点变化。一是把 String 拼单引号改成统一占位符 ?值全部收进 args 列表二是示例里把 int 类型当成 条件业务上通常是按年龄区间过滤。实际框架里应该由 QueryRule 来声明 EQ、GT、GE 这些操作符而不是靠字段类型猜。参数绑定是这套逻辑里不能省的一步pstm con.prepareStatement(sql where.toString()); for (int i 0; i args.size(); i) { pstm.setObject(i 1, args.get(i)); }setObject 让 PreparedStatement 自己做类型适配Integer、String、Date 都能处理比自己拼字符串省心。如果后面做批量插入连接串记得加上 rewriteBatchedStatementstrueMySQL 才能把多条 insert 合并成一条 multi-values 发送性能差距很大。这个环节最容易翻车的地方是条件顺序和占位符顺序不一致。建议用同一个 List 同时装值和 SQL 片段保证 and column ? 和 args.add(value) 总是成对出现别在两个地方分别维护。4.3 结果集映射getMetaData、getColumnName 与反射赋值查询执行完结果集赋值是实现 ORM 的临门一脚。教学版代码很短但包含两个关键点int columnCount rs.getMetaData().getColumnCount(); while (rs.next()) { Object instance entityClass.getDeclaredConstructor().newInstance(); for (int i 1; i columnCount; i) { String columnName rs.getMetaData().getColumnName(i); Field field entityClass.getDeclaredField(columnMapper.get(columnName)); field.setAccessible(true); field.set(instance, rs.getObject(columnName)); } result.add(instance); }columnCount 是从元数据拿到的不是写死的列数所以 select * 返回多少列它就能处理多少列。columnMapper.get(columnName) 把数据库列名翻译成实体属性名再 getDeclaredField 找到属性。这里有一个顺序容易搞反是先根据列名找到属性再给属性赋值而不是反过来。getObject 返回的类型取决于 JDBC 驱动。MySQL 的 bigint 可能返回 Longnumeric 可能返回 BigDecimal如果实体属性是 Integer某些驱动下会抛类型转换异常。成熟框架会把类型转换单独做一层教学版就先不做。getColumnName 和 getColumnLabel 的区别留到第 5 章展开。5. 避坑指南自写 ORM 时最容易忽略的五个工程细节手写 ORM 最难受的不是写不出来而是写出来之后在真实环境里各种玄学报错。下面五条都是我在类似 Demo 和项目里踩过的坑按现象到原因到解决列出来照着排查能少走很多弯路。5.1 finally 块里 rs.close() 空指针现象连接或语句创建失败时finally 块直接抛 NullPointerException原始异常被掩盖日志里看到的全是空指针。原因rs、pstm、con 三个变量只在 try 块里赋值一旦前面某一步挂了后面的变量还是 null。finally 里无条件调用 rs.close()等于把空指针又提了一层。解决最省事的是用 try-with-resourcesJava 7 之后 Connection、PreparedStatement、ResultSet 都实现了 AutoCloseabletry (Connection con dataSource.getConnection(); PreparedStatement pstm con.prepareStatement(sql); ResultSet rs pstm.executeQuery()) { while (rs.next()) { // 不需要手动 close } }如果项目被迫用 Java 6至少在 finally 里加空判断关闭顺序按 rs、pstm、con 从里到外。5.2 查询 SQL 用了别名映射全部落空现象SQL 写成 select name as n from t_member跑完发现实体里所有字段都是 null日志没有任何报错。原因getColumnName() 从元数据拿的是底层列名不是查询里的别名。select name as n 的结果集元数据里列名仍然是 namen 只能通过 getColumnLabel() 拿到。代码用 getColumnName 去 columnMapper 里找找到的列名和 map 里的 key 对不上getDeclaredField 传进去就是 null映射自然失败。解决如果是自己的封装框架统一约定 select 语句不使用别名或者把取值逻辑改成 getColumnLabel()。大多数 RowMapper 实现用的是 getColumnLabel因为要兼容别名场景。建议框架里直接从 getColumnLabel 取这样两种情况都覆盖。5.3 私有字段反射赋值报 IllegalAccessException现象运行到 field.set(instance, value) 时抛出 IllegalAccessException提示无法访问私有属性。原因getDeclaredField 能拿到私有字段但不代表可以直接读写。在 Java 8 和 Java 11 里不调用 setAccessible(true) 就赋值必然报错Java 17 强封装之后普通反射连 setAccessible(true) 都可能被安全模块拦下来。解决每个 Field 在使用前都要执行 field.setAccessible(true)而且不要每次反射都从头查 Field。把 Field 对象缓存到 Map 里按实体 Class 维度复用private static final MapString, Field[] FIELD_CACHE new ConcurrentHashMap(); Field[] fields FIELD_CACHE.computeIfAbsent( entityClass.getName(), k - entityClass.getDeclaredFields());批量插入十万条数据时反射开销会被放大缓存 Field 能有效降低耗时。另外生产环境如果跑 JDK 17框架启动参数里可能还要加 --add-opens java.base/java.langALL-UNNAMED 这类配置这是升级 JDK 时最容易踩的坑。5.4 字符串拼接 where 导致 SQL 注入和单引号报错现象业务参数里带一个单引号SQL 执行直接报语法错误带 OR 11数据被全表查出来。原因教学版为了展示 ORM 原理把值直接拼进 SQL也没有走 PreparedStatement。真实业务如果照抄等于给攻击者开了一扇门。解决规则就一条所有外部输入必须走占位符。where 片段只拼 and column ?值统一交给 setObject 绑定。Like 查询的 % 也作为参数传进去不要拼在 SQL 里否则参数本身包含 % 时语义会变。我在项目里定过一条硬规矩DAO 层禁止出现字符串形式的 SQL 拼接统一走 QueryRule 或者参数数组。这条规矩帮我挡住过好几次线上事故属于那种看着不起眼但能救命的规定。5.5 自定义类放进 javax 包升级 JDK 时直接翻车现象框架跑在 JDK 8 上没问题升级到 JDK 11 或 17 后编译或运行时频繁出现模块校验错误查了很久才发现问题在自定义包名。原因材料里把 Page、ResultMsg 放在了 javax.core.common 这个包下面。javax 是 JDK 保留命名空间和 javax.servlet、javax.persistence 这类模块冲突时自定义类会被模块系统直接拒绝加载。解决立即把自定义类挪到自己的业务包名下面比如 com.demo.core.common、com.demo.orm.framework。这是改一行就能解决的后悔药越早改越省心。如果你从这套源码里拿到工程第一件事就是全局搜索 javax.core.common批量替换包名和 import再跑一遍测试确认没漏。6. 验证方法用一组测试把 SELECT 跑通再看框架和 JdbcTemplate 的差距6.1 最小可运行验证拿到的源码如果只有接口和 QueryRule实现类不全可以先把第 2 章那套反射 select 抽出来做一次最小验证。我习惯这样组织测试public static void main(String[] args) throws Exception { Member condition new Member(); condition.setName(A同学); condition.setAge(19); List? result ReflectionOrmDemo.select(condition); System.out.println(Arrays.toString(result.toArray())); }跑起来后重点看三件事查询 SQL 是否按预期拼出了 where 条件返回对象的 name、age、addr 是否都正确赋值数据库连接的建立和关闭是否正常。6.2 与 Spring JdbcTemplate 的差异对照把自研版本和 JdbcTemplate 放在一起比差距看得更清楚维度Spring JdbcTemplate自研反射 ORM连接管理DataSource 统一接管需要自行接连接池SQL 执行模板固定流程固定流程但反射生成 SQL结果映射RowMapper 回调手写取值注解加反射自动赋值异常处理转换成 DataAccessException直接抛 Exception条件构造手写 SQL参数数组QueryRule 链式 DSL这张表就是后续改进的路线图自研框架先把连接管理交给现成连接池再把异常转成统一运行时异常最后考虑缓存 Field 和 SQL性能就能接近成熟框架。从那以后我每封装一层数据访问中间件都会强制走一遍“接口先行、条件 DSL、参数绑定、统一返回、连接池接管”这套检查清单尤其是 SQL 拼接那一步宁可多写一个 QueryRule也不让业务代码堆 if 拼字符串。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站