《手写Mybatis》第5章:数据源的解析、创建和使用
作者:小傅哥
博客:https://bugstack.cn
沉淀、分享、成长,让自己和他人都能有所收获!
一、前言
管你吃几碗粉,有流量就行!
现在我们每天所接收的信息量越来越多,但很多的个人却没有多少分辨知识的能力。很多知识信息也只是蹭热点的泛知识,但泛知识只是一种空泛、不成系统、甚至可能是错误的信息群,不过就是这样的信息却给内容消费者一种“成功获取了知识”吃饱的幻觉,却丧失了对知识层次的把控。
而作为一个本身就很理科的程序员来说,如果都是被泛知识充斥,花费着自己的精力和时间,没有经过足够的脑力思考所吸收的泛技术内容,长期以往是很难有所成长的。
以为我个人的成长经历来看,我更愿意花很多的实际来解决一个问题,而不是一片问题。当一个问题解决的足够透彻、清晰、明确以后,再结合着这个知识点所需要的内容继续扩展和深挖。很庆幸当年没有那么多的泛知识内容推送,否则可能我也会被弄的很焦虑!
二、目标
在上一章节我们解析 XML 中的 SQL 配置信息,并在代理对象调用 DefaultSqlSession 中进行获取和打印操作,从整个框架结构来看我们解决了对象的代理、Mapper的映射、SQL的初步解析,那么接下来就应该是连库和执行SQL语句并返回结果了。
那么这部分内容就会涉及到解析 XML 中关于 dataSource 数据源信息配置,并建立事务管理和连接池的启动和使用。并将这部分能力在 DefaultSqlSession 执行 SQL 语句时进行调用。但为了不至于在一个章节把整个工程撑大,这里我们会把重点放到解析配置、建立事务框架和引入 DRUID 连接池,以及初步完成 SQL 的执行和结果简单包装上。便于读者先熟悉整个框架结构,在后续章节再陆续迭代和完善框架细节。
三、设计
建立数据源连接池和 JDBC 事务工厂操作,并以 xml 配置数据源信息为入口,在 XMLConfigBuilder 中添加数据源解析和构建操作,向配置类configuration添加 JDBC 操作环境信息。以便在 DefaultSqlSession 完成对 JDBC 执行 SQL 的操作。
- 在 parse 中解析 XML DB 链接配置信息,并完成事务工厂和连接池的注册环境到配置类的操作。
- 与上一章节改造 selectOne 方法的处理,不再是打印 SQL 语句,而是把 SQL 语句放到 DB 连接池中进行执行,以及完成简单的结果封装。
四、实现
1. 工程结构
mybatis-step-04
└── src
├── main
│ └── java
│ └── cn.bugstack.mybatis
│ ├── binding
│ │ ├── MapperMethod.java
│ │ ├── MapperProxy.java
│ │ ├── MapperProxyFactory.java
│ │ └── MapperRegistry.java
│ ├── builder
│ │ ├── xml
│ │ │ └── XMLConfigBuilder.java
│ │ └── BaseBuilder.java
│ ├── datasource
│ │ ├── druid
│ │ │ └── DruidDataSourceFactory.java
│ │ └── DataSourceFactory.java
│ ├── io
│ │ └── Resources.java
│ ├── mapping
│ │ ├── BoundSql.java
│ │ ├── Environment.java
│ │ ├── MappedStatement.java
│ │ ├── ParameterMapping.java
│ │ └── SqlCommandType.java
│ ├── session
│ │ ├── defaults
│ │ │ ├── DefaultSqlSession.java
│ │ │ └── DefaultSqlSessionFactory.java
│ │ ├── Configuration.java
│ │ ├── SqlSession.java
│ │ ├── SqlSessionFactory.java
│ │ ├── SqlSessionFactoryBuilder.java
│ │ └── TransactionIsolationLevel.java
│ ├── transaction
│ │ ├── jdbc
│ │ │ ├── JdbcTransaction.java
│ │ │ └── JdbcTransactionFactory.java
│ │ ├── Transaction.java
│ │ └── TransactionFactory.java
│ └── type
│ ├── JdbcType.java
│ └── TypeAliasRegistry.java
└── test
├── java
│ └── cn.bugstack.mybatis.test.dao
│ ├── dao
│ │ └── IUserDao.java
│ ├── po
│ │ └── User.java
│ └── ApiTest.java
└── resources
├── mapper
│ └──User_Mapper.xml
└── mybatis-config-datasource.xml
数据源的解析和使用核心类关系,如图 5-2 所示
- 以事务接口 Transaction 和事务工厂 TransactionFactory 的实现,包装数据源 DruidDataSourceFactory 的功能。这里的数据源连接池我们采用的是阿里的 Druid,暂时还没有实现 Mybatis 的 JNDI 和 Pooled 连接池,这部分可以后续专门以数据源连接池的专项来开发。
- 当所有的数据源相关功能准备好后,就是在 XMLConfigBuilder 解析 XML 配置操作中,对数据源的配置进行解析以及创建出相应的服务,存放到 Configuration 的环境配置中。
- 最后在 DefaultSqlSession#selectOne 方法中完成 SQL 的执行和结果封装,最终就把整个 Mybatis 核心脉络串联出来了。
2. 事务管理
一次数据库的操作应该具有事务管理能力,而不是通过 JDBC 获取链接后直接执行即可。还应该把控链接、提交、回滚和关闭的操作处理。所以这里我们结合 JDBC 的能力封装事务管理。
2.1 事务接口
详见源码:cn.bugstack.mybatis.transaction.Transaction
public interface Transaction {
Connection getConnection() throws SQLException;
void commit() throws SQLException;
void rollback() throws SQLException;
void close() throws SQLException;
}
- 定义标准的事务接口,链接、提交、回滚、关闭,具体可以由不同的事务方式进行实现,包括:JDBC和托管事务,托管事务是交给 Spring 这样的容器来管理。
详见源码:cn.bugstack.mybatis.transaction.jdbc.JdbcTransaction
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;
}
@Override
public Connection getConnection() throws SQLException {
connection = dataSource.getConnection();
connection.setTransactionIsolation(level.getLevel());
connection.setAutoCommit(autoCommit);
return connection;
}
@Override
public void commit() throws SQLException {
if (connection != null && !connection.getAutoCommit()) {
connection.commit();
}
}
//...
}
- 在 JDBC 事务实现类中,封装了获取链接、提交事务等操作,其实使用的也就是 JDBC 本身提供的能力。
2.2 事务工厂
详见源码:cn.bugstack.mybatis.transaction.TransactionFactory
public interface TransactionFactory {
/**
* 根据 Connection 创建 Transaction
* @param conn Existing database connection
* @return Transaction
*/
Transaction newTransaction(Connection conn);
/**
* 根据数据源和事务隔离级别创建 Transaction
* @param dataSource DataSource to take the connection from
* @param level Desired isolation level
* @param autoCommit Desired autocommit
* @return Transaction
*/
Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);
}
- 以工厂方法模式包装 JDBC 事务实现,为每一个事务实现都提供一个对应的工厂。与简单工厂的接口包装不同。
3. 类型别名注册器
在 Mybatis 框架中我们所需要的基本类型、数组类型以及自己定义的事务实现和事务工厂都需要注册到类型别名的注册器中进行管理,在我们需要使用的时候可以从注册器中获取到具体的对象类型,之后在进行实例化的方式进行使用。
3.1 基础注册器
详见源码:cn.bugstack.mybatis.type.TypeAliasRegistry
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 string) {
String key = string.toLowerCase(Locale.ENGLISH);
return (Class<T>) TYPE_ALIASES.get(key);
}
}
- 在 TypeAliasRegistry 类型别名注册器中先做了一些基本的类型注册,以及提供 registerAlias 注册方法和 resolveAlias 获取方法。
3.2 注册事务
详见源码:cn.bugstack.mybatis.session.Configuration
public class Configuration {
//环境
protected Environment environment;
// 映射注册机
protected MapperRegistry mapperRegistry = new MapperRegistry(this);
// 映射的语句,存在Map里
protected final Map<String, MappedStatement> mappedStatements = new HashMap<>();
// 类型别名注册机
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
public Configuration() {
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
typeAliasRegistry.registerAlias("DRUID", DruidDataSourceFactory.class);
}
//...
}
- 在 Configuration 配置选项类中,添加类型别名注册机,通过构造函数添加 JDBC、DRUID 注册操作。
- 读者应该注意到,整个 Mybatis 的操作都是使用 Configuration 配置项进行串联流程,所以所有内容都会在 Configuration 中进行链接。
4. 解析数据源配置
通过在 XML 解析器 XMLConfigBuilder 中,扩展对环境信息的解析,我们这里把数据源、事务类内容称为操作 SQL 的环境。解析后把配置信息写入到 Configuration 配置项中,便于后续使用。
详见源码:cn.bugstack.mybatis.builder.xml.XMLConfigBuilder
public class XMLConfigBuilder extends BaseBuilder {
public Configuration parse() {
try {
// 环境
environmentsElement(root.element("environments"));
// 解析映射器
mapperElement(root.element("mappers"));
} catch (Exception e) {
throw new RuntimeException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
return configuration;
}
private void environmentsElement(Element context) throws Exception {
String environment = context.attributeValue("default");
List<Element> environmentList = context.elements("environment");
for (Element e : environmentList) {
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());
}
}
}
}
- 以 XMLConfigBuilder#parse 解析扩展对数据源解析操作,在 environmentsElement 方法中包括事务管理器解析和从类型注册器中读取到事务工程的实现类,同理数据源也是从类型注册器中获取。
- 最后把事务管理器和数据源的处理,通过环境构建 Environment.Builder 存放到 Configuration 配置项中,也就可以通过 Configuration 存在的地方都可以获取到数据源了。
5. SQL执行和结果封装
在上一章节中在 DefaultSqlSession#selectOne 只是打印了 XML 中配置的 SQL 语句,现在把数据源的配置加载进来以后,就可以把 SQL 语句放到数据源中进行执行以及结果封装。
详见源码:cn.bugstack.mybatis.session.defaults.DefaultSqlSession
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
public DefaultSqlSession(Configuration configuration) {
this.configuration = configuration;
}
@Override
public <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> objList = resultSet2Obj(resultSet, Class.forName(boundSql.getResultType()));
return objList.get(0);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
// ...
}
- 在 selectOne 方法中获取 Connection 数据源链接,并简单的执行 SQL 语句,并对执行的结果进行封装处理。
- 因为目前这部分主要是为了大家串联出整个功能结构,所以关于 SQL 的执行、参数传递和结果封装都是写死的,后续我们进行扩展。
六、测试
1. 事先准备
1.1 创建库表
创建一个数据库名称为 mybatis 并在库中创建表 user 以及添加测试数据,如下:
CREATE TABLE
USER
(
id bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',
userId VARCHAR(9) COMMENT '用户ID',
userHead VARCHAR(16) COMMENT '用户头像',
createTime TIMESTAMP NULL COMMENT '创建时间',
updateTime TIMESTAMP NULL COMMENT '更新时间',
userName VARCHAR(64),
PRIMARY KEY (id)
)
ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into user (id, userId, userHead, createTime, updateTime, userName) values (1, '10001', '1_04', '2022-04-13 00:00:00', '2022-04-13 00:00:00', '小傅哥');
2. 配置数据源
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="DRUID">
<property name="driver" value="com.mysql.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>
- 通过
mybatis-config-datasource.xml
配置数据源信息,包括:driver、url、username、password - 另外这里要注意下,DataSource 配置的是 DRUID,因为我们实现的是这个数据源的处理方式。
3. 配置Mapper
<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.bugstack.mybatis.test.po.User">
SELECT id, userId, userName, userHead
FROM user
where id = #{id}
</select>
- Mapper 的配置内容在上一章节的解析学习中已经做了配置,本章节做了简单的调整。
2. 单元测试
@Test
public void test_SqlSessionFactory() throws IOException {
// 1. 从SqlSessionFactory中获取SqlSession
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config-datasource.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();
// 2. 获取映射器对象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
// 3. 测试验证
User user = userDao.queryUserInfoById(1L);
logger.info("测试结果:{}", JSON.toJSONString(user));
}
- 单元测试没有什么改变,仍是通过 SqlSessionFactory 中获取 SqlSession 并获得映射对象和执行方法调用。
测试结果
22:34:18.676 [main] INFO c.alibaba.druid.pool.DruidDataSource - {dataSource-1} inited
22:34:19.286 [main] INFO cn.bugstack.mybatis.test.ApiTest - 测试结果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
Process finished with exit code 0
- 从现在的测试结果已经可以看出,通过我们对数据源的解析、包装和使用,已经可以对 SQL 语句进行执行和包装返回的结果信息了。
- 读者在学习的过程中可以调试下代码,看看每一步都是如何完成执行步骤的,也在这个过程中进行学习 Mybatis 框架的设计技巧。
七、总结
- 以解析 XML 配置解析为入口,添加数据源的整合和包装,引出事务工厂对 JDBC 事务的处理,并加载到环境配置中进行使用。
- 那么通过数据源的引入就可以在 DefaultSqlSession 中从 Configuration 配置引入环境信息,把对应的 SQL 语句提交给 JDBC 进行处理并简单封装结果数据。
- 结合本章节建立起来的框架结构,数据源、事务、简单的SQL调用,下个章节将继续这部分内容的扩展处理,让整个功能模块逐渐完善。
《手写Mybatis》第5章:数据源的解析、创建和使用的更多相关文章
- 要想精通Mybatis?从手写Mybatis框架开始吧!
1.Mybatis组成 动态SQL Config配置 Mapper配置 2.核心源码分析 Configuration源码解析 SqlSessionFactory源码解析 SqlSession源码解析 ...
- 手写MyBatis ORM框架实践
一.实现手写Mybatis三个难点 1.接口既然不能被实例化?那么我们是怎么实现能够调用的? 2.参数如何和sql绑定 3.返回结果 下面是Mybatis接口 二.Demo实现 1.创建Maven工程 ...
- 手写mybatis框架笔记
MyBatis 手写MyBatis流程 架构流程图 封装数据 封装到Configuration中 1.封装全局配置文件,包含数据库连接信息和mappers信息 2.封装*mapper.xml映射文件 ...
- 手写MyBatis流程
MyBatis 手写MyBatis流程 架构流程图 封装数据 封装到Configuration中 1.封装全局配置文件,包含数据库连接信息和mappers信息 2.封装*mapper.xml映射文件 ...
- 手写mybatis框架-增加缓存&事务功能
前言 在学习mybatis源码之余,自己完成了一个简单的ORM框架.已完成基本SQL的执行和对象关系映射.本周在此基础上,又加入了缓存和事务功能.所有代码都没有copy,如果也对此感兴趣,请赏个Sta ...
- 带码农《手写Mybatis》进度3:实现映射器的注册和使用
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获!
- 《手写Mybatis》第4章:Mapper XML的解析和注册使用
作者:小傅哥 系列:https://bugstack.cn/md/spring/develop-mybatis/2022-03-20-%E7%AC%AC1%E7%AB%A0%EF%BC%9A%E5%B ...
- 手写mybatis框架
前言 很久没有更新mybatis的源码解析了,因为最近在将自己所理解的mybatis思想转为实践. 在学习mybatis的源码过程中,根据mybatis的思想自己构建了一个ORM框架 .整个代码都是自 ...
- Mybatis执行流程学习之手写mybatis雏形
Mybatis是目前开发中最常用的一款基于ORM思想的半自动持久层框架,平时我们都仅仅停留在使用阶段,对mybatis是怎样运行的并不清楚,今天抽空找到一些资料自学了一波,自己写了一个mybatis的 ...
随机推荐
- python练习册 每天一个小程序 第0006题
1 # -*-coding:utf-8-*- 2 __author__ = 'Deen' 3 ''' 4 题目描述: 5 你有一个目录,放了你一个月的日记,都是 txt,为了避免分词的问题,假设内容都 ...
- Pytorch自动混合精度(AMP)介绍与使用
背景: pytorch从1.6版本开始,已经内置了torch.cuda.amp,采用自动混合精度训练就不需要加载第三方NVIDIA的apex库了.本文主要从三个方面来介绍AMP: 一.什么是AMP? ...
- k8s集群搭建过程详解
准备工作 安装CentOS7虚拟机 略 安装Docker 略 关闭CentOS7自带的防火墙服务 systemctl disable firewalld systemctl stop firewall ...
- java == 和 equals
- 运行 Spring Boot 有哪几种方式?
打包用命令或者放到容器中运行用 Maven/ Gradle 插件运行直接执行 main 方法运行
- Redis 的内存用完了会发生什么?
如果达到设置的上限,Redis 的写命令会返回错误信息(但是读命令还可以正 常返回.)或者你可以将 Redis 当缓存来使用配置淘汰机制,当 Redis 达到内存 上限时会冲刷掉旧的内容.
- 4.RDD操作
目录 一. RDD创建 从本地文件系统中加载数据创建RDD 从HDFS加载数据创建RDD 通过并行集合(列表)创建RDD 二. RDD操作 转换操作 filter(func) map(func) fl ...
- 打败算法 —— 环形链表 II
本文参考 出自LeetCode上的题库 -- 环形链表II,哈希表和快慢指针两种解法都需要O(n)的时间,但快慢指针仅占用O(1)的空间 https://leetcode-cn.com/problem ...
- C++面向对象 - 类的前向声明的用法
C++中的类应当是先定义,然后使用.但在处理相对复杂的问题,比如考虑类的组合时,有可能遇到两个类相互引用的情况,这种情况称为循环依赖. 考虑下面代码: class A { public: void f ...
- 数字电路的多路复用(MUX)
学习文章:https://mp.weixin.qq.com/s/1pfLfvkAPyhEMLvfnyDy0g 最基本--2选1的MUX结构: 可以看作是:assign Y=S?B:A; 下面使用MUX ...