MiniSpring框架学习笔记-JDBC 访问框架: MiniBatis如何将 SQL 语句配置化?
2026/8/5 17:23:02 网站建设 项目流程

MiniSpring框架学习-JDBC 访问框架: MiniBatis如何将 SQL 语句配置化?

  • 15. MiniBatis:如何将 SQL 语句配置化?
    • 一、先用 MyBatis 建立直觉
    • 二、先看最终使用方式
    • 三、SQL 放到 Mapper XML
    • 四、MapperNode:XML 中一条 SQL 的内存模型
    • 五、SqlSessionFactory:启动时解析 XML
      • 1. 配置入口
      • 2. 初始化并扫描目录
      • 3. 解析 XML 并放入 Map
    • 六、SqlSession:按 sqlId 执行 SQL
    • 七、完整调用链
    • 八、当前实现边界

教程:https://github.com/YaleGuo/minis
极客时间:手把手带你写一个 MiniSpring

前言:这节源教程写的不错,清晰易懂,点赞!

15. MiniBatis:如何将 SQL 语句配置化?

前面几章里,JdbcTemplate已经帮我们收起了 JDBC 的固定流程:获取连接、创建PreparedStatement、绑定参数、关闭资源、转换异常。

但业务代码里还有一块东西很显眼:

finalStringsql="select id, name, birthday from users where id = ?";

SQL 直接写在 Java 代码里可以工作,但当 SQL 变长、变复杂,或者需要多人协作维护时,Java 类会越来越乱。所以这一节模仿 MyBatis 的一个核心思路:把 SQL 放到 XML 里,Java 代码只通过一个 SQL id 去执行它。

一、先用 MyBatis 建立直觉

在真实 MyBatis 里,最基础的调用可以写成这样:

try(SqlSessionsession=sqlSessionFactory.openSession()){Blogblog=session.selectOne("org.mybatis.example.BlogMapper.selectBlog",101);}

这段代码表面上只有两步:

打开一次 SqlSession ↓ 根据 statement id 执行一条 SQL

但它背后已经提前做过一件重要的事:MyBatis 会把配置文件和 Mapper XML 解析成内存里的元数据。比如下面这个 id:

org.mybatis.example.BlogMapper.selectBlog

通常可以拆成:

namespace = org.mybatis.example.BlogMapper id = selectBlog

然后 MyBatis 就可以用namespace + id找到 XML 中配置好的 SQL、参数信息和结果映射规则。

粗略地说,MyBatis 的设计逻辑是:

启动阶段 读取配置和 Mapper XML 把每条 SQL 解析成内存对象 用 namespace.id 建立索引 运行阶段 业务代码传入 statement id 和参数 框架找到对应 SQL 绑定参数 执行 JDBC 把 ResultSet 映射成对象

这里有个小细节:openSession()不是每次都重新加载 XML。XML 通常在SqlSessionFactory构建或初始化时已经解析好了;openSession()更像是打开一次执行会话,让后续的selectOne(...)有地方执行 SQL、管理执行过程。

这么做的好处很直接:

  1. Java 代码更干净,业务方法不用塞一大段 SQL;
  2. SQL 有独立位置,长 SQL、复杂 SQL 更容易阅读和维护;
  3. SQL 可以用namespace.id统一管理,不容易散落在各个 Service 里;
  4. Java 层表达“我要做什么”,XML 层表达“具体 SQL 怎么写”。

我们这一章不做完整 MyBatis,只先实现它里面最容易看清的一小段:SQL 配置化。

这一节实现的是教学版 MiniBatis,不是完整 MyBatis。它只完成这条主线:

Mapper XML ↓ 启动时解析 MapperNode ↓ 放入 Map,key = namespace.id SqlSession.selectOne(sqlId, args, callback) ↓ 找到 SQL JdbcTemplate.query(...)

二、先看最终使用方式

现在UserService.getUserInfo(...)不再直接写查询 SQL,而是写一个 SQL id:

