文章目录
- 第5章 数据源的注册和使用
- 背景
- 目标
- 设计
- 实现
- 工程代码
- 类图
- 实现步骤
- 1.事务管理
- 2. 类型别名注册器
- 3. 解析数据源配置
- 4.SQL执行和结果封装
- 测试
- 事先准备
- 测试用例
- 测试结果
- 总结
第5章 数据源的注册和使用
![mybatis](https://raw.githubusercontent.com/swg209/my_img/main/d7410afe519f55aeabfcc21f57148bc4.jpeg
背景
在第4章节,我们实现了解析 XML 中的 SQL 配置信息,并在代理对象调用 DefaultSqlSession 中进行获取和打印操作,从整个框架结构来看我们解决了对象的代理、Mapper的映射、SQL的初步解析,那么接下来就应该是连接数据库和执行SQL语句并返回结果了。
那么这部分内容就会涉及到解析 XML 中关于 dataSource 数据源信息配置
,并建立事务管理和连接池的启动和使用
。并将这部分能力在 DefaultSqlSession 执行 SQL 语句时进行调用
。但为了不至于在一个章节把整个工程撑大,本章的重点放到解析配置、建立事务框架和引入 DRUID 连接池
,以及初步完成 SQL 的执行和结果简单包装上
。便于大家先熟悉整个框架结构,在后续章节再陆续迭代和完善框架细节。
ORM 框架核心流程如图:
目标
解析 XML 中关于 dataSource 数据源信息配置,并建立事务管理和连接池的启动和使用,初步完成 SQL 的执行和结果简单包装。
设计
建立数据源连接池和 JDBC 事务工厂操作,并以 xml 配置数据源信息为入口,在 XMLConfigBuilder 中添加数据源解析和构建操作
,向配置类configuration添加 JDBC 操作环境信息
。以便在 DefaultSqlSession 完成对 JDBC 执行 SQL 的操作。
整体设计如图 :
- 在 parse 中解析 XML DB 链接配置信息,并完成事务工厂和连接池的注册环境到配置类的操作。
- 改造上一章节 selectOne 方法的处理,不再是打印 SQL 语句,而是把 SQL 语句放到 DB 连接池中进行执行,并完成简单的结果封装。
实现
工程代码
类图
- 通过
事务接口 Transaction 和事务工厂 TransactionFactory 的实现
,包装数据源 DruidDataSourceFactory 的功能。这里的数据源连接池我们采用的是阿里的 Druid,暂时还没有实现 Mybatis 的 JNDI 和 Pooled 连接池,这部分可以后续专门以数据源连接池的专项来开发。 - 当所有的数据源相关功能准备好后,就是在 XMLConfigBuilder 解析 XML 配置操作中,对数据源的配置进行解析以及创建出相应的服务,存放到 Configuration 的环境配置中。
- 最后在 DefaultSqlSession#selectOne 方法中完成 SQL 的执行和结果封装,最终就把整个 Mybatis 核心脉络串联出来了。
实现步骤
1.事务管理
为了保障数据的安全,一次数据库的操作应该具有事务管理能力,而不是通过 JDBC 获取链接后直接执行。还应该把控链接、提交、回滚和关闭的操作处理。所以这里我们结合 JDBC 的能力封装事务管理。
1-1 定义标准事务接口Transaction
- 定义标准的事务接口,链接、提交、回滚、关闭,具体可以由不同的事务方式进行实现,包括:JDBC和托管事务,托管事务是交给 Spring 这样的容器来管理。
public interface Transaction {Connection getConnection() throws SQLException;void commit() throws SQLException;void rollback() throws SQLException;void close() throws SQLException;
}
1-2 实现事务接口JdbcTransaction
- 在 JDBC 事务实现类中,封装了获取链接、提交事务等操作,其实使用的也就是 JDBC 本身提供的能力。
public class JdbcTransaction implements Transaction {protected Connection connection;protected DataSource dataSource;protected TransactionIsolationLevel level = TransactionIsolationLevel.NONE;protected boolean autoCommit;public JdbcTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {this.dataSource = dataSource;this.level = level;this.autoCommit = autoCommit;}public JdbcTransaction(Connection connection) {this.connection = connection;}@Overridepublic Connection getConnection() throws SQLException {connection = dataSource.getConnection();connection.setTransactionIsolation(level.getLevel());connection.setAutoCommit(autoCommit);return connection;}@Overridepublic void commit() throws SQLException {if (connection != null && !connection.getAutoCommit()) {connection.commit();}}@Overridepublic void rollback() throws SQLException {if (connection != null && !connection.getAutoCommit()) {connection.rollback();}}@Overridepublic void close() throws SQLException {if (connection != null && !connection.getAutoCommit()) {connection.close();}}
}
1-3 定义事务工厂接口TransactionFactory
- 在 JDBC 事务实现类中,封装了获取链接、提交事务等操作,其实使用的也就是 JDBC 本身提供的能力。
public interface TransactionFactory {/*** 根据连接Connection创建一个事务Transaction.*/Transaction newTransaction(Connection conn);/*** 根据数据源和事务隔离级别创建Transaction.*/Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);
}
1-4 实现事务工厂接口JdbcTransactionFactory
- 以工厂方法模式包装 JDBC 事务实现,为每一个事务实现都提供一个对应的工厂。与简单工厂的接口包装不同。
public class JdbcTransactionFactory implements TransactionFactory {@Overridepublic Transaction newTransaction(Connection conn) {return new JdbcTransaction(conn);}@Overridepublic Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {return new JdbcTransaction(dataSource, level, autoCommit);}
}
2. 类型别名注册器
在 Mybatis 框架中我们所需要的基本类型、数组类型
以及自己定义的事务实现
和事务工厂
都需要注册到类型别名的注册器
中进行管理,在我们需要使用的时候可以从注册器中获取到具体的对象类型,之后在进行实例化的方式进行使用。
2-1 基础注册器TypeAliasRegistry
- 在 TypeAliasRegistry 类型别名注册器中先做了一些基本的类型注册,以及提供 registerAlias 注册方法和 resolveAlias 获取方法。
public class TypeAliasRegistry {private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<>();public TypeAliasRegistry() {// 构造函数里注册系统内置的类型别名registerAlias("string", String.class);// 基本包装类型registerAlias("byte", Byte.class);registerAlias("long", Long.class);registerAlias("short", Short.class);registerAlias("int", Integer.class);registerAlias("integer", Integer.class);registerAlias("double", Double.class);registerAlias("float", Float.class);registerAlias("boolean", Boolean.class);}public void registerAlias(String alias, Class<?> value) {String key = alias.toLowerCase(Locale.ENGLISH);TYPE_ALIASES.put(key, value);}public <T> Class<T> resolveAlias(String input) {String key = input.toLowerCase(Locale.ENGLISH);return (Class<T>) TYPE_ALIASES.get(key);}
}
2-2 注册事务
- 在 Configuration 配置选项类中,添加类型别名注册机,通过构造函数添加 JDBC、DRUID 注册操作。
- 整个 Mybatis 的操作都是使用 Configuration 配置项进行串联流程,所以所有内容都会在 Configuration 中进行链接。
public class Configuration {/*** 映射注册机.*/protected MapperRegistry mapperRegistry = new MapperRegistry();/*** 映射的语句,存在Map.*/protected final Map<String, MappedStatement> mappedStatementMap = new HashMap<>();/*** 类型别名注册机.*/protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();/*** 环境.*/protected Environment environment;public Configuration() {typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);typeAliasRegistry.registerAlias("DRUID", DruidDataSourceFactory.class);}public TypeAliasRegistry getTypeAliasRegistry() {return typeAliasRegistry;}public void addMappers(String packageName) {mapperRegistry.addMappers(packageName);}public <T> void addMapper(Class<T> type) {mapperRegistry.addMapper(type);}public <T> T getMapper(Class<T> type, SqlSession sqlSession) {return mapperRegistry.getMapper(type, sqlSession);}public boolean hasMapper(Class<?> type) {return mapperRegistry.hasMapper(type);}public void addMappedStatement(MappedStatement ms) {mappedStatementMap.put(ms.getId(), ms);}public MappedStatement getMappedStatement(String id) {return mappedStatementMap.get(id);}public Environment getEnvironment() {return environment;}public void setEnvironment(Environment environment) {this.environment = environment;}
}
3. 解析数据源配置
通过在 XML 解析器 XMLConfigBuilder 中,扩展对环境信息的解析,我们这里把数据源、事务类内容称为操作 SQL 的环境。解析后把配置信息写入到 Configuration 配置项中,便于后续使用。
XML解析器XMLConfigBuilder
XMLConfigBuilder#parse
解析扩展对数据源解析操作,environmentsElement
方法中包括事务管理器解析
和从类型注册器
中读取到事务工程的实现类
,以及数据源
。- 最后把事务管理器和数据源的处理,通过
环境构建 Environment.Builder
存放到 Configuration 配置项中,也就可以通过 Configuration 存在的地方都可以获取到数据源了。
public class XMLConfigBuilder extends BaseBuilder {private Element root;public XMLConfigBuilder(Reader reader) {// 1. 调用父类初始化Configurationsuper(new Configuration());// 2. dom4j 处理 xmlSAXReader saxReader = new SAXReader();try {Document document = saxReader.read(new InputSource(reader));root = document.getRootElement();} catch (DocumentException e) {e.printStackTrace();}}//从xml文件解析Configuration.public Configuration parse() {try {// 解析环境配置.environmentsElement(root.element("environments"));// 解析映射器,读取XML文件中的mappers标识.mapperElement(root.element("mappers"));} catch (Exception e) {throw new RuntimeException("Error parsing SQL Mapper Configuration. Cause: " + e, e);}return configuration;}/*** <environments default="development">* <environment id="development">* <transactionManager type="JDBC">* <property name="..." value="..."/>* </transactionManager>* <dataSource type="POOLED">* <property name="driver" value="${driver}"/>* <property name="url" value="${url}"/>* <property name="username" value="${username}"/>* <property name="password" value="${password}"/>* </dataSource>* </environment>* </environments>*/private void environmentsElement(Element context) throws Exception {//获取环境String environment = context.attributeValue("default");//获取environments标签下的所有environment标签.List<Element> environmentList = context.elements("environment");for (Element e : environmentList) {//获取environment标签的id属性.String id = e.attributeValue("id");if (environment.equals(id)) {//创建事务管理器.TransactionFactory txFactory = (TransactionFactory) typeAliasRegistry.resolveAlias(e.element("transactionManager").attributeValue("type")).newInstance();// 数据源.Element dataSourceElement = e.element("dataSource");DataSourceFactory dataSourceFactory = (DataSourceFactory) typeAliasRegistry.resolveAlias(dataSourceElement.attributeValue("type")).newInstance();List<Element> propertyList = dataSourceElement.elements("property");Properties props = new Properties();for (Element property : propertyList) {props.setProperty(property.attributeValue("name"), property.attributeValue("value"));}dataSourceFactory.setProperties(props);DataSource dataSource = dataSourceFactory.getDataSource();//构建环境Environment.Builder environmentBuilder = new Environment.Builder(id).transactionFactory(txFactory).dataSource(dataSource);configuration.setEnvironment(environmentBuilder.build());}}}/*** XMLmapper 格式.* <mappers>* <mapper resource="mapper/User_Mapper.xml"/>* </mappers>*** <mapper namespace="cn.suwg.mybatis.test.dao.IUserDao">** <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.suwg.mybatis.test.po.User">* SELECT id, user_id, user_head, create_time* FROM user* where id = #{id}* </select>** </mapper>** @param mappers* @throws Exception*/private void mapperElement(Element mappers) throws Exception {//获取mappers标签下的所有mapper标签.List<Element> mapperList = mappers.elements("mapper");for (Element mapper : mapperList) {//获取mapper标签的resource属性.String resource = mapper.attributeValue("resource");Reader reader = Resources.getResourceAsReader(resource);SAXReader saxReader = new SAXReader();Document document = saxReader.read(new InputSource(reader));Element root = document.getRootElement();//命名空间String namespace = root.attributeValue("namespace");//SELECT 解析语句.List<Element> selectNodes = root.elements("select");for (Element node : selectNodes) {String id = node.attributeValue("id");String parameterType = node.attributeValue("parameterType");String resultType = node.attributeValue("resultType");String sql = node.getText();// ?匹配Map<Integer, String> parameter = new HashMap<>();Pattern pattern = Pattern.compile("(#\\{(.*?)})");Matcher matcher = pattern.matcher(sql);//匹配到的参数替换为?for (int i = 1; matcher.find(); i++) {String g1 = matcher.group(1);String g2 = matcher.group(2);parameter.put(i, g2);sql = sql.replace(g1, "?");}String msId = namespace + "." + id;String nodeName = node.getName();SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));// 构建BoundSql.BoundSql boundSql = new BoundSql(sql, parameter, parameterType, resultType);MappedStatement mappedStatement = new MappedStatement.Builder(configuration, msId, sqlCommandType,boundSql).build();// 添加解析SQLconfiguration.addMappedStatement(mappedStatement);}// 注册Mapper映射器.configuration.addMapper(Resources.classForName(namespace));}}}
4.SQL执行和结果封装
在上一章节中在 DefaultSqlSession#selectOne 只是打印了 XML 中配置的 SQL 语句,现在把数据源的配置加载进来以后,就可以把 SQL 语句放到数据源中进行执行以及结果封装。
DefaultSqlSession默认sql会话
- 在 selectOne 方法中获取 Connection 数据源链接,并简单的执行 SQL 语句,并对执行的结果进行封装处理。
- 因为目前这部分主要是为了大家串联出整个功能结构,所以关于 SQL 的执行、参数传递和结果封装都是写死的,后续我们进行扩展。
public class DefaultSqlSession implements SqlSession {/*** 配置项.*/private Configuration configuration;public DefaultSqlSession(Configuration configuration) {this.configuration = configuration;}public Configuration getConfiguration() {return configuration;}/*** 根据给定的执行SQL获取一条记录的封装对象.** @param statement* @param <T>* @return*/@Overridepublic <T> T selectOne(String statement) {return (T) ("你的操作被代理了," + statement);}@Overridepublic <T> T selectOne(String statement, Object parameter) {try {//映射语句MappedStatement mappedStatement = configuration.getMappedStatement(statement);//环境Environment environment = configuration.getEnvironment();//连接Connection connection = environment.getDataSource().getConnection();BoundSql boundSql = mappedStatement.getBoundSql();PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSql());preparedStatement.setLong(1, Long.parseLong(((Object[]) parameter)[0].toString()));//执行查询ResultSet resultSet = preparedStatement.executeQuery();List<T> objectList = resultSet2Obj(resultSet, Class.forName(boundSql.getResultType()));return objectList.get(0);} catch (Exception e) {e.printStackTrace();return null;}}private <T> List<T> resultSet2Obj(ResultSet resultSet, Class<?> clazz) {List<T> list = new ArrayList<>();try {ResultSetMetaData metaData = resultSet.getMetaData();int columnCount = metaData.getColumnCount();// 每次遍历行值while (resultSet.next()) {T obj = (T) clazz.newInstance();for (int i = 1; i <= columnCount; i++) {Object value = resultSet.getObject(i);String columnName = metaData.getColumnName(i);String setMethod = "set" + columnName.substring(0, 1).toUpperCase() + StrUtil.toCamelCase(columnName.substring(1));java.lang.reflect.Method method;if (value instanceof java.sql.Timestamp) {method = clazz.getMethod(setMethod, java.util.Date.class);} else {method = clazz.getMethod(setMethod, value.getClass());}method.invoke(obj, value);}list.add(obj);}} catch (Exception e) {e.printStackTrace();}return list;}@Overridepublic <T> T getMapper(Class<T> type) {return configuration.getMapper(type, this);}}
测试
事先准备
创建库表
-- 建表
CREATE TABLE `my_user` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',`user_id` varchar(9) DEFAULT NULL COMMENT '用户ID',`user_head` varchar(16) DEFAULT NULL COMMENT '用户头像',`create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',`update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',`user_name` varchar(64) DEFAULT NULL COMMENT '用户名',`user_password` varchar(64) DEFAULT NULL COMMENT '用户密码',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ;-- 插入数据
INSERT INTO my_user (user_id, user_head, create_time, update_time, user_name, user_password) VALUES('1', '头像', '2024-12-13 18:00:12', '2024-12-13 18:00:12', '小苏', 's123asd');
定义一个数据库接口 IUserDao
IUserDao
public interface IUserDao {String queryUserInfoById(String uid);}
配置数据源
- 通过
mybatis-config-datasource.xml
配置数据源信息,包括:driver、url、username、password - 这里DataSource 配置的是 DRUID,因为我们实现的是这个数据源的处理方式。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration><environments default="development"><environment id="development"><transactionManager type="JDBC"/><dataSource type="DRUID"><property name="driver" value="com.mysql.cj.jdbc.Driver"/><property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true"/><property name="username" value="root"/><property name="password" value="123456"/></dataSource></environment></environments><mappers><mapper resource="mapper/User_Mapper.xml"/></mappers>
</configuration>
定义对应的mapper xml文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.suwg.mybatis.test.dao.IUserDao"><select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.suwg.mybatis.test.po.User">SELECT id, user_id, user_head, create_timeFROM userwhere id = #{id}</select></mapper>
测试用例
- 在单元测试中,实现的步骤为:
- 从XML文件读取配置项,通过SqlSessionFactoryBuilder获取到SqlSessionFactory
- 从SqlSessionFactory获取SqlSession
- 获取映射器对象
- 调用Dao方法
public class ApiTest {private Logger logger = LoggerFactory.getLogger(ApiTest.class);// 测试SqlSessionFactory@Testpublic void testSqlSessionFactory() throws IOException {// 1.从xml文件读取mybatis配置项, 从SqlSessionFactory获取SqlSession.Reader reader = Resources.getResourceAsReader("mybatis-config-datasource.xml");SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);SqlSession sqlSession = sqlSessionFactory.openSession();// 2.获取映射器对象IUserDao userDao = sqlSession.getMapper(IUserDao.class);// 3.测试验证User user = userDao.queryUserInfoById(1L);logger.info("测试结果:{}", JSON.toJSONString(user));}}
测试结果
-
从输出的结果来看,我们实现的Mybatis手写 ORM 框架中,目前通过对数据源的解析、包装和使用,已经可以对 SQL 语句进行执行和包装返回的结果信息了。
-
建议大家在实现过程中,可以debug调试看下调用链路,了解看每一步都是如何完成执行步骤的,也在这个过程中进行学习 Mybatis 框架的设计技巧。
总结
- 以
解析 XML 配置解析为入口
,添加数据源的整合和包装
,引出事务工厂对 JDBC 事务的处理
,并加载到环境配置
中进行使用。 - 那么通过数据源的引入就可以在
DefaultSqlSession
中从Configuration 配置引入环境信息
,把对应的 SQL 语句提交给 JDBC 进行处理并简单封装结果数据。 - 结合本章节建立起来的框架结构,数据源、事务、简单的SQL调用,下个章节将继续这部分内容的扩展处理,让整个功能模块逐渐完善。
参考书籍:《手写Mybatis渐进式源码实践》
书籍源代码:https://github.com/fuzhengwei/book-small-mybatis