简介
说到持久层框架,我们很快能想到hibernate、mybatis。Hibernate是全自动的持久层框架,而mybatis则是半自动的。那么两者有何区别?下面先简单认识一下这两个框架的异同,以便更好的理解下面阐述的自定义持久层框架的设计思想,毕竟下面的自定义框架实际就是通过阅读mybatis源码,取其核心设计,去掉细枝旁叶而设计的一个半自动持久层框架。
ORM全称Object/Relation Mapping:表示对象-关系映射的缩写。
Hibernate:全自动的持久层ORM框架。Hibernate是一个开源的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,它将POJO(Plain Ordinary Java Object)与数据库表建立映射关系,是一个全自动的ORM框架,hibernate可以自动生成SQL语句,自动执行,使得Java程序员可以随心所欲的使用对象编程思维来操纵数据库。
Mybatis:MyBatis 是一款优秀的半自动持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO为数据库中的记录。
使用 Hibernate 查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。而 MyBatis 在查询关联对象或关联集合对象时,需要手动编写 sql 来完成,所以,称之为半自动 ORM 映射工具。
既然无论是全自动的hibernate,或是半自动的mybatis,其本质都是对JDBC进行了封装,使Java程序员可以使用对象编程思维来操纵数据库。
下面我们不妨看看使用JDBC编程是怎样的,直接使用JDBC编程会有什么问题,找到问题,解决问题,然后提出针对问题的解决方案。
jdbc操作存在的问题
一个使用原始jdbc对数据进行查询的案例
package com.kmning.wallet.jdbc;
import com.kmning.wallet.jdbc.pojo.User;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
/**
* @author kangming.ning
* @date 2021/5/2 9:16
*/
public class JdbcQueryDemo {
public static void main(String[] args) {
Connection connection=null;
PreparedStatement preparedStatement=null;
ResultSet resultSet =null;
try {
//加载数据库驱动
//Class.forName("com.mysql.jdbc.Driver");
//过驱动管理类获取数据库链接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/yourdb?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai", "root", "root");
//编写sql语句
String sql="select * from user where username=? or telphone=?";
//获取预处理statement
preparedStatement = connection.prepareStatement(sql);
//设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值
preparedStatement.setString(1,"aa");
preparedStatement.setString(2,"666");
//向数据库发出sql执行查询,查询出结果集
resultSet = preparedStatement.executeQuery();
//遍历结果集,封装数据
List<User> userList=new ArrayList<>(10);
while (resultSet.next()){
int id = resultSet.getInt("id");
String username = resultSet.getString("username");
String telphone = resultSet.getString("telphone");
//封装结果集
User user = new User();
user.setId(id);
user.setUsername(username);
user.setTelphone(telphone);
userList.add(user);
}
System.out.println(userList);
}catch (Exception e){
e.printStackTrace();
}finally {
//释放资源
if (resultSet!=null){
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (preparedStatement!=null){
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection!=null){
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
代码非常简单且常规,JDBC操作无非都类似于上面的套路。通过驱动获取数据库连接,通过PreparedStatement预编译SQL语句,设置参数,然后向数据库发出sql执行查询,查询出结果集,遍历结果集,将结果集封装到POJO对象集合。当然,最后还得将相关资源释放。
但在实际的企业应用中,数据库的表少则几十个,多则几百个,如果使用类似于上述的方式去操作数据库,不仅前期开发工作巨大,而且后期的维护也将是一场灾难。那么上述代码存在哪些问题?让我们带着疑问对代码进行分析,然后找出问题所在,然后找到解决方案去解决它们。
以上代码存在的问题:
- 数据库配置信息存在硬编码
- 数据库连接创建、释放频繁造成系统资源浪费,从而影响系统性能
- Sql语句在代码中硬编码,造成代码不易维护,实际应用中sql变化的可能较大,sql变动需要改变java代码
- 使用preparedStatement向占位符号传参数存在硬编码,因为sql语句的where条件不一定,可能多也可能少,修改sql还要修改代码,系统不易维护
- 对结果集解析存在硬编码(查询列名),sql变化导致解析代码变化,系统不易维护,如果能将数据库记录封装成pojo对象解析比较方便
解决思路:
- 数据库配置信息提供配置文件,避免改动连接等配置信息需要重新编译代码
- 使用数据库连接池初始化连接资源
- 将sql语句、设置参数、获取结果集参数抽取到xml配置文件中
- 使用反射、内省等底层技术,自动将实体与表进行属性与字段的自动映射
问题找到了,也整理出了解决思路,那么就可以着手设计框架了。
自定义mybatis框架
我们整理出了解决思路,接下来需要编写一个通用的解决方案,也就是框架,去解决问题。下面我们整理一下实现这样一个框架需要去做哪些工作。
首先是自定义框架肯定是独立的一个jar,然后提供给客户端使用。我们可以将开发工作分为两部分,一是框架需要做哪些东西,二是使用端(项目)需要做哪些东西。为了描述与理解方便,下面称框架端为mybatis-custom框架。
mybatis-custom框架负责对jdbc进行封装,提供基本操作接口,并对结果集和pojo实体进行映射,而客户端(项目)则需要提供数据库配置信息、sql配置信息(如UserMapper.xml)。使用端提供xxMapper.xml,意味着对sql的编写的控制权还是属于用户的(半自动映射框架)。
使用端
提供两部分配置信息:一是数据库配置信息。二是sql mapper配置信息,mapper文件定义了sql语句、参数类型、返回值类型、结果返回类型等信息。
使用配置文件来提供这两部分配置信息
- sqlMapConfig.xml:存放数据库配置信息、同时存放mapper.xml的全路径(这样在方便在解析sqlMapConfig时直接解析mapper.xml,相关信息保存到Configuration)。样例如下
<configuration>
<!--数据库配置信息-->
<dataSource>
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql:///yourdb?serverTimezone=Asia/Shanghai"></property>
<property name="username" value="root"></property>
<property name="password" value="root"></property>
</dataSource>
<!--存放mapper.xml的全路径-->
<mapper resource="UserMapper.xml"></mapper>
<mapper resource="ProductMapper.xml"></mapper>
</configuration>
自定义持久层框架端
创建框架工程,引入相关maven依赖,根据上面的分析,框架需要解析xml,用连接池解决连接创建关闭频繁问题,那么引入相关依赖如下
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.20</version>
</dependency>
根据自定义的核心配置文件sqlMapConfig.xml和userMapper.xml文件可以看出,核心配置文件里面有连接池信息,mapper信息,那么解析成核心配置类后也应该记录这些信息,方便后面使用。而userMapper.xml记录了数据表的sql操作的配置文件肯定也要映射成某个类, 我们根据上面定义的userMapper.xml配置信息抽象成成类即可。
使用端已经使用数据库xml配置文件l和SQL xml配置文件来解决相关的硬编码问题,接下来就是框架端的工作了。框架端本质就是需要对JDBC代码进行封装。框架端解析客户端提供的数据库配置文件以创建数据库连接,当然,这里我们使用数据库连接池来解决频繁创建、释放连接问题。框架端解析xxMapper.xml配置文件,以将每个Mapper.xml文件对应的SQL标签解析成特定的配置对象(比如叫MappedStatement),最后我们把这些解析出来的配置对象保存到一个全局的配置类中(比如叫Configuration)。这个配置类在mybatis-custom中起到了基石的作用,毕竟这个框架最终是要操作数据库的,而这个配置类对象为我们提供了数据库操作的相关信息。
Configuration
package com.kmning.mybatis.config;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @author kangming.ning
* @date 2021/5/2 17:18
*/
public class Configuration {
/**
* 保存连接池信息
* */
private DataSource dataSource;
/**
* key: namespace+id,唯一标识一条sql
* value:MappedStatement对象 通过key找到当前业务表的mapper,所以在定义dao接口时,接口的方法全限定名称
* 和其对应的mapper中的namespace+id 必须是一致的,框架会根据当前的查询方法的全限定方法名称去找到对应的MappedStatement
* 对象
* 指定初始Map大小,默认16
* */
private Map<String,MappedStatement> mappedStatementMap=new HashMap<>(100);
public DataSource getDataSource() {
return dataSource;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public Map<String, MappedStatement> getMappedStatementMap() {
return mappedStatementMap;
}
}
可以看出,核心配置类主要保存连接池信息、另外用一个Map保存了所有数据库操作信息(简单的理解就是Mapper.xml里面的SQL标签对应的对象),其中key是namespace+id,唯一标识一条sql。namespace(Mapper.xml中定义的)就是我们定义的DAO接口全限定名,id则是具体的接口名称。这样一来,当我们调用DAO某个接口时,框架就能找到调用接口对应的Mapper.xml文件对应的SQL标签(或者说MappedStatement对象),从而解析出需要执行的SQL。
到此,我们已经准备好了框架执行SQL的一切条件,接下来应该要交给PreparedStatement去执行,然后得到结果,对结果进行封装等。我们定义一个执行器接口,提供查询、增删查改接口。
Executor接口
定义一个执行器接口,负责和数据库进行交互,提供查询、增删查改接口。
package com.kmning.mybatis.sqlSession;
import com.kmning.mybatis.config.Configuration;
import com.kmning.mybatis.config.MappedStatement;
?
import java.util.List;
?
/**
* 定义数据库查询、更新接口
* @author kangming.ning
* @date 2021/5/2 20:33
*/
public interface Executor {
?
/**
* 查询接口,使用jdbc与数据通讯,封装数据映射到pojo类返回
* @param configuration 核心配置对象
* @param mappedStatement 封装了一条sql相关信息的对象
* @param params sql入参对象
* @return 已自动映射成pojo类的查询结果
* */
<E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception;
?
/**
* 更新接口,使用jdbc与数据通讯,封装数据映射到pojo类返回
* @param configuration 核心配置对象
* @param mappedStatement 封装了一条sql相关信息的对象
* @param params sql入参对象
* @return 影响行数
* */
int update(Configuration configuration, MappedStatement mappedStatement,Object... params) throws Exception;
}
接口定义了一个列表查询方法,一个更新方法,然后需要传递核心配置对象与MappedStatement对象,这样才能根据配置信息解析出要执行的SQL,将参数设置到PreparedStatement。在查询接口中,要求返回一个List<E>,查询结果需要封装到泛型列表对象进行返回。从上面的Mapper.xml的定义样例中我们大概能猜到这个接口的实现需要做的事情,比如根据MappedStatement解析SQL、设置SQL参数、查询结果,通过反射或内省将数据库查询结果映射到Java对象中。更新接口也是做差不多的事情,只是不需要映射结果集到对象,只需要返回影响行数即可。
MappedStatement
将userMapper.xml抽象成MappedStatement类,用于记录相关信息,命名空间加相关查询标签的id属性可以唯一确定一个sql,另外入参类型,结果类型,标签里面的sql文本为重要信息,应该抽象成字段。
package com.kmning.mybatis.config;
/**
* 对某个mapper的一条sql作封装,即系统有多少条sql,将会产生多少个MappedStatement对象
* 注意下面的属性名称应该和mapper.xml定义的sql标签的属性对应起来,方便理解的同时也规范
* @author kangming.ning
* @date 2021/5/2 17:05
*/
public class MappedStatement {
/**
* 唯一标签某个mapper的某条sql,用namespace+id,方便针对不同表映射到不同的mapper.xml
* 总不能把所有表放同一个mapper吧
* */
private String id;
/**
* 入参类型全限定类路径
* */
private String parameterType;
/**
* 结果类型全限定类路径
* */
private String resultType;
/**
* 标签里面的sql语句,此语句后续需要进一步解析(如自定义入参标签#{}等)
* */
private String sql;
//get set方法
...
}
SimpleExecutor
package com.kmning.mybatis.sqlSession;
import com.kmning.mybatis.config.BoundSql;
import com.kmning.mybatis.config.Configuration;
import com.kmning.mybatis.config.MappedStatement;
import com.kmning.mybatis.utils.GenericTokenParser;
import com.kmning.mybatis.utils.ParameterMapping;
import com.kmning.mybatis.utils.ParameterMappingTokenHandler;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
/**
* @author kangming.ning
* @date 2021/5/2 20:46
*/
public class SimpleExecutor implements Executor{
@Override
public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception{
PreparedStatement preparedStatement = getPreparedStatement(configuration, mappedStatement, params);
ResultSet resultSet = preparedStatement.executeQuery();
//封装结果集
String resultType = mappedStatement.getResultType();
Class<?> resultClassType = getClassType(resultType);
//遍历结果集封装到resultType对应的pojo类对象中
List<Object> resultList=new ArrayList<>(10);
while (resultSet.next()){
Object resultObj = resultClassType.newInstance();
//通过元数据找到字段名和其对应的值 这样才能准确的设置到返回类型对象对应的属性中
ResultSetMetaData metaData = resultSet.getMetaData();
//注意是从第一列开始
for (int i = 1; i <= metaData.getColumnCount(); i++) {
String columnName = metaData.getColumnName(i);
Object columnValue = resultSet.getObject(columnName);
//使用反射或者内省,根据数据库表和实体的对应关系,完成封装
//使用内省(Introspector)
PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName,resultClassType);
//如setUsername
Method writeMethod = propertyDescriptor.getWriteMethod();
//调用setter方法设置对象当前属性的值
writeMethod.invoke(resultObj,columnValue);
//使用反射(反射和内省,选一种即可)
/* Field declaredField = resultClassType.getDeclaredField(columnName);
declaredField.setAccessible(true);
declaredField.set(resultObj,columnValue);*/
}
resultList.add(resultObj);
}
return (List<E>) resultList;
}
@Override
public int update(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception{
PreparedStatement preparedStatement = getPreparedStatement(configuration, mappedStatement, params);
preparedStatement.execute();
int updateCount = preparedStatement.getUpdateCount();
return updateCount;
}
/**
* 通用获取PreparedStatement方法
* */
private PreparedStatement getPreparedStatement(Configuration configuration, MappedStatement mappedStatement, Object... params) throws SQLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
//通过核心配置获取连接池中的一个连接
Connection connection = configuration.getDataSource().getConnection();
//获取sql 获取sql语句 : select * from user where id = #{id} and username = #{username}
String sql = mappedStatement.getSql();
//转换sql语句: select * from user where id = ? and username = ? ,转换的过程中,还需要对#{}里面的值进行解析存储
BoundSql boundSql = getBoundSql(sql);
//获取预处理对象:preparedStatement
PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());
//设置参数
//获取参数的全路径
String parameterType = mappedStatement.getParameterType();
//获取入参Class对象
Class<?> paramClassType = getClassType(parameterType);
//根据通过反射获取入参对象中对应了 #{id}中的id和#{username}的username字段的值,设置到preparedStatement
//这个列表是有序的,比如示例sql中,下面列表第0个元素就是id,第1一个元素就是username
List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
for (int i = 0; i < parameterMappingList.size(); i++) {
ParameterMapping parameterMapping = parameterMappingList.get(i);
//其实保存的就是入参对象的字段名
String filedName = parameterMapping.getContent();
//反射获取入参对象当前字段的值
Field declaredField = paramClassType.getDeclaredField(filedName);
//忽视修饰符号 直接访问
declaredField.setAccessible(true);
//注意下面的params[0]其实就是入参对象 如User对象,根据这个对象直接获取到了对象的值
Object fieldValue = declaredField.get(params[0]);
//设置preparedStatement的入参 第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值
preparedStatement.setObject(i+1,fieldValue);
}
return preparedStatement;
}
/**
* 完成对#{}的解析工作:1.将#{}使用?进行代替,2.解析出#{}里面的值进行存储
* @param sql 如 select * from user where id = #{id} and username = #{username}
* @return
*/
private BoundSql getBoundSql(String sql) {
//标记处理类:配置标记解析器来完成对占位符的解析处理工作
ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
//解析出来的sql select * from user where id = ? and username = ?
String parseSql = genericTokenParser.parse(sql);
//#{}里面解析出来的参数名称 id username 通过取入参的查询对象的id和username值分别设置为第一个?和第二个?的参数值进行查询(结果是有序的)
List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
BoundSql boundSql = new BoundSql(parseSql,parameterMappings);
return boundSql;
}
/**
* 根据class全路径反射获取Class对象
* */
private Class<?> getClassType(String clazzPath) throws ClassNotFoundException {
if (null!=clazzPath){
Class<?> aClass = Class.forName(clazzPath);
return aClass;
}
return null;
}
}
这个执行器是自动映射的关键。当调用查询接口时,框架使用核心配置对象,将用户传进来的参数对象,通过反射获取值,配合Mapper.xml解析出的内容给SQL设置参数,执行后将结果集自动映射到resultType对象,最终返回结果列表。这里的参数值获取和结果数据映射都使用了反射或内省技术,很好的解决了硬编码等问题。反射,程序员的快乐。
至此,我们基本上完成了框架的配置文件解析(框架初始化时解析到configuration对象)、SQL参数设置、执行结果解析封装。接下来我们需要提供一个会话对象给用户使用,提供增删查改等接口。这样用户只需要知道这个会话对象怎么使用的就行,不必理会其内部是如何实现的。
SqlSession
package com.kmning.mybatis.sqlSession;
import java.util.List;
/**
* sql会话接口,定义sql基本接口
* @author kangming.ning
* @date 2021/5/2 18:58
*/
public interface SqlSession {
/**
* 根据statementId查询数据列表
* @param statementId mapper.xml中唯一标识一条sql
* @param params 查询参数
* */
<E> List<E> selectList(String statementId, Object... params) throws Exception;
/**
* 根据statementId查询一条数据
* @param statementId mapper.xml中唯一标识一条sql
* @param params 查询参数
* */
<T> T selectOne(String statementId,Object... params) throws Exception;
/**
* 根据statementId 更新、新增、删除数据
* @param statementId mapper.xml中唯一标识一条sql
* @param params 查询参数
* */
Integer excuteUpdate(String statementId, Object... params) throws Exception;
/**
* 为Dao接口生成动态代理类
* */
<T> T getMapper(Class<?> mapperClass);
}
从接口的声明中可以看出,用户只需要知道什么是statementId,然后传SQL参数进去即可,至于内部如何实现则完全不需要关心。
DefaultSqlSession
package com.kmning.mybatis.sqlSession;
import com.kmning.mybatis.config.Configuration;
import com.kmning.mybatis.config.MappedStatement;
import java.lang.reflect.*;
import java.util.List;
/**
* 默认SqlSession实现
* @author kangming.ning
* @date 2021/5/2 19:36
*/
public class DefaultSqlSession implements SqlSession{
private Configuration configuration;
private Executor executor;
public DefaultSqlSession(Configuration configuration) {
this.configuration = configuration;
executor=new SimpleExecutor();
}
@Override
public <E> List<E> selectList(String statementId, Object... params) throws Exception {
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
List<E> resultList = executor.query(configuration, mappedStatement, params);
return resultList;
}
@Override
public <T> T selectOne(String statementId, Object... params) throws Exception {
List<Object> results = selectList(statementId, params);
if (results.size()==1){
return (T) results.get(0);
}else {
throw new RuntimeException("查询结果为空或者返回结果过多");
}
}
@Override
public Integer excuteUpdate(String statementId, Object... params) throws Exception {
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
int update = executor.update(configuration, mappedStatement, params);
return update;
}
@Override
public <T> T getMapper(Class<?> mapperClass) {
Object mapperObj = Proxy.newProxyInstance(mapperClass.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 底层都还是去执行JDBC代码 //根据不同情况,来调用selctList或者selectOne
// 准备参数 1:statmentid :sql语句的唯一标识:namespace.id= 接口全限定名.方法名
// 方法名:findAll
String clazzName = method.getDeclaringClass().getName();
String methodName = method.getName();
String statementId=clazzName+"."+methodName;
// 准备参数2:params:args
// 获取被调用方法的返回值类型
Type genericReturnType = method.getGenericReturnType();
//判断是否进行了 泛型类型参数化
if (genericReturnType instanceof ParameterizedType){
List<Object> selectList = selectList(statementId, args);
return selectList;
}
//处理添加、修改、删除的情况
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
String sql = mappedStatement.getSql();
if (sql.startsWith("update")||sql.startsWith("insert")
||sql.startsWith("delete")||
sql.startsWith("UPDATE")||sql.startsWith("INSERT")
||sql.startsWith("DELETE")){
Integer count = excuteUpdate(statementId, args);
return count;
}
return selectOne(statementId,args);
}
});
return (T) mapperObj;
}
}
可以看出,实现方法无非是根据statementId将MappedStatement对象从框架初始化时就已经存下来的Map中取出,然后调用Executor对象去执行,得到结果。这些步骤对于不同的SQL查询基本都是一样的,所以不必让用户去写,提供接口即可。当然,细心的读者可能注意到上面定义了一个getMapper接口并且给出了实现代码。其实这就是mybatis的代理模式的实现原理。mybatis的代理是使用JDK的动态代理进行实现的,使用动态代理用户就没必要对Dao接口进行实现,因为其实现代码都是大同小异,甚至可以认为是基本重复的,在文末的测试类中我会给出一个Dao实现类,读者可以观察其实现代码。
SqlSessionFactory
有了Sqlsession操作接口及实现就已经可以使用Sqlsession对象去和数据库进行交互了。但使用Sqlsession是需要在构造函数里传入configuration对象的。不可能由客户端自己去创建这个对象。所以我们需要一个生产Sqlsession对象的工厂接口,将configuration对象保存在工厂类里面。另外在工厂Builder里面提供创建工厂对象方法。通常系统只需要创建一次即可,有了工厂生产Sqlsession对象就比较方便了。
package com.kmning.mybatis.sqlSession;
/**
* 定义生产SqlSession接口
* @author kangming.ning
* @date 2021/5/2 22:44
*/
public interface SqlSessionFactory {
/**
* 获取一个sqlsession会话对象
* */
SqlSession openSession();
}
DefaultSqlSessionFactory
/**
* 默认工厂实现
* @author kangming.ning
* @date 2021/5/2 22:45
*/
public class DefaultSqlSessionFactory implements SqlSessionFactory{
private Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
@Override
public SqlSession openSession() {
return new DefaultSqlSession(configuration);
}
}
会话对象解决了,但核心配置类还是需要传进来。这是必然的,因为这个配置类是框架的核心,并且依靠用户的配置文件来生成。所以必须要有个接口能让用户传配置文件的流进来,然后使用解析类去解析配置文件,封装到核心配置文件里,然后就可以将核心配置对象传进工厂里面生产会话对象进行使用了。
SqlSessionFactoryBuilder
package com.kmning.mybatis.sqlSession;
import com.kmning.mybatis.config.Configuration;
import com.kmning.mybatis.parser.XMLConfigBuilder;
import org.dom4j.DocumentException;
import java.io.InputStream;
/**
* 构建SqlSessionFactory默认实现工厂对象
* @author kangming.ning
* @date 2021/5/2 22:52
*/
public class SqlSessionFactoryBuilder {
public static SqlSessionFactory build(InputStream inputStream) throws DocumentException {
//第一:使用dom4j解析配置文件,将解析出来的内容封装到Configuration中
XMLConfigBuilder xmlConfigBuilder=new XMLConfigBuilder();
Configuration configuration = xmlConfigBuilder.parseConfig(inputStream);
//第二:创建sqlSessionFactory对象:工厂类:生产sqlSession:会话对象
DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);
return defaultSqlSessionFactory;
}
}
上面的XMLConfigBuilder就是用来解析核心配置文件以将相关信息保存到核心配置对象中的。基本这个方法就是提供给用户使用的了。
框架端开发流程整理
- 读取配置文件:读取完成以后以流的形式存在,我们不能将读取到的配置信息以流的形式存放在内存中,不好操作,可以创建javaBean来存储。
- (1)Configuration : 核心配置类(sqlMapConfig.xml解析出来的内容),存放数据库基本信息、Map<唯一标识,Mapper> 唯一标识:namespace + "." +id
- (2)MappedStatement:sql语句、statement类型、输入参数java类型、输出参数java类型
- 解析配置文件
- 创建sqlSessionFactoryBuilder类:
- 方法:sqlSessionFactory build():
- 第一:使用dom4j解析配置文件,将解析出来的内容封装到Configuration和MappedStatement中
- 第二:创建SqlSessionFactory的实现类DefaultSqlSession
- 创建SqlSessionFactory
- 方法:openSession() : 获取sqlSession接口的实现类实例对象
- 创建sqlSession接口及实现类:主要封装crud方法
- 方法:selectList(String statementId,Object param):查询所有
- selectOne(String statementId,Object param):查询单个
- 具体实现:封装JDBC完成对数据库表的查询操作
涉及到的设计模式:Builder构建者设计模式、工厂模式、代理模式。
使用端测试自定义框架
引入相关依赖
<dependency>
<groupId>com.kmning.mybatis</groupId>
<artifactId>mybatis-custom</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
UserMapper.xml
namespace+id可以标识一条sql
<mapper namespace="com.kmning.mybatis.test.dao.UserDao">
<!--sql的唯一标识:namespace.id来组成 : statementId-->
<select id="findAll" resultType="com.kmning.mybatis.test.pojo.User" >
select * from user
</select>
<select id="findByCondition" resultType="com.kmning.mybatis.test.pojo.User" parameterType="com.kmning.mybatis.test.pojo.User">
select * from user where id = #{id} and username = #{username}
</select>
<update id="updateUserById" resultType="java.lang.Integer" parameterType="com.kmning.mybatis.test.pojo.User">
UPDATE `user` SET username=#{username},telphone=#{telphone} WHERE id=#{id};
</update>
<insert id="insertUser" resultType="java.lang.Integer" parameterType="com.kmning.mybatis.test.pojo.User">
INSERT INTO `user` VALUES(#{id},#{username},#{telphone});
</insert>
<delete id="deleteUserById" resultType="java.lang.Integer" parameterType="com.kmning.mybatis.test.pojo.User">
DELETE FROM `user` WHERE id=#{id};
</delete>
</mapper>
User实体
用于映射数据库查询记录
package com.kmning.mybatis.test.pojo;
/**
* @author kangming.ning
* @date 2021/5/2 16:43
*/
public class User {
private Integer id;
private String username;
private String telphone;
// get set
...
}
dao接口
package com.kmning.mybatis.test.dao;
import com.kmning.mybatis.test.pojo.User;
import java.util.List;
/**
* @author kangming.ning
* @date 2021/5/2 18:11
*/
public interface UserDao {
List<User> findAll() throws Exception;
User findByCondition(User user) throws Exception;
Integer insertUser(User user) throws Exception;
Integer updateUserById(User user) throws Exception;
Integer deleteUserById(User user)throws Exception;
}
创建dao接口实现类
package com.kmning.mybatis.test.dao;
?
import com.kmning.mybatis.io.Resources;
import com.kmning.mybatis.sqlSession.SqlSession;
import com.kmning.mybatis.sqlSession.SqlSessionFactory;
import com.kmning.mybatis.sqlSession.SqlSessionFactoryBuilder;
import com.kmning.mybatis.test.pojo.User;
import org.dom4j.DocumentException;
import java.io.InputStream;
import java.util.List;
/**
* 数据层实现类
* @author kangming.ning
* @date 2021/5/2 22:58
*/
public class UserDaoImpl implements UserDao{
private SqlSessionFactory sessionFactory;
public UserDaoImpl() {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
try {
sessionFactory = SqlSessionFactoryBuilder.build(resourceAsStream);
} catch (DocumentException e) {
e.printStackTrace();
}
}
public List<User> findAll() throws Exception {
SqlSession sqlSession = sessionFactory.openSession();
List<User> userList = sqlSession.selectList("com.kmning.mybatis.test.dao.UserDao.findAll");
return userList;
}
public User findByCondition(User user) throws Exception {
SqlSession sqlSession = sessionFactory.openSession();
User result = sqlSession.selectOne("com.kmning.mybatis.test.dao.UserDao.findByCondition",user);
return result;
}
public Integer insertUser(User user) throws Exception {
SqlSession sqlSession = sessionFactory.openSession();
Integer rows = sqlSession.excuteUpdate("com.kmning.mybatis.test.dao.UserDao.insertUser", user);
return rows;
}
public Integer updateUserById(User user) throws Exception {
SqlSession sqlSession = sessionFactory.openSession();
Integer rows = sqlSession.excuteUpdate("com.kmning.mybatis.test.dao.UserDao.updateUserById", user);
return rows;
}
public Integer deleteUserById(User user) throws Exception {
SqlSession sqlSession = sessionFactory.openSession();
Integer rows = sqlSession.excuteUpdate("com.kmning.mybatis.test.dao.UserDao.deleteUserById", user);
return rows;
}
}
测试类
package com.kmning.mybatis.test;
import com.kmning.mybatis.test.dao.UserDao;
import com.kmning.mybatis.test.dao.UserDaoImpl;
import com.kmning.mybatis.test.pojo.User;
import org.junit.Test;
import java.util.List;
/**
* @author kangming.ning
* @date 2021/5/2 23:15
*/
public class UserTest {
private UserDao userDao=new UserDaoImpl();
@Test
public void findAll() throws Exception {
List<User> list = userDao.findAll();
for (User user : list) {
System.out.println(user);
}
}
@Test
public void findByCondition() throws Exception {
User user = new User();
user.setUsername("aa");
user.setId(1);
User byCondition = userDao.findByCondition(user);
System.out.println(byCondition);
}
@Test
public void insertOne() throws Exception {
User user = new User();
user.setId(4);
user.setUsername("cc");
user.setTelphone("666");
Integer count = userDao.insertUser(user);
System.out.println("影响行数:"+count);
}
@Test
public void updateOne() throws Exception {
User user = new User();
user.setId(4);
user.setUsername("ccc");
user.setTelphone("6667");
Integer count = userDao.updateUserById(user);
System.out.println("影响行数:"+count);
}
@Test
public void deleteOne() throws Exception {
User user = new User();
user.setId(4);
Integer count = userDao.deleteUserById(user);
System.out.println("影响行数:"+count);
}
}
使用Mapper(动态)代理模式开发
通过上述我们的自定义框架,我们解决了JDBC操作数据库带来的一些问题:例如频繁创建释放数据库连接,硬编码,手动封装返回结果集等问题,但是现在我们继续来分析刚刚完成的自定义框架代码,有没有什么问题?
问题如下 :
- dao的实现类中存在重复的代码,整个操作的过程模板重复(创建sqlsession,调用sqlsession方法,关闭 sqlsession)
- dao的实现类中存在硬编码,调用sqlsession的方法时,参数statement的id硬编码
解决:使用代理模式来创建接口的代理对象
在SqlSession的实现类(DefaultSqlSession)中实现获取代理对象的方法
@Override
public <T> T getMapper(Class<?> mapperClass) {
Object mapperObj = Proxy.newProxyInstance(mapperClass.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 底层都还是去执行JDBC代码 //根据不同情况,来调用selctList或者selectOne
// 准备参数 1:statmentid :sql语句的唯一标识:namespace.id= 接口全限定名.方法名
// 方法名:findAll
String clazzName = method.getDeclaringClass().getName();
String methodName = method.getName();
String statementId=clazzName+"."+methodName;
// 准备参数2:params:args
// 获取被调用方法的返回值类型
Type genericReturnType = method.getGenericReturnType();
//判断是否进行了 泛型类型参数化
if (genericReturnType instanceof ParameterizedType){
List<Object> selectList = selectList(statementId, args);
return selectList;
}
//处理添加、修改、删除的情况
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
String sql = mappedStatement.getSql();
if (sql.startsWith("update")||sql.startsWith("insert")
||sql.startsWith("delete")||
sql.startsWith("UPDATE")||sql.startsWith("INSERT")
||sql.startsWith("DELETE")){
Integer count = excuteUpdate(statementId, args);
return count;
}
return selectOne(statementId,args);
}
});
return (T) mapperObj;
}
测试代理模式下的CRUD
package com.kmning.mybatis.test;
?
import com.kmning.mybatis.io.Resources;
import com.kmning.mybatis.sqlSession.SqlSession;
import com.kmning.mybatis.sqlSession.SqlSessionFactory;
import com.kmning.mybatis.sqlSession.SqlSessionFactoryBuilder;
import com.kmning.mybatis.test.dao.UserDao;
import com.kmning.mybatis.test.pojo.User;
import org.dom4j.DocumentException;
import org.junit.Before;
import org.junit.Test;
import java.io.InputStream;
import java.util.List;
/**
* @author kangming.ning
* @date 2021/5/2 23:15
*/
public class UserMapperTest {
private UserDao userDao;
@Before
public void before() throws DocumentException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = SqlSessionFactoryBuilder.build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
userDao= sqlSession.getMapper(UserDao.class);
}
@Test
public void findAll() throws Exception {
List<User> list = userDao.findAll();
for (User user : list) {
System.out.println(user);
}
}
@Test
public void findByCondition() throws Exception {
User user = new User();
user.setUsername("aa");
user.setId(1);
User byCondition = userDao.findByCondition(user);
System.out.println(byCondition);
}
@Test
public void insertOne() throws Exception {
User user = new User();
user.setId(4);
user.setUsername("cc");
user.setTelphone("666");
Integer count = userDao.insertUser(user);
System.out.println("影响行数:"+count);
}
@Test
public void updateOne() throws Exception {
User user = new User();
user.setId(6);
user.setUsername("ccc");
user.setTelphone("6667");
Integer count = userDao.updateUserById(user);
System.out.println("影响行数:"+count);
}
@Test
public void deleteOne() throws Exception {
User user = new User();
user.setId(4);
Integer count = userDao.deleteUserById(user);
System.out.println("影响行数:"+count);
}
}
至此,已经基本完整介绍了自定义框架的思路并对其进行实现。当然,忽略不了不少非核心代码,比如解析配置文件等代码,相对简单,避免文章过于拖沓冗长(可能已经冗长了)就没有全部贴出来。
?
本文暂时没有评论,来添加一个吧(●'◡'●)