packagecom.chenhai.jdbc.example;importcom.chenhai.beans.factory.annotation.Autowired;importcom.chenhai.batis.SqlSession;importcom.chenhai.batis.SqlSessionFactory;importcom.chenhai.jdbc.core.JdbcTemplate;importjava.sql.ResultSet;importjava.sql.Date;importjava.util.List;/** * 演示业务层怎样使用 JdbcTemplate 和教学版 SqlSession。 * * 本节新增的变化是:查询 SQL 可以放到 Mapper XML 中, * 业务代码只通过 SQL id 发起调用。 */publicclassUserService{@AutowiredprivateJdbcTemplatejdbcTemplate;/** * SQL 配置化入口。字段名必须和 XML 中的 bean id="sqlSessionFactory" 一致。 */@AutowiredprivateSqlSessionFactorysqlSessionFactory;publicUsergetUserInfo(intuserId){StringsqlId="com.chenhai.jdbc.example.User.getUserInfo";SqlSessionsqlSession=this.sqlSessionFactory.openSession();returnsqlSession.selectOne(sqlId,newObject[]{userId},statement->{try(ResultSetresultSet=statement.executeQuery()){returnresultSet.next()?mapUser(resultSet):null;}});}publicList<User>getUsers(intminUserId){finalStringsql="select id, name, birthday from users where id > ?";returnjdbcTemplate.query(sql,newObject[]{minUserId},(resultSet,rowNum)->mapUser(resultSet));}publicintupdateUserName(intuserId,Stringname){finalStringsql="update users set name = ? where id = ?";returnjdbcTemplate.update(sql,newObject[]{name,userId});}privateUsermapUser(ResultSetresultSet)throwsjava.sql.SQLException{Useruser=newUser();user.setId(resultSet.getInt("id"));user.setName(resultSet.getString("name"));Datebirthday=resultSet.getDate("birthday");if(birthday!=null){user.setBirthday(newjava.util.Date(birthday.getTime()));}returnuser;}}

对比一下变化:

上一章: Java 代码直接写 SQL 这一章: Java 代码写 sqlId SQL 放在 mapper/UserMapper.xml

注意,当前教学版还没有做到“根据 Mapper 接口自动生成代理对象”。所以业务代码仍然手动调用:

sqlSession.selectOne(sqlId,args,callback)

结果映射也仍然由调用者传入的PreparedStatementCallback完成。

三、SQL 放到 Mapper XML

项目中的 XML 文件是:

src/main/resources/mapper/UserMapper.xml

内容如下:

<?xml version="1.0" encoding="UTF-8"?><mappernamespace="com.chenhai.jdbc.example.User"><!-- namespace + id 组成 SQL 唯一标识: com.chenhai.jdbc.example.User.getUserInfo parameterType 和 resultType 当前主要用于教学说明,真正的参数绑定和结果映射 仍由 SqlSession 传入的 PreparedStatementCallback 完成。 --><selectid="getUserInfo"parameterType="java.lang.Integer"resultType="com.chenhai.jdbc.example.User">select id, name, birthday from users where id = ?</select></mapper>

这里最关键的是两个属性:

属性作用
namespace一组 SQL 的命名空间
id当前 SQL 在这个命名空间下的名字

两者拼起来就是 SQL id:

com.chenhai.jdbc.example.User.getUserInfo

这个 id 会作为 Map 的 key。运行时业务代码传入这个 key,就能找到对应 SQL。

parameterTyperesultType在当前版本里只是保存到MapperNode中,方便理解 MyBatis 的配置结构;真正的参数绑定还是JdbcTemplate做,结果映射还是回调做。

四、MapperNode:XML 中一条 SQL 的内存模型

XML 是文本,不适合每次查询时反复解析。更合理的方式是:容器启动时解析一次,把每条 SQL 变成 Java 对象放到内存里。

这个 Java 对象就是MapperNode

