自己动手写一个持久层框架
1. JDBC问题分析
我们来看一段JDBC的代码:
public static void main(String[] args) {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
//1. 加载数据库驱动
Class.forName("com.mysql.jdbc.Drive");
//2. 通过驱动管理类获取数据库链接
connection = DriverManager.getConnection("jdbc:mysql://hocalhost:3306/mybatis?characterEncoding=utf-8",
"root","root");
//3. 定义SQL语句 ?表示占位符
String sql = "SELECT * FROM user WHERE username = ?";
//4. 获取预处理对象Statement
preparedStatement = connection.prepareStatement(sql);
//5. 设置参数,第一个参数为SQL语句中参数的序号(从1开始),第二个参数为设置的参数值
preparedStatement.setString(1,"tom");
//6. 向数据库发出SQL执行查询,查询出结果集
resultSet = preparedStatement.executeQuery();
//7. 遍历查询结果集
while (resultSet.next()){
int id = resultSet.getInt("id");
String userName = resultSet.getString("username");
//封装User
user.setId(id);
user.setUserName(userName);
}
System.out.println(user);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
可以看到,直接使用JDBC开发是存在一些问题的,我们来分析下:
问题分析:
数据库配置信息存在硬编码问题 频繁创建、释放数据库链接
//1. 加载数据库驱动
Class.forName("com.mysql.jdbc.Drive");
//2. 通过驱动管理类获取数据库链接
connection = DriverManager.getConnection("jdbc:mysql://hocalhost:3306/mybatis?characterEncoding=utf-8","root","root");
sql语句、设置参数、获取结果集均存在硬编码问题
//3. 定义SQL语句 ?表示占位符
String sql = "SELECT * FROM user WHERE username = ?";
//4. 获取预处理对象Statement
preparedStatement = connection.prepareStatement(sql);
//5. 设置参数,第一个参数为SQL语句中参数的序号(从1开始),第二个参数为设置的参数值
preparedStatement.setString(1,"tom");
//6. 向数据库发出SQL执行查询,查询出结果集
resultSet = preparedStatement.executeQuery();
int id = resultSet.getInt("id");
String userName = resultSet.getString("username");
手动封装返回结果集 较为繁琐
//7. 遍历查询结果集
while (resultSet.next()){
int id = resultSet.getInt("id");
String userName = resultSet.getString("username");
//封装User
user.setId(id);
user.setUserName(userName);
}
System.out.println(user);
解决思路:
写在配置文件中 连接池(c3p0、dbcp、德鲁伊...) 配置文件 (和1放一起吗?No,经常变动和不经常变动的不要放在一起) 反射、内省
下面根据这个解决思路,自己动手写一个持久层框架,写框架之前分析这个框架需要做什么
2. 自定义框架思路分析
使用端(项目):
引入自定义持久层框架的jar包 提供两部分配置信息:
数据库配置信息 SQL配置信息(SQL语句)
使用配置文件来提供这些信息: sqlMapConfig.xml :存放数据库的配置信息 mapper.xml :存放SQL配置信息
自定义持久层框架(工程):
持久层框架的本质就是对JDBC代码进行了封装
加载配置文件:根据配置文件的路径加载配置文件成字节输入流,存储内存中
“
Q:getResourceAsStearm方法需要执行两次分别加载sqlMapConfig额和mapper吗?
A:可以但没必要,我们可以在sqlMapConfig.xml中写入mapper.xml的全路径即可
”创建Resources类 方法:getResourceAsStream(String path) 创建两个javaBean:(容器对象):存放的就是配置文件解析出来的内容
Configuration:核心配置类:存放sqlMapConfig.xml解析出来的内容 MappedStatement:映射配置类:存放mapper.xml解析出来的内容 解析配置文件:使用dom4j
创建类:SqlSessionFactoryBuilder 方法:build(InputStream in) 这个流就是刚才存在内存中的 使用dom4j解析配置文件,将解析出来的内容封装到容器对象中 创建SqlSessionFactory对象;生产sqlSession:会话对象(工厂模式 降低耦合,根据不同需求生产不同状态的对象) 创建sqlSessionFactory接口及实现类DefaultSqlSessionFactory
openSession(); 生产sqlSession 创建SqlSession接口及实现类DefaultSession
selectList() selectOne() update() delete() ... 定义对数据库的CRUD操作,例如: 创建Executor接口及实现类SimpleExecutor实现类
query(Configuration con,MappedStatement ms,Object ...param);执行JDBC代码, Object ...param
具体的参数值,可变参;
3. 创建表并编写测试类
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'lucy');
INSERT INTO `user` VALUES (2, 'tom');
INSERT INTO `user` VALUES (3, 'jack');
SET FOREIGN_KEY_CHECKS = 1;
1. 创建一个Maven项目—— Ipersistence_test
2. 在resource中创建sqlMapConfig.xml 和 UserMapper.xml
UserMapper.xml
<mapper namespace="user">
<select id="selectList" resultType="com.dxh.pojo.User">
select * from user
select>
<select id="selectOne" resultType="com.dxh.pojo.User" paramterType="com.dxh.pojo.User">
select * from user where id = #{id} and username = #{userName}
select>
mapper>
“Q:为什么要有namespace和id ?A:当一个
”*Mapper.xml
中有多条sql时,无法区分具体是哪一条所以增加 id 如果有UserMapper.xml
和ProductMapper.xml
,假设他们的查询的id都为”selectList“,那么将无法区分具体是查询user还是查询product的。所以增加 namespacenamespace.id 组成sql的唯一标识,也称为statementId
sqlMapConfig.xml
<configuration>
<dataSource>
<property name="driverClass" value="com.mysql.jdbc.Driver">property>
<property name="jdbcUrl" value="jdbc:mysql:///zdy_mybatis">property>
<property name="username" value="root">property>
<property name="password" value="root">property>
dataSource>
<mapper resource="UserMapper.xml">mapper>
configuration>
4. 开始编写持久层框架
自定义持久层框架(工程):
“本质就是对JDBC代码进行了封装
”
加载配置文件:根据配置文件的路径加载配置文件成字节输入流,存储内存中
创建Resources类 方法:getResourceAsStream(String path) 创建两个javaBean:(容器对象):存放的就是配置文件解析出来的内容
Configuration:核心配置类:存放sqlMapConfig.xml解析出来的内容 MappedStatement:映射配置类:存放mapper.xml解析出来的内容 解析配置文件:使用dom4j
创建类:SqlSessionFactoryBuilder 方法:build(InputStream in) 这个流就是刚才存在内存中的 使用dom4j解析配置文件,将解析出来的内容封装到容器对象中 创建SqlSessionFactory对象;生产sqlSession:会话对象(工厂模式 降低耦合,根据不同需求生产不同状态的对象) 创建sqlSessionFactory接口及实现类DefaultSqlSessionFactory
openSession(); 生产sqlSession 创建SqlSession接口及实现类DefaultSession
定义对数据库的CRUD操作 创建Executor接口及实现类SimpleExecutor实现类
query(Configuration con,MappedStatement ms,Object ...param);执行JDBC代码, Object ...param
具体的参数值,可变参;
我们之前已经对持久层框架进行了分析,需要做6部分组成,如下:
1. 加载配置文件
我们要把用户端的配置文件成字节输入流并存到内存中:
新建Resource类,提供一个static InputStream getResourceAsStream(String path)
方法,并返回inputstream
package com.dxh.io;
import java.io.InputStream;
public class Resource {
//根据配置文件的路径,将配置文件加载成字节输入流,存储在内存中
public static InputStream getResourceAsStream(String path){
InputStream resourceAsStream = Resource.class.getClassLoader().getResourceAsStream(path);
return resourceAsStream;
}
}
2. 创建JavaBean(容器对象)
之前我们说到,要把解析出来的配置文件封装成对象。
MappedStatement (存放SQL信息) Configuration (存放数据库配置信息)
// MappedStatement,我们存放SQL的信息
package com.dxh.pojo;
public class MappedStatement {
// id标识
private String id;
//返回值类型
private String resultType;
//参数值类型
private String paramterType;
//sql语句
private String sql;
getset省略...
}
这里我们把封装好的MappedStatement
对象也放在Configuration
中,同时我们不存放数据库的url、username...了,直接存放DataSource
package com.dxh.pojo;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
public class Configuration {
private DataSource dataSource;
/**
* key statementId (就是namespace.id)
* value:封装好的MappedStatement对象
*/
Map mappedStatementMap = new HashMap<>();
getset省略...
}
3.解析xml文件
这一步我们解析两个xml文件sqlMapConfig.xml
、mapper.xml
我们首先把解析的过程封装起来:新建XMLConfigBuild.java
package com.dxh.config;
import com.dxh.io.Resource;
import com.dxh.pojo.Configuration;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.beans.PropertyVetoException;
import java.io.InputStream;
import java.util.List;
import java.util.Properties;
public class XMLConfigBuild {
private Configuration configuration;
public XMLConfigBuild() {
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);
}
//C3P0连接池
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 resourceAsStream = Resource.getResourceAsStream(mapperPath);
//dom4j解析
// 因为解析完成后的MappedStatement要放在Configuration里,所以传入一个configuration进去
XMLMapperBuild xmlMapperBuild = new XMLMapperBuild(configuration);
xmlMapperBuild.parse(resourceAsStream);
}
return configuration;
}
}
3.1 解析Mapper.xml文件:
package com.dxh.config;
import com.dxh.pojo.Configuration;
import com.dxh.pojo.MappedStatement;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.InputStream;
import java.util.List;
public class XMLMapperBuild {
private Configuration configuration;
public XMLMapperBuild(Configuration configuration) {
this.configuration = configuration;
}
public void parse(InputStream inputStream) throws DocumentException {
Document document = new SAXReader().read(inputStream);
Element rootElement = document.getRootElement();
String namespace = rootElement.attributeValue("namespace");
List list = rootElement.selectNodes("//select");
for (Element element : list) {
String id = element.attributeValue("id");
String resultType = element.attributeValue("resultType");
String paramterType = element.attributeValue("paramterType");
String sqlText = element.getTextTrim();
MappedStatement mappedStatement = new MappedStatement();
mappedStatement.setId(id);
mappedStatement.setParamterType(paramterType);
mappedStatement.setResultType(resultType);
mappedStatement.setSql(sqlText);
String key = namespace+"."+id;
configuration.getMappedStatementMap().put(key,mappedStatement);
}
}
}
很容易理解,因为我们解析后要返回Configuration
对象,所以我们需要声明一个Configuration 并初始化。
我们把加载文件后的流传入,通过dom4j解析,并通过ComboPooledDataSource
(C3P0连接池)生成我们需要的DataSource
,并存入Configuration对象中。
Mapper.xml解析方式同理。
3.2 创建SqlSessionFactoryBuilder类:有了上述两个解析方法后,我们创建一个类,用来调用这个方法,同时这个类返回SqlSessionFacetory
SqlSessionFacetory:用来生产sqlSession:sqlSession就是会话对象(工厂模式 降低耦合,根据不同需求生产不同状态的对象)
package com.dxh.sqlSession;
import com.dxh.config.XMLConfigBuild;
import com.dxh.pojo.Configuration;
import org.dom4j.DocumentException;
import java.beans.PropertyVetoException;
import java.io.InputStream;
public class SqlSessionFacetoryBuild {
public SqlSessionFacetory build(InputStream in) throws DocumentException, PropertyVetoException {
//1. 使用dom4j解析配置文件,将解析出来的内容封装到configuration中
XMLConfigBuild xmlConfigBuild = new XMLConfigBuild();
Configuration configuration = xmlConfigBuild.parseConfig(in);
//2. 创建sqlSessionFactory对象 工厂类:生产sqlSession:会话对象,与数据库交互的增删改查都封装在sqlSession中
DefaultSqlSessionFactory sqlSessionFacetory = new DefaultSqlSessionFactory(configuration);
return sqlSessionFacetory;
}
}
4. 创建SqlSessionFacetory接口和实现类
基于开闭原则我们创建SqlSessionFacetory接口和实现类DefaultSqlSessionFactory
接口中我们定义openSession()
方法,用于生产SqlSession
package com.dxh.sqlSession;
public interface SqlSessionFacetory {
public SqlSession openSession();
}
package com.dxh.sqlSession;
import com.dxh.pojo.Configuration;
public class DefaultSqlSessionFactory implements SqlSessionFacetory{
private Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
@Override
public SqlSession openSession() {
return new DefaultSqlSession(configuration);
}
}
同样我们在DefaultSqlSessionFactory
中传入Configuration
,Configuration需要我们一直往下传递
5.创建SqlSession接口以及它的实现类
在接口中,我定义两个方法:
因为参数类型和个数我们都不知道,所以我们使用泛型,同时,传入statementId
(namespace、. 、id 组成)
package com.dxh.sqlSession;
import java.util.List;
public interface SqlSession {
//查询多条
public List selectList(String statementId,Object... params) throws Exception ;
//根据条件查询单个
public T selectOne(String statementId,Object... params) throws Exception;
}
package com.dxh.sqlSession;
import com.dxh.pojo.Configuration;
import java.util.List;
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
public DefaultSqlSession(Configuration configuration) {
this.configuration = configuration;
}
@Override
public List selectList(String statementId, Object... params) throws Exception {
//将要完成对simpleExecutor里的query方法调用
SimpleExecutor simpleExecutor = new SimpleExecutor();
List