mybatis查询返回null的原因_可怕!你没看错,这次确实是纯手工实现一个MyBatis框架...
目录
- 前言
- JDBC
- MyBatis
- 源码分析
- 前置知识
- 原理分析
- 自己实现一个 MyBatis 框架
前言
MyBatis是一个非常优秀的持久层应用框架,目前几乎已经一统天下。既然是持久层框架,那么一定是对于数据库的操作,Java 中谈到数据库操作,一定少不了 JDBC。那么 ,MyBatis 比传统的 JDBC 好在哪那?MyBatis 又在哪方面做了优化那?
JDBC
如果我们需要查询所有用户,传统的 JDBC 会这样写。
public static void main(String[] args) {//声明Connection对象Connection con = null;try {//加载驱动程序Class.forName("com.mysql.jdbc.Driver");//创建 connection 对象con = DriverManager.getConnection("jdbc:mysql://localhost:3306/db","username","password");//使用 connection 对象创建statement 或者 PreparedStatement 类对象,用来执行SQL语句Statement statement = con.createStatement();//要执行的SQL语句String sql = "select * from user";//3.ResultSet类,用来存放获取的结果集!!ResultSet rs = statement.executeQuery(sql);String job = "";String id = "";while(rs.next()){//获取job这列数据job = rs.getString("job");//获取userId这列数据id = rs.getString("userId");//输出结果System.out.println(id + "t" + job);}} catch(ClassNotFoundException e) {e.printStackTrace();} catch(SQLException e) {//数据库连接失败异常处理e.printStackTrace();}catch (Exception e) {e.printStackTrace();}finally{rs.close();con.close();}
}
通过上面的代码,我们可以将 JDBC 对于数据库的操作总结为以下几个步骤:
- 加载驱动
- 创建连接,Connection 对象
- 根据 Connection 创建 Statement 或者 PreparedStatement 来执行 SQL 语句
- 返回结果集到 ResultSet 中
- 手动将 ResultSet 映射到 JavaBean 中
传统的 JDBC 操作的问题也一目了然,整体非常繁琐,也不够灵活,执行一个 SQL 查询就要写一堆代码。
# MyBatis
来看看 MyBatis 代码如何查询数据库。几行代码就完成了数据库查询操作,并且将数据库查询出来的结果映射到了 JavaBean 中了。我们的代码没有加入 Spring Mybatis,加入 Spring 后整体流程会复杂很多,不方便我们理解。
//获取 sqlSession,sqlSession 相当于传统 JDBC 的 Conection
public static SqlSession getSqlSession(){InputStream configFile = new FileInputStream(filePath);SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder.build(configFile);return sqlSessionFactory.openSession();
}//使用 sqlSession 获得对应的 mapper,mapper 用来执行 sql 语句。
public static User get(SqlSession sqlSession, int id){UserMapper userMapper = sqlSession.getMapper(UserMapper.class);return userMapper.selectByPrimaryKey(id);
}
我们来对 MyBatis 操作数据库做一个总结:
- 使用配置文件构建 SqlSessionFactory
- 使用 SqlSessionFactory 获得 SqlSession,SqlSession 相当于传统 JDBC 的 Conection
- 使用 SqlSession 得到 Mapper
- 用 Mapper 来执行 SQL 语句,并返回结果直接封装到 JavaBean 中
源码分析
大家平时应该经常使用 MyBatis 框架,对于 SqlSessionFactory、SqlSession、Mapper 等也有一些概念。下面我们从源码来分析怎么实现这些概念。
前置知识
先给出一个大部分框架的代码流程,方便大家理解框架。下面的图片就说明了接口、抽象类和实现类的关系,我们自己写代码时也要多学习这种思想。
带着结果看过程
看源码对于很多人来说都是一个比较枯燥和乏味的过程,如果不做抽象和总结,会觉得非常乱。另外,看源码不要去扣某个细节,尽量从宏观上理解它。这样带着结果看过程你就会知道设计者为什么这么做。
先给出整个 MyBatis 框架的架构图,大家先有一个印象:
原理分析
说明,我们讲解的是原生的 MyBatis 框架,并不是与 Spring 结合的 MyBatis 框架。
还是把上面 MyBatis 操作数据库的代码拿过来,方便我们与源码对照。
//获取 sqlSession,sqlSession 相当于传统 JDBC 的 Conectionpublic static SqlSession getSqlSession(){//步骤一InputStream configFile = new FileInputStream(filePath);//步骤二SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder.build(configFile);return sqlSessionFactory.openSession();}//使用 sqlSession 获得对应的 mapper,mapper 用来执行 sql 语句。public static User get(SqlSession sqlSession, int id){//步骤三UserMapper userMapper = sqlSession.getMapper(UserMapper.class);return userMapper.selectByPrimaryKey(id);}
MyBatis 框架的第一步就是加载我们数据库的相关信息,比如用户名、密码等。以及我们在 XML 文件中写的 SQL 语句。
//配置文件中指定了数据库相关的信息和写 sql 语句的 mapper 相关信息,稍后我们需要读取并加载到我们的配置类中。<configuration><environments default="development"><environment id="development"><transactionManager type="JDBC"><dataSource type="POOLED"><property name="driver" value="com.mysql.jdbc.Driver"/><property name="url" value="jdbc:mysql://localhost:3306/db"/><property name="username" value="root"/><property name="password" value="123456"/></dataSource></transactionManager></environment></environments></configuration><mappers><mapper resource="xml/UserMapper.xml"/></mappers>
第二步就是通过读取到的配置文件信息,构建一个 SqlSessionFactory。
通过 openSession 方法返回了一个 sqlSession,我们来看看 openSession 方法做了什么。
//我们来重点看看 openSession 做了什么操作, DefaultSqlSessionFactory.java@Overridepublic SqlSession openSession() {return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);}public Configuration getConfiguration() {return this.configuration;}//这个函数里面有着事务控制相关的代码。private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {Transaction tx = null;DefaultSqlSession var8;try {Environment environment = this.configuration.getEnvironment();TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);//根据上面的参数得到 TransactionFactory,通过 TransactionFactory 生成一个 Transaction,可以理解为这个 SqlSession 的事务控制器tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);// 将这个事务控制器封装在 Executor 里Executor executor = this.configuration.newExecutor(tx, execType);// 使用 configuration 配置类,Executor,和 configuration(是否自动提交) 来构建一个 DefaultSqlSession。var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);} catch (Exception var12) {this.closeTransaction(tx);throw ExceptionFactory.wrapException("Error opening session. Cause: " + var12, var12);} finally {ErrorContext.instance().reset();}return var8;}
看了上面的一大段代码你可能会觉得蒙,没关系,我们来划重点,最终结果返回了一个
DefaultSqlsession。
// 使用 configuration 配置类(我们上面读取的配置文件就需要加载到这个类中),Executor(包含了数据事务控制相关信息),和 autoCommit(是否自动提交) 来构建一个 DefaultSqlSession。var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
有了这个 sqlSession 之后,我们就可以实现所有对数据库的操作了,因为我们已经把所有的信息加载到这里面了。数据库信息、SQL 信息、SQL 语句执行器等。
当然我们一般使用这个 sqlSession 获得对应的 mapper 接口类,然后用这个接口类查询数据库。
既然所有东西都封装在 sqlSession 中,先来看看 sqlSession 的组成部分。
SqlSession 的接口定义:里面定义了增删改查和提交回滚等方法。
public interface SqlSession extends Closeable {<T> T selectOne(String var1);<T> T selectOne(String var1, Object var2);<E> List<E> selectList(String var1);<E> List<E> selectList(String var1, Object var2);<E> List<E> selectList(String var1, Object var2, RowBounds var3);<K, V> Map<K, V> selectMap(String var1, String var2);<K, V> Map<K, V> selectMap(String var1, Object var2, String var3);<K, V> Map<K, V> selectMap(String var1, Object var2, String var3, RowBounds var4);<T> Cursor<T> selectCursor(String var1);<T> Cursor<T> selectCursor(String var1, Object var2);<T> Cursor<T> selectCursor(String var1, Object var2, RowBounds var3);void select(String var1, Object var2, ResultHandler var3);void select(String var1, ResultHandler var2);void select(String var1, Object var2, RowBounds var3, ResultHandler var4);int insert(String var1);int insert(String var1, Object var2);int update(String var1);int update(String var1, Object var2);int delete(String var1);int delete(String var1, Object var2);void commit();void commit(boolean var1);void rollback();void rollback(boolean var1);List<BatchResult> flushStatements();void close();void clearCache();Configuration getConfiguration();<T> T getMapper(Class<T> var1);Connection getConnection();}
接下来用 sqlSession 获取对应的 Mapper。
DefaultSqlSession 的 getMapper 实现:
public <T> T getMapper(Class<T> type) {return this.configuration.getMapper(type, this);}//从 configuration 里面 getMapper,Mapper 就在 Configuration 里public <T> T getMapper(Class<T> type, SqlSession sqlSession) {return this.mapperRegistry.getMapper(type, sqlSession);}
MapperRegistry 里 getMapper 的最终实现,同时我们需要思考一个问题,我们的 sqlSession 接口里面只定义了抽象的增删改查,而这个接口并没有任何实现类,那么这个 XML 到底是如何与接口关联起来并生成实现类那?通过 MapperRegistry 可以得出答案,那就是动态代理。
public class MapperRegistry {private final Configuration config;// 用一个 Map 来存储接口和 xml 文件之间的映射关系,key 应该是接口,但是 value 是 MapperProxyFactoryprivate final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap();public MapperRegistry(Configuration config) {this.config = config;}public <T> T getMapper(Class<T> type, SqlSession sqlSession) {//获取到这个接口对应的 MapperProxyFactory。MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);if (mapperProxyFactory == null) {throw new BindingException("Type " + type + " is not known to the MapperRegistry.");} else {try {//用上一步获取的 MapperProxyFactory 和 sqlSession 构建对应的 Classreturn mapperProxyFactory.newInstance(sqlSession);} catch (Exception var5) {throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);}}}}
最终的结果是生成一个 mapper 接口的动态代理
最终的结果是生成一个 mapper 接口的动态代理类,通过这个类,我们实现对数据库的增删改查。
接下来我们看看 newInstance 的具体实现:
public T newInstance(SqlSession sqlSession) {// mapperInterface 就是接口MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);return this.newInstance(mapperProxy);}protected T newInstance(MapperProxy<T> mapperProxy) {//动态代理,这里的动态代理有一些不一样return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);}
为什么说这里的动态代理有一些不一样那?我们先看看正常流程的动态代理,接口,和接口实现类是必须的。而我们的 Mapper 接口只有充满了 SQL 语句的 XML 文件,没有具体实现类。
与传统的动态代理相比,MyBatis 的 Mapper 接口是没有实现类的,那么它又是怎么实现动态代理的那?
我们来看一下 MapperProxy 的源码:
public class MapperProxy<T> implements InvocationHandler, Serializable {private static final long serialVersionUID = -6424540398559729838L;private final SqlSession sqlSession;private final Class<T> mapperInterface;private final Map<Method, MapperMethod> methodCache;public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {this.sqlSession = sqlSession;this.mapperInterface = mapperInterface;this.methodCache = methodCache;}// 正常的动态代理中 Object proxy 这个参数应该是接口的实现类// com.paul.pkg.UserMapper@5a123uf// 现在里面是 org.apache.ibatis.binding.MapperProxy@6y213kn, 这俩面public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, args);}if (this.isDefaultMethod(method)) {return this.invokeDefaultMethod(proxy, method, args);}} catch (Throwable var5) {throw ExceptionUtil.unwrapThrowable(var5);}// Mapper 走这个流程,先尝试在缓存里获取 methodMapperMethod mapperMethod = this.cachedMapperMethod(method);return mapperMethod.execute(this.sqlSession, args);}private MapperMethod cachedMapperMethod(Method method) {MapperMethod mapperMethod = (MapperMethod)this.methodCache.get(method);if (mapperMethod == null) {// mapperMethod 的构建,通过接口名,方法,和 xml 配置(通过 sqlSession 的 Configuration 获得)mapperMethod = new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration());//通过 execute 执行方法,因为 sqlSession 封装了 Executor,所以还要传进来,execute 方法使用//sqlSession 里面的方法。this.methodCache.put(method, mapperMethod);}return mapperMethod;}}
来看 MapperMethod 的定义:
// command 里面包含了方法名,比如 com.paul.pkg.selectByPrimaryKey// type, 表示是 SELECT,UPDATE,INSERT,或者 DELETE// method 是方法的签名public class MapperMethod {private final MapperMethod.SqlCommand command;private final MapperMethod.MethodSignature method;public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {this.command = new MapperMethod.SqlCommand(config, mapperInterface, method);this.method = new MapperMethod.MethodSignature(config, mapperInterface, method);}}
public Object execute(SqlSession sqlSession, Object[] args) {Object result;//返回结果//INSERT操作if (SqlCommandType.INSERT == command.getType()) {//处理参数Object param = method.convertArgsToSqlCommandParam(args);//调用sqlSession的insert方法 result = rowCountResult(sqlSession.insert(command.getName(), param));}..........}
通过 sqlSession 来执行我们的 SQL 语句,返回结果,动态代理的方法调用结束。
进入 DefaultSqlSession 执行对应的 SQL 语句。
public <T> T selectOne(String statement, Object parameter) {List<T> list = this.selectList(statement, parameter);if (list.size() == 1) {return list.get(0);} else if (list.size() > 1) {throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());} else {return null;}}public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {List var5;try {// 这里又需要 configuration 来获取对应的 statement// MappedStatement 里面有 xml 文件,和要执行的方法,就是 xml 里面的 id,statementType,以及 sql 语句。MappedStatement ms = this.configuration.getMappedStatement(statement);// 用 executor 执行 query,executor 里面应该是包装了 JDBC。var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);} catch (Exception var9) {throw ExceptionFactory.wrapException("Error querying database. Cause: " + var9, var9);} finally {ErrorContext.instance().reset();}return var5;}
Executor 的实现类里面执行 query 方法。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameterObject);CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {Cache cache = ms.getCache();if (cache != null) {this.flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) {this.ensureNoOutParams(ms, boundSql);List<E> list = (List)this.tcm.getObject(cache, key);if (list == null) {list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);this.tcm.putObject(cache, key, list);}return list;}}// 使用 delegate 去 query,delegate 是 SimpleExecutor。里面使用 JDBC 进行数据库操作。return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
# 自己实现一个 MyBatis 框架
整体流程
image
- 首先创建 SqlSessionFactory 实例,SqlSessionFactory 就是创建 SqlSession 的工厂类。
- 加载配置文件创建 Configuration 对象,配置文件包括数据库相关配置文件以及我们在 XML 文件中写的 SQL。
- 通过 SqlSessionFactory 创建 SqlSession。
- 通过 SqlSession 获取 mapper 接口动态代理。
- 动态代理回调 SqlSession 中某查询方法。
- SqlSession 将查询方法转发给 Executor。
- Executor 基于 JDBC 访问数据库获取数据,最后还是通过 JDBC 操作数据库。
- Executor 通过反射将数据转换成 POJO 并返回给 SqlSession。
- 将数据返回给调用者。
项目整体使用 Maven 构建,mybatis-demo 是脱离 Spring 的 MyBatis 使用的例子,大家可以先熟悉以下 Mybatis 框架如何使用,代码就不在讲解了。paul-mybatis 是我们自己实现的 MyBatis 框架。关注公众号Java面试那些事儿,回复关键字面试,获取最新的面试资料。
首先按照我们以前的使用 MyBatis 代码时的流程,创建 Mapper 接口、XML 文件,和 POJO 以及集一些配置文件,这几个文件我们和 mybatis-demo 创建一样的即可,方便我们比较结果。
Mapper 接口,这里面定义两个抽象方法,根据主键查找用户和查找所有用户:
package com.paul.mybatis.mapper;import com.paul.mybatis.entity.User;import java.util.List;public interface UserMapper {User selectByPrimaryKey(long userId);List<User> selectAll();}
XML 文件,里面是上面两个抽象方法的具体 SQL 实现,完全消防官方 XML 文件的写法,需要注意 namespace、id、resultType、SQL 语句这几个点,都是我们后面代码需要处理的。
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.paul.mybatis.mapper.UserMapper"><select id="selectByPrimaryKey" resultType="User">select *from t_userwhere userId = #{userId}</select><select id="selectAll" resultType="User">select *from t_user</select></mapper>
最后,是我们的实体类,它的属性与数据库的表相对应:
package com.paul.mybatis.entity;public class User {private long userId;private String userName;private int sex;private String role;public long getUserId() {return userId;}public void setUserId(long userId) {this.userId = userId;}public String getUserName() {return userName;}public void setUserName(String userName) {this.userName = userName;}public int getSex() {return sex;}public void setSex(int sex) {this.sex = sex;}public String getRole() {return role;}public void setRole(String role) {this.role = role;}}
最后一个配置文件,数据库连接配置文件 db.propreties:
jdbc.driver=com.mysql.jdbc.Driverjdbc.url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf8jdbc.username=rootjdbc.password=root
配置文件和一些测试的必须类已经写完了,首先我们需要把这些配置信息加载到 Configuration 配置类中。关注公众号Java面试那些事儿,回复关键字面试,获取最新的面试资料。
先定义一个类来加载写 SQL 语句的 XML 文件,上面我们说过要注意四个点,namespace、id、resultType、SQL 语句,我们写对应的属性来保存它,代码很简单,就不多讲了。
package com.paul.mybatis.confiuration;/**** XML 中的 sql 配置信息加载到这个类中**/public class MappedStatement {private String namespace;private String id;private String resultType;private String sql;public String getNamespace() {return namespace;}public void setNamespace(String namespace) {this.namespace = namespace;}public String getId() {return id;}public void setId(String id) {this.id = id;}public String getResultType() {return resultType;}public void setResultType(String resultType) {this.resultType = resultType;}public String getSql() {return sql;}public void setSql(String sql) {this.sql = sql;}}
接下来我们定义一个 Configuration 总配置类,来保存 db.propeties 里面的属性和 XML 文件的 SQL 信息,Configuration 类里面的文件对应我们配置文件中的属性。
package com.paul.mybatis.confiuration;import java.util.HashMap;import java.util.List;import java.util.Map;/**** 所有的配置信息**/public class Configuration {private String jdbcDriver;private String jdbcUrl;private String jdbcPassword;private String jdbcUsername;private Map<String,MappedStatement> mappedStatement = new HashMap<>();public Map<String, MappedStatement> getMappedStatement() {return mappedStatement;}public void setMappedStatement(Map<String, MappedStatement> mappedStatement) {this.mappedStatement = mappedStatement;}public String getJdbcDriver() {return jdbcDriver;}public void setJdbcDriver(String jdbcDriver) {this.jdbcDriver = jdbcDriver;}public String getJdbcUrl() {return jdbcUrl;}public void setJdbcUrl(String jdbcUrl) {this.jdbcUrl = jdbcUrl;}public String getJdbcPassword() {return jdbcPassword;}public void setJdbcPassword(String jdbcPassword) {this.jdbcPassword = jdbcPassword;}public String getJdbcUsername() {return jdbcUsername;}public void setJdbcUsername(String jdbcUsername) {this.jdbcUsername = jdbcUsername;}}
按照上面的流程图,我们来创建一个 SqlSessionFactory 工厂类,这个类有两个功能,一个是加载配置文件信息到 Configuration 类中,另一个是创建 SqlSession。
SqlSessionFactory 抽象模版:
package com.paul.mybatis.factory;import com.paul.mybatis.sqlsession.SqlSession;public interface SqlSessionFactory {SqlSession openSession();}
创建 SqlSessionFactory 的 Default 实现类,Default 实现类主要完成了两个功能,加载配置信息到 Configuration 对象里,实现创建 SqlSession 的功能。
package com.paul.mybatis.factory;import com.paul.mybatis.confiuration.Configuration;import com.paul.mybatis.confiuration.MappedStatement;import com.paul.mybatis.sqlsession.DefaultSqlSession;import com.paul.mybatis.sqlsession.SqlSession;import org.dom4j.Document;import org.dom4j.DocumentException;import org.dom4j.Element;import org.dom4j.io.SAXReader;import java.io.File;import java.io.IOException;import java.io.InputStream;import java.net.URL;import java.util.ArrayList;import java.util.List;import java.util.Properties;/**** 1.初始化时就完成了 configuration 的实例化* 2.工厂类,生成 sqlSession**/public class DefaultSqlSessionFactory implements SqlSessionFactory{private final Configuration configuration = new Configuration();// xml 文件存放的位置private static final String MAPPER_CONFIG_LOCATION = "mappers";// 数据库信息存放的位置private static final String DB_CONFIG_FILE = "db.properties";public DefaultSqlSessionFactory() {loadDBInfo();loadMapperInfo();}private void loadDBInfo() {InputStream db = this.getClass().getClassLoader().getResourceAsStream(DB_CONFIG_FILE);Properties p = new Properties();try {p.load(db);} catch (IOException e) {e.printStackTrace();}//将配置信息写入Configuration 对象configuration.setJdbcDriver(p.get("jdbc.driver").toString());configuration.setJdbcUrl(p.get("jdbc.url").toString());configuration.setJdbcUsername(p.get("jdbc.username").toString());configuration.setJdbcPassword(p.get("jdbc.password").toString());}//解析并加载xml文件private void loadMapperInfo(){URL resources = null;resources = this.getClass().getClassLoader().getResource(MAPPER_CONFIG_LOCATION);File mappers = new File(resources.getFile());//读取文件夹下面的文件信息if(mappers.isDirectory()){File[] files = mappers.listFiles();for(File file:files){loadMapperInfo(file);}}}private void loadMapperInfo(File file){SAXReader reader = new SAXReader();//通过read方法读取一个文件转换成Document 对象Document document = null;try {document = reader.read(file);} catch (DocumentException e) {e.printStackTrace();}//获取根结点元素对象<mapper>Element e = document.getRootElement();//获取命名空间namespaceString namespace = e.attribute("namespace").getData().toString();//获取select,insert,update,delete子节点列表List<Element> selects = e.elements("select");List<Element> inserts = e.elements("insert");List<Element> updates = e.elements("update");List<Element> deletes = e.elements("delete");List<Element> all = new ArrayList<>();all.addAll(selects);all.addAll(inserts);all.addAll(updates);all.addAll(deletes);//遍历节点,组装成 MappedStatement 然后放入到configuration 对象中for(Element ele:all){MappedStatement mappedStatement = new MappedStatement();String id = ele.attribute("id").getData().toString();String resultType = ele.attribute("resultType").getData().toString();String sql = ele.getData().toString();mappedStatement.setId(namespace+"."+id);mappedStatement.setResultType(resultType);mappedStatement.setNamespace(namespace);mappedStatement.setSql(sql);// xml 文件中的每个 sql 方法都组装成 mappedStatement 对象,以 namespace+"."+id 为 key, 放入// configuration 配置类中。configuration.getMappedStatement().put(namespace+"."+id,mappedStatement);}}@Overridepublic SqlSession openSession() {// openSession 方法创建一个 DefaultSqlSession,configuration 配置类作为 构造函数参数传入return new DefaultSqlSession(configuration);}}
在 SqlSessionFactory 里创建了 DefaultSqlSession,我们看看它的具体实现。SqlSession 里面应该封装了所有数据库的具体操作和一些获取 mapper 实现类的方法。
SqlSession 接口,定义模版方法package com.paul.mybatis.sqlsession;import java.util.List;/**** 封装了所有数据库的操作* 所有功能都是基于 Excutor 来实现的,Executor 封装了 JDBC 操作***/public interface SqlSession {/*** 根据传入的条件查询单一结果* @param statement namespace+id,可以用做 key,去 configuration 里面获取 sql 语句,resultType* @param parameter 要传入 sql 语句中的查询参数* @param <T> 返回指定的结果对象* @return*/<T> T selectOne(String statement, Object parameter);<T> List<T> selectList(String statement, Object parameter);<T> T getMapper(Class<T> type);}
Default 的 SqlSession 实现类。里面需要传入 Executor,这个 Executor 里面封装了 JDBC 操作数据库的流程。我们重点关注 getMapper 方法,使用动态代理生成一个加强类。关注公众号Java面试那些事儿,回复关键字面试,获取最新的面试资料。这里面最终还是把数据库的相关操作转给 SqlSession,使用 Mapper 能使编程更加优雅。
package com.paul.mybatis.sqlsession;import com.paul.mybatis.bind.MapperProxy;import com.paul.mybatis.confiuration.Configuration;import com.paul.mybatis.confiuration.MappedStatement;import com.paul.mybatis.executor.Executor;import com.paul.mybatis.executor.SimpleExecutor;import java.lang.reflect.Proxy;import java.util.List;public class DefaultSqlSession implements SqlSession {private final Configuration configuration;private Executor executor;public DefaultSqlSession(Configuration configuration) {super();this.configuration = configuration;executor = new SimpleExecutor(configuration);}@Overridepublic <T> T selectOne(String statement, Object parameter) {List<T> selectList = this.selectList(statement,parameter);if(selectList == null || selectList.size() == 0){return null;}if(selectList.size() == 1){return (T) selectList.get(0);}else{throw new RuntimeException("too many result");}}@Overridepublic <T> List<T> selectList(String statement, Object parameter) {MappedStatement ms = configuration.getMappedStatement().get(statement);// 我们的查询方法最终还是交给了 Executor 去执行,Executor 里面封装了 JDBC 操作。传入参数包含了 sql 语句和 sql 语句需要的参数。return executor.query(ms,parameter);}@Overridepublic <T> T getMapper(Class<T> type) {//通过动态代理生成了一个实现类,我们重点关注,动态代理的实现,它是一个 InvocationHandler,传入参数是 this,就是 sqlSession 的一个实例。MapperProxy mp = new MapperProxy(this);//给我一个接口,还你一个实现类return (T)Proxy.newProxyInstance(type.getClassLoader(),new Class[]{type},mp);}}
来看看我们的 InvocationHandler 如何实现 invoke 方法:
package com.paul.mybatis.bind;import com.paul.mybatis.sqlsession.SqlSession;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.util.Collection;import java.util.Collections;/**** 将请求转发给 sqlSession**/public class MapperProxy implements InvocationHandler {private SqlSession sqlSession;public MapperProxy(SqlSession sqlSession) {this.sqlSession = sqlSession;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println(method.getDeclaringClass().getName()+"."+method.getName());//最终还是将执行方法转给 sqlSession,因为 sqlSession 里面封装了 Executor//根据调用方法的类名和方法名以及参数,传给 sqlSession 对应的方法if(Collection.class.isAssignableFrom(method.getReturnType())){return sqlSession.selectList(method.getDeclaringClass().getName()+"."+method.getName(),args==null?null:args[0]);}else{return sqlSession.selectOne(method.getDeclaringClass().getName()+"."+method.getName(),args==null?null:args[0]);}}}
获取 Mapper 接口的实现类我们已经实现了,通过动态代理调用 sqlSession 的方法。那么就剩最后一个重要的工作了,那就是实现 Exectuor 类去操作数据库,封装 JDBC。
Executor 抽象模版,我们只实现了 query、update 等操作慢慢增加。
package com.paul.mybatis.executor;import com.paul.mybatis.confiuration.MappedStatement;import java.util.List;/**** mybatis 核心接口之一,定义了数据库操作的最基本的方法,JDBC,sqlSession的所有功能都是基于它来实现的**/public interface Executor {/**** 查询接口* @param ms 封装sql 语句的 mappedStatemnet 对象,里面包含了 sql 语句,resultType 等。* @param parameter 传入sql 参数* @param <E> 将数据对象转换成指定对象结果集返回* @return*/<E> List<E> query(MappedStatement ms, Object parameter);}
Executor 接口的实现类,主要是对 JDBC 的封装,和利用反射方法将结果映射到 resultType 对应的实体类中
java
package com.paul.mybatis.executor;import com.paul.mybatis.confiuration.Configuration;
import com.paul.mybatis.confiuration.MappedStatement;
import com.paul.mybatis.util.ReflectionUtil;import java.sql.*;
import java.util.ArrayList;
import java.util.List;public class SimpleExecutor implements Executor {private final Configuration configuration;public SimpleExecutor(Configuration configuration) {this.configuration = configuration;}@Overridepublic <E> List<E> query(MappedStatement ms, Object parameter) {System.out.println(ms.getSql().toString());List<E> ret = new ArrayList<>(); //返回结果集try {Class.forName(configuration.getJdbcDriver());} catch (ClassNotFoundException e) {e.printStackTrace();}Connection connection = null;PreparedStatement preparedStatement = null;ResultSet resultSet = null;try {connection = DriverManager.getConnection(configuration.getJdbcUrl(), configuration.getJdbcUsername(), configuration.getJdbcPassword());String regex = "#{([^}])*}";// 将 sql 语句中的 #{userId} 替换为 ?String sql = ms.getSql().replaceAll(regex,"");preparedStatement = connection.prepareStatement(sql);//处理占位符,把占位符用传入的参数替换parametersize(preparedStatement, parameter);resultSet = preparedStatement.executeQuery();handlerResultSet(resultSet, ret,ms.getResultType());}catch (SQLException e){e.printStackTrace();}finally {try {resultSet.close();preparedStatement.close();connection.close();}catch (Exception e){e.printStackTrace();}}return ret;}private void parametersize(PreparedStatement preparedStatement,Object parameter) throws SQLException{if(parameter instanceof Integer){preparedStatement.setInt(1,(int)parameter);}else if(parameter instanceof Long){preparedStatement.setLong(1,(Long)parameter);}else if(parameter instanceof String){preparedStatement.setString(1,(String)parameter);}}private <E> void handlerResultSet(ResultSet resultSet, List<E> ret,String className){Class<E> clazz = null;//通过反射获取类对象try {clazz = (Class<E>)Class.forName(className);} catch (ClassNotFoundException e) {e.printStackTrace();}try {while (resultSet.next()) {Object entity = clazz.newInstance();//通过反射工具 将 resultset 中的数据填充到 entity 中ReflectionUtil.setPropToBeanFromResultSet(entity, resultSet);ret.add((E) entity);}} catch (Exception e) {e.printStackTrace();}}
}
看一下测试的结果,整个 MyBatis 框架已经实现完成了,当然有很多地方需要完善,比如 XML 中的 SQL 语句处处理还缺很多功能,目前只支持 select 等,希望大家能通过源码解读和自己写的过程明白 MyBatis 的具体实现要点。
mybatis查询返回null的原因_可怕!你没看错,这次确实是纯手工实现一个MyBatis框架...相关推荐
- mybatis查询返回null解决方案
mybatis查询返回null解决方案 参考文章: (1)mybatis查询返回null解决方案 (2)https://www.cnblogs.com/zipon/p/6361661.html 备忘一 ...
- mysql查询有数据但返回null_Mybatis查不到数据查询返回Null问题
mybatis突然查不到数据,查询返回的都是Null,但是 select count(*) from xxx查询数量,返回却是正常的. Preparing: SELECT id,a9004,a9005 ...
- Mybatis 查询 返回值中只有id有值,其他都是null;
最近在重温mybatis, 但是在做练习的时候发现一个问题; 查询,简单的查询,返回之后发现结果中,只有id被映射了值,其他属性都是null; 很纳闷,为什么一个简单的测试会出现这种问题; 一开始以为 ...
- mybatis查询返回空,SQL数据库执行有数据!
我的数据库为Oracle,可以插入,更新,但在在Mybatis中执行SQL查询返回的数据发现返回NULL,但是生成SQL放在数据库中查询是有数据的,并且SQL是正确的! 这是没有改动的XML: < ...
- modelandview为null的原因_一千个不用 Null 的理由!
★★★建议星标我们★★★ 公众号改版后文章乱序推荐,希望你可以点击上方"Java进阶架构师",点击右上角,将我们设为★"星标"!这样才不会错过每日进阶架构文章呀 ...
- modelandview为null的原因_如何在Java代码中去掉烦人的“!=null”
云栖号资讯:[点击查看更多行业资讯] 在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来!问题 为了避免空指针调用,我们经常会看到这样的语句 if (someobject != null) ...
- mybatis查询返回map的问题
文章目录 背景 1.mybatis只返回单个map 2.查询返回map的list 3.利用mybatis的@MapKey注解返回map 4.重写handler 背景 假设背景: 想获取某个省下各个市有 ...
- 消防管件做的机器人图片_报废消防器材变身“机器人” 由消防官兵纯手工打造(图)...
原标题:报废消防器材变身"机器人" 由消防官兵纯手工打造(图) 由报废的消防零配件组成的机器人模型. 厦门网讯 (厦门日报记者林路然通讯员阙凤芳曾德猛)远看好似变形金刚,凑近还会说 ...
- MyBatis查询返回类型为int,查询结果为空NULL,报异常解决
例如: <select id="getPersonRecordId" parameterType="java.lang.String" resultTyp ...
最新文章
- Window Server 2008 R2 安装 Share Point 2013
- android 输入法 suretype,没输入法的年代,黑莓都有哪些经典键盘?
- 终于要揭开神秘面纱?Magic Leap将要展示产品
- python的web抓取_python实现从web抓取文档的方法
- JAVA【long值与ip地址互转】 - ip的判别
- Maven警告:“java使用了未经检查或不安全的操作。java: 有关详细信息, 请使用 -Xlint:unchecked 重新编译。“
- TensorFlow学习笔记(二十一) tensorflow机器学习模型
- mixin network_基于Mixin Network的Go语言比特币开发教程 : 用 Mixin Messenger 机器人接受和发送比特币...
- Vue+ElementUI项目使用webpack输出MPA【华为云分享】
- android+委托列表,在Android适配器中使用委托者模式
- css几个居中的方法
- linux mysql c语言 api_linux连接MySQL数据库(C语言 API 分析,动态插入修改数据)
- python抓取图片_Python3简单爬虫抓取网页图片
- 做朋友圈需先从做人开始
- JavaScript DOM 编程艺术(第2版)读书笔记 (7)
- linux yum安装驱动,centos8安装alsa驱动
- UE4-(蓝图)第四十七课过场动画之主序列新建镜头、镜头剪辑、部分功能简介
- metasploit的SET的Credential Harvester Attack Method
- windows如何截图
- Nexmo 短信平台接口 遇到的坑
热门文章
- JVM分代垃圾回收策略的基础概念
- c lambda表达式 select 改变字段名称_大神是如何学习 Go 语言之浅谈 select 的实现原理...
- mac os android连接wifi密码,Mac使用小技巧:找回WiFi密码
- java程序计时器_求完整简单java计时器小程序代码,急!!
- Python+pandas查找前5位成绩最高的同学与前5个最高成绩的同学
- matlab生成exe失败,求助,m文件生成exe遇到的错误
- 广工android嵌入式系统试卷_教育录播系统的选择
- .html()与.text()区别与辨析
- python判断对错题_可以在线答题,并且能判断对错,将错题保存起来
- 防仿百度图片背景色php,基于jQuery实现仿百度首页换肤背景图片切换代码_jquery...