Mybatis源码解析之执行SQL语句
作者:互联网
mybatis 操作数据库的过程
// 第一步:读取mybatis-config.xml配置文件 InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml"); // 第二步:构建SqlSessionFactory(框架初始化) SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().bulid(); // 第三步:打开sqlSession SqlSession session = sqlSessionFactory.openSession(); // 第四步:获取Mapper接口对象(底层是动态代理) AccountMapper accountMapper = session.getMapper(AccountMapper.class); // 第五步:调用Mapper接口对象的方法操作数据库; Account account = accountMapper.selectByPrimaryKey(1);
通过调用 session.getMapper (AccountMapper.class) 所得到的 AccountMapper 是一个动态代理对象,所以执行
accountMapper.selectByPrimaryKey (1) 方法前,都会被 invoke () 拦截,先执行 invoke () 中的逻辑。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 要执行的方法所在的类如果是Object,直接调用,不做拦截处理 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); //如果是默认方法,也就是java8中的default方法 } else if (isDefaultMethod(method)) { // 直接执行default方法 return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } // 从缓存中获取MapperMethod final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); }
从 methodCache 获取对应 DAO 方法的 MapperMethod
MapperMethod 的主要功能是执行 SQL 语句的相关操作,在初始化的时候会实例化两个对象:SqlCommand(Sql 命令)和 MethodSignature(方法签名)。
/** * 根据Mapper接口类型、接口方法、核心配置对象 构造MapperMethod对象 * @param mapperInterface * @param method * @param config */ public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) { this.command = new SqlCommand(config, mapperInterface, method); // 将Mapper接口中的数据库操作方法(如Account selectById(Integer id);)封装成方法签名MethodSignature this.method = new MethodSignature(config, mapperInterface, method);
new SqlCommand()调用 SqlCommand 类构造方法:
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) { // 获取Mapper接口中要执行的某个方法的方法名 // 如accountMapper.selectByPrimaryKey(1) final String methodName = method.getName(); // 获取方法所在的类 final Class<?> declaringClass = method.getDeclaringClass(); // 解析得到Mapper语句对象(对配置文件中的<mapper></mapper>中的sql语句进行封装) MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration); if (ms == null) { if (method.getAnnotation(Flush.class) != null) { name = null; type = SqlCommandType.FLUSH; } else { throw new BindingException("Invalid bound statement (not found): " + mapperInterface.getName() + "." + methodName); } } else { // 如com.bjpowernode.mapper.AccountMapper.selectByPrimaryKey name = ms.getId(); // SQL类型:增 删 改 查 type = ms.getSqlCommandType(); if (type == SqlCommandType.UNKNOWN) { throw new BindingException("Unknown execution method for: " + name); } } } private MapperMethod cachedMapperMethod(Method method) { MapperMethod mapperMethod = (MapperMethod)this.methodCache.get(method); if (mapperMethod == null) { mapperMethod = new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration()); this.methodCache.put(method, mapperMethod); } return mapperMethod; }
调用 mapperMethod.execute (sqlSession, args)
在 mapperMethod.execute () 方法中,我们可以看到:mybatis 定义了 5 种 SQL 操作类型:
insert/update/delete/select/flush。其中,select 操作类型又可以分为五类,这五类的返回结果都不同,分别对应:
・返回参数为空:executeWithResultHandler ();
・查询多条记录:executeForMany (),返回对象为 JavaBean
・返参对象为 map:executeForMap (), 通过该方法查询数据库,最终的返回结果不是 JavaBean,而是 Map
・游标查询:executeForCursor ();关于什么是游标查询,自行百度哈;
・查询单条记录: sqlSession.selectOne (),通过该查询方法,最终只会返回一条结果;
通过源码追踪我们可以不难发现:当调用 mapperMethod.execute () 执行 SQL 语句的时候,无论是
insert/update/delete/flush,还是 select(包括 5 种不同的 select), 本质上时通过 sqlSession 调用的。在 SELECT 操作中,虽然调用了 MapperMethod 中的方法,但本质上仍是通过 Sqlsession 下的 select (), selectList (), selectCursor (), selectMap () 等方法实现的。
而 SqlSession 的内部实现,最终是调用执行器 Executor(后面会细说)。这里,我们可以先大概看一下 mybatis 在执行 SQL 语句的时候的调用过程:
以accountMapper.selectByPrimaryKey (1) 为例:
・调用 SqlSession.getMapper ():得到 xxxMapper (如 UserMapper) 的动态代理对象;
・调用
accountMapper.selectByPrimaryKey (1):在 xxxMapper 动态代理内部,会根据要执行的 SQL 语句类型 (insert/update/delete/select/flush) 来调用 SqlSession 对应的不同方法,如 sqlSession.insert ();
・在 sqlSession.insert () 方法的实现逻辑中,又会转交给 executor.query () 进行查询;
・executor.query () 又最终会转交给 statement 类进行操作,到这里就是 jdbc 操作了。
有人会好奇,为什么要通过不断的转交,SqlSession->Executor->Statement,而不是直接调用 Statement 执行 SQL 语句呢?因为在调用 Statement 之前,会处理一些共性的逻辑,如在 Executor 的实现类 BaseExecutor 会有一级缓存相关的逻辑,在 CachingExecutor 中会有二级缓存的相关逻辑。如果直接调用 Statement 执行 SQL 语句,那么在每个 Statement 的实现类中,都要写一套一级缓存和二级缓存的逻辑,就显得冗余了。这一块后面会细讲。
// SQL命令(在解析mybatis-config.xml配置文件的时候生成的) private final SqlCommand command; public Object execute(SqlSession sqlSession, Object[] args) { Object result; // 从command对象中获取要执行操作的SQL语句的类型,如INSERT/UPDATE/DELETE/SELECT switch (command.getType()) { // 插入 case INSERT: { // 把接口方法里的参数转换成sql能识别的参数 // 如:accountMapper.selectByPrimaryKey(1) // 把其中的参数"1"转化为sql能够识别的参数 Object param = method.convertArgsToSqlCommandParam(args); // sqlSession.insert(): 调用SqlSession执行插入操作 // rowCountResult(): 获取SQL语句的执行结果 result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } // 更新 case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); // sqlSession.insert(): 调用SqlSession执行更新操作 // rowCountResult(): 获取SQL语句的执行结果 result = rowCountResult(sqlSession.update(command.getName(), param)); break; } // 删除 case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); // sqlSession.insert(): 调用SqlSession执行更新操作 // rowCountResult(): 获取SQL语句的执行结果 result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } // 查询 case SELECT: // method.returnsVoid(): 返参是否为void // method.hasResultHandler(): 是否有对应的结果处理器 if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { // 查询多条记录 result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { // 查询结果返参为Map result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { // 以游标的方式进行查询 result = executeForCursor(sqlSession, args); } else { // 参数转换 转成sqlCommand参数 Object param = method.convertArgsToSqlCommandParam(args); // 执行查询 查询单条数据 result = sqlSession.selectOne(command.getName(), param); if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) { result = Optional.ofNullable(result); } } break; case FLUSH: // 执行清除操作 result = sqlSession.flushStatements(); break; default: throw new BindingException("Unknown execution method for: " + command.getName()); } if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { throw new BindingException("Mapper method '" + command.getName() + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); } return result;