packagecom.chenhai.batis;/** * Mapper XML 中一条 SQL 语句的内存模型。 * * MyBatis 不会在业务调用时才去读 XML,而是在启动阶段把 XML 解析成 * 类似 MapperNode 的对象并放入 Map。 */publicclassMapperNode{privateStringnamespace;privateStringid;privateStringparameterType;privateStringresultType;privateStringsql;privateStringparameter;publicStringgetStatementId(){returnthis.namespace+"."+this.id;}@OverridepublicStringtoString(){returngetStatementId()+" : "+this.sql;}// 省略普通 getter/setter}

它对应 XML 里的这几块:

<mapper namespace="..."> <select id="..." parameterType="..." resultType="..."> SQL 文本 </select> </mapper>

可以理解成:

Mapper XML MapperNode ---------- ---------- namespace ---> namespace select.id ---> id parameterType ---> parameterType resultType ---> resultType SQL 文本 ---> sql

五、SqlSessionFactory:启动时解析 XML

SqlSessionFactory做两件事:

packagecom.chenhai.batis;/** * Factory 负责保存 Mapper 元数据并创建 SqlSession。 */publicinterfaceSqlSessionFactory{SqlSessionopenSession();MapperNodegetMapperNode(Stringname);}

默认实现是DefaultSqlSessionFactory。它在容器启动时扫描mapperLocations指定的目录,解析里面的 XML。

1. 配置入口

applicationContext.xml中这样配置:

<!-- MiniBatis 教程配置。 1. mapperLocations 指向 classpath 下的 mapper 目录。 2. sqlSessionFactory 启动时解析 Mapper XML,把 namespace.id 映射到 SQL。 3. 业务代码只传 SQL id;真正执行仍然委托给 jdbcTemplate。 --><beanid="sqlSessionFactory"class="com.chenhai.batis.DefaultSqlSessionFactory"init-method="init"><propertytype="String"name="mapperLocations"value="mapper"/></bean>

init-method="init"表示 MiniSpring 创建完这个 Bean、注入完属性后,会调用init()

2. 初始化并扫描目录

publicvoidinit(){if(this.mapperLocations==null||this.mapperLocations.trim().isEmpty()){thrownewIllegalStateException("mapperLocations must be configured");}scanLocation(this.mapperLocations);}privatevoidscanLocation(Stringlocation){URLlocationUrl=getClass().getClassLoader().getResource(location);if(locationUrl==null){thrownewIllegalStateException("Mapper location not found: "+location);}Filedir=newFile(decodePath(locationUrl));File[]files=dir.listFiles();if(files==null){return;}for(Filefile:files){StringchildLocation=location+"/"+file.getName();if(file.isDirectory()){scanLocation(childLocation);}elseif(file.getName().endsWith(".xml")){buildMapperNodes(childLocation);}}}

这段逻辑和前面 IoC 扫描配置文件有点像:先找到 classpath 下的mapper目录,再递归处理里面的 XML 文件。

3. 解析 XML 并放入 Map

privateMap<String,MapperNode>buildMapperNodes(StringfilePath){SAXReadersaxReader=newSAXReader();URLxmlPath=getClass().getClassLoader().getResource(filePath);if(xmlPath==null){thrownewIllegalStateException("Mapper XML not found: "+filePath);}try{Documentdocument=saxReader.read(xmlPath);ElementrootElement=document.getRootElement();Stringnamespace=rootElement.attributeValue("namespace");if(namespace==null||namespace.trim().isEmpty()){thrownewIllegalStateException("Mapper namespace must not be empty: "+filePath);}Iterator<?>nodes=rootElement.elementIterator();while(nodes.hasNext()){Elementnode=(Element)nodes.next();MapperNodemapperNode=buildMapperNode(namespace,node);this.mapperNodeMap.put(mapperNode.getStatementId(),mapperNode);}returnthis.mapperNodeMap;}catch(Exceptione){thrownewIllegalStateException("Parse mapper XML failed: "+filePath,e);}}privateMapperNodebuildMapperNode(Stringnamespace,Elementnode){Stringid=node.attributeValue("id");if(id==null||id.trim().isEmpty()){thrownewIllegalStateException("Mapper statement id must not be empty: "+namespace);}MapperNodemapperNode=newMapperNode();mapperNode.setNamespace(namespace);mapperNode.setId(id);mapperNode.setParameterType(node.attributeValue("parameterType"));mapperNode.setResultType(node.attributeValue("resultType"));mapperNode.setSql(node.getTextTrim());mapperNode.setParameter("");returnmapperNode;}

解析完成后,内存里大概是这样:

mapperNodeMap | +-- key: | com.chenhai.jdbc.example.User.getUserInfo | +-- value: MapperNode { namespace = "com.chenhai.jdbc.example.User" id = "getUserInfo" parameterType = "java.lang.Integer" resultType = "com.chenhai.jdbc.example.User" sql = "select id, name, birthday from users where id = ?" }

业务调用时不用再读 XML,直接从这个 Map 里拿就行。

六、SqlSession:按 sqlId 执行 SQL

SqlSessionFactory负责保存 SQL 配置,SqlSession负责面向业务代码执行 SQL。

接口很小:

packagecom.chenhai.batis;importcom.chenhai.jdbc.core.JdbcTemplate;importcom.chenhai.jdbc.core.PreparedStatementCallback;/** * 当前教学版只实现 selectOne,并保留 PreparedStatementCallback 作为结果处理回调。 */publicinterfaceSqlSession{voidsetJdbcTemplate(JdbcTemplatejdbcTemplate);voidsetSqlSessionFactory(SqlSessionFactorysqlSessionFactory);<T>TselectOne(StringsqlId,Object[]args,PreparedStatementCallback<T>callback);}

创建SqlSession时,把两个依赖塞进去:

@OverridepublicSqlSessionopenSession(){DefaultSqlSessionsqlSession=newDefaultSqlSession();sqlSession.setJdbcTemplate(this.jdbcTemplate);sqlSession.setSqlSessionFactory(this);returnsqlSession;}

这两个依赖分别负责:

依赖作用
SqlSessionFactory根据sqlId找到MapperNode
JdbcTemplate执行 SQL、绑定参数、关闭资源

DefaultSqlSession.selectOne(...)的核心代码是:

@Overridepublic<T>TselectOne(StringsqlId,Object[]args,PreparedStatementCallback<T>callback){if(this.jdbcTemplate==null){thrownewIllegalStateException("JdbcTemplate must be configured");}if(this.sqlSessionFactory==null){thrownewIllegalStateException("SqlSessionFactory must be configured");}MapperNodemapperNode=this.sqlSessionFactory.getMapperNode(sqlId);if(mapperNode==null){thrownewIllegalArgumentException("No mapped SQL statement found: "+sqlId);}returnthis.jdbcTemplate.query(mapperNode.getSql(),args,callback);}

所以selectOne(...)并没有重新实现一套 JDBC。它只是多做了一步:

sqlId ↓ MapperNode ↓ SQL 字符串 ↓ JdbcTemplate.query(...)

七、完整调用链

再回到业务代码:

publicUsergetUserInfo(intuserId){StringsqlId="com.chenhai.jdbc.example.User.getUserInfo";SqlSessionsqlSession=this.sqlSessionFactory.openSession();returnsqlSession.selectOne(sqlId,newObject[]{userId},statement->{try(ResultSetresultSet=statement.executeQuery()){returnresultSet.next()?mapUser(resultSet):null;}});}

实际执行过程是:

UserService.getUserInfo(1) ↓ sqlSessionFactory.openSession() ↓ 创建 DefaultSqlSession ↓ 注入 JdbcTemplate 和 SqlSessionFactory ↓ sqlSession.selectOne(sqlId, args, callback) ↓ sqlSessionFactory.getMapperNode(sqlId) ↓ 取出 MapperNode.sql ↓ jdbcTemplate.query(sql, args, callback) ↓ ArgumentPreparedStatementSetter 绑定参数 ↓ PreparedStatement.executeQuery() ↓ callback 把 ResultSet 转成 User

换成组件关系图就是:

UserService | | sqlId + args + callback v SqlSession | | 根据 sqlId 找 SQL v SqlSessionFactory | | mapperNodeMap v MapperNode | | sql v JdbcTemplate | | JDBC 固定流程 v DataSource / Database

八、当前实现边界

这一章只实现了 SQL 配置化的第一步,重点是把“SQL 文本从 Java 代码移到 XML”讲清楚。

当前还没有实现:

  • Mapper 接口代理;
  • #{id}这种命名参数解析;
  • 自动根据resultType反射封装对象;
  • <insert><update><delete>的独立语义;
  • 动态 SQL,例如<if><where><foreach>
  • 一级缓存、二级缓存;
  • 事务和真实 MyBatis 的SqlSession生命周期。

所以当前版本里:

parameterType / resultType 只是被解析并保存,还没有真正驱动参数绑定和结果映射。 PreparedStatementCallback 仍然由业务代码提供,用来执行 SQL 和处理 ResultSet。 JdbcTemplate 仍然是最终执行 JDBC 的核心。

对应测试主要验证两件事:

  1. DefaultSqlSessionFactory能把mapper/UserMapper.xml解析成MapperNode
  2. SqlSession.selectOne(...)能通过 SQL id 找到 SQL,并委托JdbcTemplate执行。

这一节可以总结成一句话:

MiniBatis 先不急着做完整 ORM,而是先把 SQL 配置化:启动时解析 XML,运行时按namespace.id找 SQL,最后仍然交给JdbcTemplate执行。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询