mybatis学习笔记(手写持久层框架)
1.原生JDBC存在问题
1)数据库配置信息硬编码;频繁创建释放数据库连接
// 加载数据库驱动
Class.forName("com.mysql.jdbc.Driver");
// 通过驱动管理类获取数据库链接
connection =DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?
characterEncoding=utf-8", "root", "root");
解决:配置文件;连接池
2)sql,设置参数,获取结果集参数硬编码问题
// 定义sql语句?表示占位符
String sql = "select * from user where username = ?";
// 获取预处理statement
preparedStatement = connection.prepareStatement(sql);
// 设置参数,第⼀个参数为sql语句中参数的序号(从1开始),第⼆个参数为设置的参数值
preparedStatement.setString(1, "tom");
// 向数据库发出sql执⾏查询,查询出结果集
resultSet = preparedStatement.executeQuery();
解决:配置文件
3)手动封装结果集,过程繁琐
实体类属性较多的时候会影响
while (resultSet.next()) {
int id = resultSet.getInt("id");
String username = resultSet.getString("username");
// 封装User
user.setId(id);
user.setUsername(username);
}
解决:反射,内省(需要学习)*
反思:好的代码应该是抽象的,可复用的,这也是编程需要的重要思想
2.自定义持久层框架设计
针对原生JDBC的问题以及提出的解决思路,对持久层框架进行设计。
1)使用端(项目):引入自定义持久层框架的jar包
1:提供两部分配置信息:数据库配置信息;sql配置信息、参数类型、返回值类型
使用配置文件提供
(1) .sqlMapConfig.xml:数据库配置信息
(2) . mapper.xml: 存放sql配置信息
2)框架本身(工程):本质是对jdbc进行封装
1:根据文件路径,加载配置文件成字节输入流,存储在内存中
创建Resource类 方法:Inputstream getResourceAsStream(String path)
2:创建两个javaBean(容器对象):存放的是对配置文件解析出来的内容
Configuration:核心配置类:存放sqlMapConfig.xml解析内容
MappedStatement:映射配置类 :存放mapper.xml解析内容
3:解析配置文件 :dom4j
创建类:SqlSessionFactoryBuilder 方法:build(InputStream in)
第一:使用dom4j解析配置文件,讲解析出来的内容封装到容器对象中
第二:创建SqlSessionFactory对象;生产Sqlsession;会话对象(工厂模式)
4:创建SqlSessionFactory接口以及实现类DefaultSqlSessionFactory
openSession:生产SqlSession
5:创建SqlSession接口以及实现类DefaultSession
定义对数据库的CURD操作:selectList(); selectone(); update(); delete()
6:创建Executor接口及实现类SimpleExecutor实现类
query(Configuration,MappedStatement,Object…params)(JDBC封装)
3.测试类编写
根据分析思路编写配置文件
1) : sqlMapConfig.xml
<configuration>
<!--数据库配置信息-->
<dataSource>
<property name="driverClass" value="com.mysql.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql:///zmx_mybatis"></property>
<property name="username" value="root"></property>
<property name="password" value="root"></property>
</dataSource>
<!--存放mapper.xml的全路径-->
<mapper resource="UserMapper.xml"></mapper>
</configuration>
2): UserMapper.xml (关联实体类)
<mapper namespace ="user">
<!--sql的唯一标识:由namespace.id 来决定-->
<select id="selectList" resultType="com.zmx.pojo.User">
select * from user
</select>
<!--
User user = new User();
user.setId(1);
user.setName("zhangsan");
-->
<select id="selectOne" resultType="com.zmx.pojo.User" paramType="com.zmx.pojo.User">
<!--select * from user where id= ? and name = ?-->
select * from user where id= #{id} and name = #{name}
</select>
</mapper>
4.Resource类的定义
使用端:应用本身
框架端:被使用端调用(mybatis)
1).新建module,创建类(框架端)
public class Resources {
//根据配置文件路径 将配置文件加载成字节输入流 存储在内存中
public static InputStream getResourceAsStream(String path){
InputStream resourceAsStram = Resources.class.getClassLoader().getResourceAsStream(path);
return resourceAsStram;
}
}
2).在使用端引入打包好的框架端,创建test类引入方法
public class IPersistenceTest {
public void test(){
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
}
}
反思:对maven的理解加深
框架端的pom文件信息
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zmx</groupId>
<artifactId>IPersistence</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>
使用端引入打包的依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zmx</groupId>
<artifactId>Ipersistence_Test</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<!--引入自定义持久层框架的依赖-->
<dependencies>
<dependency>
<groupId>com.zmx</groupId>
<artifactId>IPersistence</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
5.容器对象
1).存放的配置信息对应文件(sqlMapConfig.xml)
public class Configuration {
//数据源 存放数据库配置信息
private DataSource dateResource;
/**
* key:statementId
* value:封装好的mappedStatement对象
* */
Map<String,MappedStatement> mappedStatementMap = new HashMap<>();
}
public class MappedStatement {
//id标识
private String id;
//返回值类型
private String resultType;
//参数值类型
private String paramType;
//sql语句
private String sql;
}
实体类:DataSource:数据库配置
MappedStatement :sql语句
6-7.解析配置文件sqlMapConfig.xml;解析映射配置文件mapper.xml
1).创建XMLConfigBuilder实体类解析配置文件
首先解析sqlMapConfig.xml
public class XMLConfigBuilder {
private Configuration configuration;
public XMLConfigBuilder() {
this.configuration = new Configuration();
}
/*
* 该方法解析配置文件,封装到Configuration
* */
public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {
Document document = new SAXReader().read(inputStream);
Element rootElement = document.getRootElement();
List<Element> elements = rootElement.selectNodes("//property");
Properties properties = new Properties();
for (Element element : elements) {
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.setDateResource(comboPooledDataSource);
//解析mapper.xml 拿到路径-字节输入流 dom4j进行解析
List<Element> mapperElements = rootElement.selectNodes("//mapper");
Properties mappereProperties = new Properties();
for (Element element : mapperElements) {
String mapperPath = element.attributeValue("resource");
InputStream resourceAsStream = Resources.getResourceAsStream(mapperPath);
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
xmlMapperBuilder.parse(resourceAsStream);
}
return configuration;
}
}
解析sqlMapConfig.xml之后解析mapper.xml
public class XMLMapperBuilder {
private Configuration configuration;
public XMLMapperBuilder(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<Element> sqlElement = rootElement.selectNodes("//select");
for (Element element : sqlElement) {
String id = element.attributeValue("id");
String resultType = element.attributeValue("resultType");
String paramType = element.attributeValue("paramType");
String sqlText = element.getTextTrim();
MappedStatement mappedStatement = new MappedStatement();
mappedStatement.setId(id);
mappedStatement.setParamType(paramType);
mappedStatement.setResultType(resultType);
mappedStatement.setSql(sqlText);
String key = namespace+"."+id;
configuration.getMappedStatementMap().put("",mappedStatement);
}
}
}
反思:学习过程跟随思路走,重要理解其逻辑,因果关系
8-9.会话对象Sqlsession类定义以及方法定义
1).sqlSession类的定义
用来创建会话对象,里边包含封装的jdbc方法(增删改查),根据传递过来的唯一标识以及 结果集/参数集 返回对应的结果。
public interface SqlSession {
//查询所有
public <E> List<E> selectList(String statementId,Object...params);
//根据条件查询单个
public <T> T selectOne(String statementId,Object...params);
}
2)方法的定义
方法不止查询,也可以添加修改删除等操作 selectOne可以视作selectList的某种形式
public class DefaultSqlSession implements SqlSession{
private Configuration configuration;
public DefaultSqlSession(Configuration configuration) {
this.configuration = configuration;
}
@Override
public <E> List<E> selectList(String statementId, Object... params) {
//完成对simpleExecutor里的query方法调用
SimpleExecutor simpleExecutor = new SimpleExecutor();
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
List<Object> query = simpleExecutor.query(configuration, mappedStatement, params);
return (List<E>) query;
}
@Override
public <T> T selectOne(String statementId, Object... params) {
List<Object> objects = selectList(statementId, params);
if(objects.size() == 1) {
return (T) objects.get(0);
}else{
throw new RuntimeException("查询结果过多或结果为空");
}
}
}
3)创建SqlSessionFactory
public class sqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException {
//第一:使用dom4j解析配置文件,解析的内容封装到Configuration
XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
Configuration configuration = xmlConfigBuilder.parseConfig(in);
//第二:创建sqlSessionFactory对象 工厂类:生产sqlSession会话对象
DefaultSqlSessionFactory defaultSqlSessionFactory =
new DefaultSqlSessionFactory(configuration);
return defaultSqlSessionFactory;
}
}
反思:在openSession时候需要先创建sqlSessionFactoryBuilder调用build方法创建SqlSessionFactory;SqlSessionFactory中包含了SqlSession接口,接口中封装jdbc方法进行调用。
10-12. 查询对象query定义–参数设置实现–封装返回结果集实现
内部封装JDBC
public class SimpleExecutor implements Executor{
@Override
public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception {
//1.注册驱动 获取链接
Connection connection = configuration.getDateResource().getConnection();
//2.获取sql语句 select * from user where id= #{id} and name = #{name}
//转换sql 需要对#{} 的值进行解析存储
String sql = mappedStatement.getSql();
BoundSql boundSql = getBoundSql(sql);
//3.获取预处理对象 preparedStatement
PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());
//4.设置参数
String paramType = mappedStatement.getParamType();
Class<?> paramTypeClass = getClassType(paramType);
List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
for (int i = 0; i < parameterMappingList.size(); i++) {
ParameterMapping parameterMapping = parameterMappingList.get(i);
String content = parameterMapping.getContent(); //对应#{}里边的值
//反射
Field declaredField = paramTypeClass.getDeclaredField(content);
//暴力访问
declaredField.setAccessible(true);
Object o = declaredField.get(params[0]);
preparedStatement.setObject(i+1,o);//下标从1开始
}
//5.执行sql
ResultSet resultSet = preparedStatement.executeQuery();
String resultType = mappedStatement.getResultType();
Class<?> resultTypeClass = getClassType(resultType);
Object o = resultTypeClass.newInstance();
ArrayList<Object> objects = new ArrayList<>();
//6.封装返回结果
while(resultSet.next()){
//元数据
ResultSetMetaData metaData = resultSet.getMetaData();
for (int i = 1 ;i <=metaData.getColumnCount(); i++){
//字段名
String columnName = metaData.getColumnName(i);
//获取字段的值
Object value = resultSet.getObject(columnName);
//使用反射根据数据库表和实体的对应关系,完成封装
PropertyDescriptor propertyDescriptor =
new PropertyDescriptor(columnName, resultTypeClass);
Method writeMethod = propertyDescriptor.getWriteMethod();
writeMethod.invoke(o,value);
objects.add(o);
}
}
return (List<E>) o;
}
private Class<?> getClassType(String paramType) throws ClassNotFoundException {
if(paramType != null){
Class<?> aClass = Class.forName(paramType);
return aClass;
}
return null;
}
/*
* 完成对#{}的解析
* 1.将#{}使用?进行代替
* 2.解析出#{} 内容进行存储
* */
private BoundSql getBoundSql(String sql){
//标记处理类 配置标记解析器来完成对占位符的解析处理工作
ParameterMappingTokenHandler parameterMappingTokenHandler =
new ParameterMappingTokenHandler();
GenericTokenParser genericTokenParser =
new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
//解析出的sql
String parseSql = genericTokenParser.parse(sql);
//解析出来的参数名称
List<ParameterMapping> parameterMappings =
parameterMappingTokenHandler.getParameterMappings();
BoundSql boundSql = new BoundSql(parseSql,parameterMappings);
return boundSql;
}
}
public class BoundSql {
private String sqlText; //sql语句
private List<ParameterMapping> parameterMappingList ; //#{}内部值
public BoundSql(String sqlText, List<ParameterMapping> parameterMappingList) {
this.sqlText = sqlText;
this.parameterMappingList = parameterMappingList;
}
}
13.运行测试
运行过程碰到很多问题。
1).target目录下没有resources路径下的文件
解决方法:pom文件中将 标签内容改为jar;之前为pom
2).java.lang.IllegalArgumentException: argument type mismatch 表中字段类型为integer,对象对应字段类型为string
解决方法:修改对象类型
3).java.lang.ClassCastException: class com.zmx.pojo.User cannot be cast to class java.util.List (com.zmx.pojo.User is in unnamed module of loader ‘app’; java.util.List is in module java.base of loader ‘bootstrap’)
类型转换异常 在返回结果集时,返回的是某个对象并不是list
解决方法:返回list
4).java.lang.RuntimeException: 查询结果过多或结果为空
结果集添加数据时,放进了set值得一步 导致多次添加数据,结果变多
解决方法:add方法放到循环外
解决完毕,程序正常运行!
14-15 功能扩展
1).存在问题
1.Dao层使用自定义持久层框架,存在代码重读,操作过程模板重复(加载配置文件,创建sqlSessionFactory,生产sqlSession)
//此处为何要放在不同的jar包 为何不直接在框架包使用该流
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new sqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
-
statementId存在硬编码问题
List<User> users = sqlSession.selectList("user.selectList");
解决思路:使用代理模式生成Dao层接口的代理实现类
2). 在DefaultSqlSession创建新的方法(invoke)
代理对象调用接口中任意方法,都会执行invoke方法
IUserDao userDao = sqlSession.getMapper(IUserDao.class); List<User> all = userDao.findAll();
@Override
public <T> T getMapper(Class<?> mapperClass) {
//使用jdk动态代理为dao接口生成代理对象 并返回
Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(),
new Class[]{mapperClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//proxy 当前代理对象的应用 method 当前被调用方法的引用 args 传递的参数
//根据不同情况 选择调用selectList或者selectOne
//准备参数 1.statementId sql语句唯一标识 namespace.id = 接口全限定名.方法名
//方法名:findAll
String name = method.getName();
String className = method.getDeclaringClass().getName();
String statementId =className+"."+name;
//准备参数 2.params 传递的参数 Object[] args
//获取被调用方法的返回值类型
Type genericReturnType = method.getGenericReturnType();
//判断是否进行了 泛型类型参数化
if(genericReturnType instanceof ParameterizedType){
return selectList(statementId,args);
}
return selectOne(statementId,args);
}
});
return (T) proxyInstance;
}