文章内容输出来源:拉勾教育Java高薪训练营
说明
通过分析使用原生JDBC操作存在的问题,带着这些问题的解决思路,结合Mybatis框架主流程,一步一步搭建一个简易版本。
一、数据准备
- 创建MYSQL数据库
DROP DATABASE IF EXISTS db_test;
CREATE DATABASE db_test DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
- 创建表、初始化一些数据
CREATE TABLE user(
`id` int(10) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`name` varchar(200) NOT NULL COMMENT '名称',
PRIMARY KEY(`id`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_bin
insert into user(name) values('钱大'),('何二'),('张三'),('李四'),('王五');
二、项目准备
- 创建自定义框架的Maven项目simple_mybatis
- pom.xml配置
com.yyh simple_mybatis 1.0-SNAPSHOT UTF-8 UTF-8 1.8 1.8 1.8 mysql mysql-connector-java 5.1.47
- 创建测试Maven项目simple_mybatis_test
测试项目作为使用端,引入simple_mybatis的框架,可以通过创建DAO接口,实现数据的操作
- 在
com.yyh.entity
包下创建UserEntity
实体,对应于数据表user
public class UserEntity { private Integer id; private String name; public UserEntity() {} public UserEntity(Integer id, String name) { this.id = id; this.name = name; } //ignore getter/setter/toString }
- pom.xml配置
com.yyh simple_mybatis_test 1.0-SNAPSHOT UTF-8 UTF-8 1.8 1.8 1.8 com.yyh simple_mybatis 1.0-SNAPSHOT junit junit 4.12
三、分析问题
- 原生JDBC的查询
-
在测试项目中创建类
com.yyh.test.JdbcDemo
,对用户表进行查询,查询张三的用户信息- 加载驱动
- 创建连接
- 获取SQL查询语句的预编译statement,设置参数
- 执行查询操作
- 处理结果集
-
代码如下
Connection connection = null; PreparedStatement preparedStatement = null; ResultSet resultSet = null; List
list = new ArrayList<>(); try { //加载数据库驱动 Class.forName("com.mysql.jdbc.Driver"); //获取数据库连接 connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/db_test?characterEncoding=utf-8", "root", "password"); //获取预处理statement preparedStatement = connection.prepareStatement("select * from user where name=?"); //设置参数,从1开始 preparedStatement.setString(1, "张三"); //执行查询操作 resultSet = preparedStatement.executeQuery(); //处理结果集 while (resultSet.next()) { int id = resultSet.getInt("id"); String username = resultSet.getString("name"); UserEntity userEntity = new UserEntity(); list.add(new UserEntity(id, username)); } } catch (Exception e) { e.printStackTrace(); }finally { //... 省略关闭资源操作 } //遍历查询到的用户 if(null != list && list.size() > 0) { for (UserEntity userEntity : list) { System.out.println(userEntity.toString()); } }
- 总结原生JDBC查询使用上的问题以及大概的解决思路
- 数据库连接频繁创建、关闭,消耗资源
- 使用连接池
- 代码繁琐,不易于复用(需要加载驱动、创建连接、生成statement)
- 对底层细节进行封装
- 数据库配置、数据脚本与代码紧耦合,如果脚本比较复杂,不易维护
- 增加配置文件、脚本文件,与代码分离
- prepareStatement的参数设置需要手动一一对照顺序设值,如果SQL条件多或者条件复杂,不易维护
- 反射,解析参数实体
- 需要手动解析结果集,对返回对象进行一一设值
- 反射,将数据库记录封装成pojo返回
四、项目设计
设计思路
根据上面分析出来的问题,对简易版本框架的设计。
- 测试驱动开发,首先看下下测试项目需要如何设计
作为使用端,引入了自定义的框架,希望可以这样使用:
- 有一个配置文件可以对数据源进行维护
- 将相关的查询、更新、添加等SQL代码放到数据脚本文件中进行维护,与代码分离
- 编写接口方法,业务方可以直接调用接口方法就可以实现数据的操作
- 框架端就应该能提供以下的功能
- 读取配置文件、数据脚本文件,然后进行解析
(1)创建数据源
(2)创建Sql脚本的对象,能将接口方法与对应的Sql脚本进行映射 - 获取数据源,打开连接
- 创建与JDBC的交互,暴露接口方便外部的调用
框架设计
-
基础实体类
- MappedStatement
- Sql映射对象。对mapper文件的每一个节点(insert/select/update/delete)的封装。
- 包括了标识ID(由mapper文件的namespace和节点的id组合而成)、参数类型、结果类型、SQL
- 核心配置类:Configuration
存储数据源、以及所有扫描解析到的Sql映射对象
- BoundSql
标识解析后生态生成的SQL以及参数信息
-
读取配置文件
- 资源读取:Resources
将配置文件加载为字节输入流,存储到内存中
-
解析配置文件
- 使用dom4j对配置文件进行解析,分为两部分
(1)对mybatis核心配置文件的解析:XmlConfigBuilder
(2)对mapper文件的解析:XmlMapperBuilder
-
Session会话层
- SqlSession接口,以及对应的实现类DefaultSqlSession
提供SQL操作接口方法(调用执行器进行具体SQL执行操作),供外部调用
- SqlSessionFactory接口,以及对应的实现类DefaultSqlSessionFactory
通过工厂模式进行SqlSession的创建
- SqlSessionFactoryBuilder构造器
读取配置文件信息、进行解析、创建Session工厂
-
执行器
- Executor接口,以及对应的实现类SimpleExecutor
获取连接,解析SQL参数、创建预编译对象,执行SQL请求、解析映射结果集
五、项目实现
基于
simple_mybatis
项目
1. 读取并解析配置文件
1.1 读取工具类
- 创建
com.yyh.core.io.Resources
读取文件加载为字节流,存储到内存中
public class Resources {
public static InputStream getResourceAsStream(String path) {
InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path);
return resourceAsStream;
}
}
1.2 创建配置类
- 创建
com.yyh.pojo.MappedStatement
此类用于记录mapper数据脚本中的每一个节点(select|update|delete|insert)的SQL信息
public class MappedStatement {
//标识ID
private String id;
//参数类型
private String parameterType;
//结果集类型
private String resultType;
//SQL语句
private String sql;
//ignore getter/setter
}
- 创建
com.yyh.pojo.Configuration
此类用于加载数据源以及所有的mapper数据脚本的SQL信息
public class Configuration {
//数据源
private DataSource dataSource;
//key为statementId,由mapper脚本文件的namespace和节点标签的id组成
private Map mappedStatementMap = new HashMap<>();
//ignore getter/setter
}
1.3 解析配置文件
- 引入dom4j依赖
dom4j
dom4j
1.6.1
jaxen
jaxen
1.1.6
- 创建核心配置文件的解析类:
com.yyh.core.xml.XMLConfigBuilder
public class XMLConfigBuilder {
private Configuration configuration;
public XMLConfigBuilder() {
this.configuration = new Configuration();
}
// 使用dom4j对配置文件进行解析,封装Configuration
public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {
Document document = new SAXReader().read(inputStream);
Element rootElement = document.getRootElement();
//获取所有的属性值
List list = rootElement.selectNodes("//property");
Properties properties = new Properties();
for (Element element : list) {
String name = element.attributeValue("name");
String value = element.attributeValue("value");
properties.setProperty(name,value);
}
//构建数据源
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
comboPooledDataSource.setDriverClass(properties.getProperty("driverClass"));
comboPooledDataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
comboPooledDataSource.setUser(properties.getProperty("username"));
comboPooledDataSource.setPassword(properties.getProperty("password"));
configuration.setDataSource(comboPooledDataSource);
//mapper.xml解析: 拿到路径--字节输入流---dom4j进行解析
List mapperList = rootElement.selectNodes("//mapper");
for (Element element : mapperList) {
String mapperPath = element.attributeValue("resource");
InputStream resourceAsSteam = Resources.getResourceAsSteam(mapperPath);
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
xmlMapperBuilder.parse(resourceAsSteam);
}
return configuration;
}
}
- 创建mapper数据文件的解析类:
com.yyh.core.xml.XmlMapperBuilder
public class XmlMapperBuilder {
private Configuration configuration;
public XmlMapperBuilder(Configuration configuration) {
this.configuration = configuration;
}
//解析Mapper文件
public void parse(InputStream inputStream) throws Exception{
Document document = new SAXReader().read(inputStream);
Element rootElement = document.getRootElement();
String namespace = rootElement.attributeValue("namespace");
List list = rootElement.selectNodes("select|insert|update|delete");
for (Element element : list) {
String id = element.attributeValue("id");
String resultType = element.attributeValue("resultType");
String parameterType = element.attributeValue("parameterType");
String sqlText = element.getTextTrim();
MappedStatement mappedStatement = new MappedStatement();
mappedStatement.setId(id);
mappedStatement.setResultType(resultType);
mappedStatement.setParameterType(parameterType);
mappedStatement.setSql(sqlText);
//唯一key
String key = namespace + "." + id;
configuration.getMappedStatementMap().put(key, mappedStatement);
}
}
}
2. 创建执行器
执行器主要负责SQL语句的生成、执行、结果映射
2.1 创建执行层接口:com.yyh.core.executor.Executor
增加一个查询数据的接口,查询数据就要知道执行哪一个SQL,SQL如果还有参数得传递相应的参数。所以接口方法参数要传入MappedStatement和params,因为需要将结果集封装到实体中返回,返回值就使用了泛型
public interface Executor {
//查询集合数据
List selectList(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception;
}
2.2 创建执行层接口实现类:com.yyh.core.executor.SimpleExecutor
public class SimpleExecutor implements Executor{
@Override
public List selectList(Configuration configuration,
MappedStatement mappedStatement, Object... params)
throws Exception {
//获取连接
Connection connection = configuration.getDataSource().getConnection();
//获取sql
String sql = mappedStatement.getSql();
//对sql语句进行解析
BoundSql boundSql = getBoundSql(sql);
//获取预编译对象
PreparedStatement statement = connection.prepareStatement(boundSql.getSqlText());
//获取参数类型
String parameterType = mappedStatement.getParameterType();
Class> paramterTypeClass = getClassType(parameterType);
List parameterMappingList = boundSql.getParameterMappingList();
for (int i = 0; i < parameterMappingList.size(); i++) {
ParameterMapping parameterMapping = parameterMappingList.get(i);
String content = parameterMapping.getContent();
if(null != paramterTypeClass) {
if(paramterTypeClass == Integer.class) {
statement.setObject(i + 1, params[0]);
}else {
Field declaredField = paramterTypeClass.getDeclaredField(content);
declaredField.setAccessible(true);
Object o = declaredField.get(params[0]);
statement.setObject(i + 1, o);
}
}
}
//执行sql
ResultSet resultSet = statement.executeQuery();
String resultType = mappedStatement.getResultType();
Class> resultTypeClass = getClassType(resultType);
List
getBoundSql方法就是对SQL进行解析的方法
使用了Mybatis提供的GenericTokenParser和ParameterMappingTokenHandler工具类进行解析
2.3 上一步的执行器实现类,需要对SQL语句进行解析
使用到了BoundSql类即com.yyh.core.pojo.BoundSql
。如下
public class BoundSql {
//解析后的SQL语句
private String sqlText;
//解析后的参数
private List parameterMappingList = new ArrayList<>();
//ignore getter/setter
}
3. 创建会话层
3.1 创建会话层接口:com.yyh.core.session.SqlSession
接口主要是提供通用的查询、添加、编辑等接口,如下即查询列表数据的接口,传入相应的statementId以及对应的参数
public interface SqlSession {
//查询数据
List selectList(String statementId, Object... params) throws Exception;
}
3.2 创建会话层实现类:com.yyh.core.session.DefaultSession
实现类中对查询数据方法进行了实现,主要是获取到对应的MappedStatement
对象,调用执行器Executor
的查询数据方法
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
private Executor executor;
public DefaultSqlSession(Configuration configuration) {
this.configuration = configuration;
this.executor = new SimpleExecutor();
}
//查询数据
@Override
public List selectList(String statementId, Object... params) throws Exception {
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
return executor.selectList(configuration, mappedStatement, params);
}
}
3.2 创建会话层工厂接口:com.yyh.core.session.SqlSessionFactory
public interface SqlSessionFactory {
/**
* 开启一个session
* @return
*/
SqlSession openSession();
}
3.3 创建会话层创建工厂实现类:com.yyh.core.session.DefaultSqlSessionFactory
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
//开启一个session
@Override
public SqlSession openSession() {
return new DefaultSqlSession(configuration);
}
}
3.4 创建会话层工厂的构造器:com.yyh.core.session.SqlSessionFactoryBuilder
主要用于对配置文件信息进行解析,创建核心配置类,从而去创建一个session工厂
public class SqlSessionFactoryBuilder {
//构造工厂
public SqlSessionFactory build(InputStream inputStream) throws Exception {
//解析配置信息
XmlConfigBuilder xmlConfigBuilder = new XmlConfigBuilder();
Configuration configuration = xmlConfigBuilder.parseConfig(inputStream);
DefaultSqlSessionFactory factory = new DefaultSqlSessionFactory(configuration);
return factory;
}
}
项目测试
基于simple_mybatis_test项目
1. 创建核心配置文件
在resources目录下创建mybatis-config.xml
。主要配置下数据源,配置下Mapper数据脚本的路径
2. 创建用户DAO接口
创建类com.yyh.dao.UserDao.java
public interface UserDao {
/**
* 查询数据列表
* @param user
* @return
*/
List selectList(UserEntity user);
}
3. 创建用户Mapper数据脚本
在resources/mapper目录下创建UserMapper.xml
4. 创建用户单元测试类
创建类com.yyh.test.SqlSessionUserTest
public class SqlSessionUserTest {
private SqlSession sqlSession;
@Before
public void before() throws Exception {
//1.读取配置文件
InputStream stream = Resources.getResourceAsStream("mybatis-config.xml");
//2.创建SqlSession工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(stream);
//3.打开session
sqlSession = sqlSessionFactory.openSession();
}
@After
public void after() {
//5.关闭session
sqlSession.close();
}
//测试查询用户
@Test
public void testSelectUserList() throws Exception {
UserEntity user = new UserEntity();
user.setName("张三");
//4.调用session的查询数据方法获取数据
List users = sqlSession.selectList("com.yyh.dao.UserDao.selectList", user);
Assert.checkNonNull(users);
}
}
项目代码
- 自定义框架simple_mybatis
- 自定义框架测试项目simple_mybatis_test