mybatis 操作数据库的过程
// 第一步:阅读mybatiss-config.Inputstreamml配置文件 inputStream = Resources.getResourceAsStream("mybatis-config.xml");// 第二步:构建SqlSessionFactory(框架初始化)SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().bulid();// 第三步:打开sqlsessionSqlsesion 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是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调用accountmapp.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命令(分析mybatiss-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; }
这样一行代码出现在上面很多地方:method.convertArgsToSqlCommandParam (args),该方法的作用是将方法参数转换为 SqlCommandParam;paramnameresolver.getNamedParams () 实现。在看 paramNameResolver.getNamedParams () 以前,我们先来看看 paramNameResolver 是什么?
public Object convertArgsToSqlCommandParam(Object[] args) { return paramNameResolver.getNamedParams(args); }
在我们面前,我们在实例化 MethodSignature 对象 (new MethodSignature) 在其结构方法中,将实例化 ParamNameResolver 对象,对象主要用于处理接口形式的参数,最后将参数放置在一个位置 map(即属性 names)中。map 的 key 是参数的位置,value 名称为参数。
public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) { ... this.paramNameResolver = new ParamNameResolver(configuration, method);}
对 names 解释字段:
假设在 xxxMapper 有这样一种接口方法 selectByIdAndName ()
・selectByIdAndName (@Param ("id") String id, @Param ("name") String name) 转化为 map 为 {{0, "id"}, {1, "name"}}
・selectByIdAndName (String id, String name) 转化为 map 为 {{0, "0"}, {1, "1"}}
・selectByIdAndName (int a, RowBounds rb, int b) 转化为 map 为 {{0, "0"}, {2, "1"}}
构造方法将经历以下步骤
- 该方法的参数类型和方法的参数通过反射注释,method.getParameterAnnotations () 该方法返回注解二维数组,每种方法的参数包含一个注解数组。
- 遍历所有参数
- 首先判断这个参数的类型是否特殊,RowBounds 和 ResultHandler,是的,跳过,我们不处理
- 判断这个参数是否用于判断 Param 注释,如果使用的话 name 就是 Param 并将注解值 name 放到 map 键是方法中参数的位置,value 为 Param 的值
- 若未使用 Param 注释,判断是否打开 UseActualParamName,若打开,则使用 java8 得到方法名称的反射,这里容易引起异常,
具体原因参考上一篇博文的具体原因.
- 如果不满足上述条件,则该参数的名称为参数的下标
// 由于key有param1,param2,通用key前缀,param3等; public static final String GENERIC_NAME_PREFIX = "param"; // 存储参数的位置和相应的参数名称 private final SortedMap<Integer, String> names; // 是否使用@Param注释 private boolean hasParamAnnotation; public ParamNameResolver(Configuration config, Method method) { // 通过注释获得方法的参数类型数组 final Class<?>[] paramTypes = method.getParameterTypes(); // 通过反射获得的参数注解数组 final Annotation[][] paramAnnotations = method.getParameterAnnotations(); // SortedMap对象用于存储所有参数名称 final SortedMap<Integer, String> map = new TreeMap<>(); // 参数注解数组长度,即方法用于参数中的几个地方@Param // 如selectByIdandname(@Param("id") String id, @Param("name") String name)中,paramCount=2 int paramCount = paramAnnotations.length; // 遍历所有参数 for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) { // 判断该参数的类型是否为特殊类型,Rowbounds和ResultHandler,是的话跳过 if (isSpecialParameter(paramTypes[paramIndex])) { continue; } String name = null; for (Annotation annotation : paramAnnotations[paramIndex]) { // 判断该参数是否使用@Param注释 if (annotation instanceof Param) { // Param注释用于标记当前方法 hasParamAnnotation = true; // 如果使用,name是Param注解值 name = ((Param) annotation).value(); break; } } // 若经上述处理,参数名还是null,这意味着当前参数没有指定@Param注释 if (name == null) { // 判断UseactualParmnameme是否打开 if (config.isUseActualParamName()) { // 若打开,如果打开,使用java8的反射获得参数对应的属性名 name = getActualParamName(method, paramIndex); } // 如果name仍然是nulll if (name == null) { // use the parameter index as the name ("0", "1", ...) // 以map中的参数下标为参数的name,如 ("0", "1", ...) name = String.valueOf(map.size()); } } // 将参数放入map中,key是参数在方法中的位置,value是参数的name(@param的value值/参数对应的属性名/参数在map中的位置下标) map.put(paramIndex, name); } // 最后,使用Collections工具类的静态方法将结果map变成不可修改的类型 names = Collections.unmodifiableSortedMap(map); }
getNamedParams(): 该方法将对应参数名和参数值,并保存额外的一份 param 开始时,参数顺序数字的值
public Object getNamedParams(Object[] args) { // 这里的names是Paramnameresolver中的names,在构建Paramnameresolver对象时,Map创建了 // 获取方法参数的数量 final int paramCount = names.size(); // 没有参数 if (args == null || paramCount == 0) { return null; // 只有一个参数,而且没有使用@Param注释。 } else if (!hasParamAnnotation && paramCount == 1) { // 直接返回,不做任务处理 return args[names.firstKey()]; } else { // 包装成Parammap对象。这个对象继承了Hashmap,重写了get方法。 final Map<String, Object> param = new ParamMap<>(); int i = 0; // names中的所有键值对 for (Map.Entry<Integer, String> entry : names.entrySet()) { // 以参数名为key, 将相应的参数值作为value放入结果param对象中 param.put(entry.getValue(), args[entry.getKey()]); // 按顺序(param1)添加一般参数名称, param2, ...) final String genericParamName = GENERIC_NAME_PREFIX + (i + 1); // 确保不覆盖@Param 命名的参数 if (!names.containsValue(genericParamName)) { param.put(genericParamName, args[entry.getKey()]); } i++; } return param; } }}
getNamedParams () 总结:
- 只有一个参数时,直接返回,不做任务处理;
- 否则,存入 Map 键值对的形式如下:paramName=paramValue
・selectByIdAndName (@Param ("id") String id, @Param ("name") String name): 引入的参数是 ["1", “张三”],最后分析出来 map 为:{“id”:”1”,”“name”:” 张三”}
・selectByIdAndName (String id, @Param ("name") String name): 引入的参数是 ["1", “张三”],最后分析出来 map 为:{param1:”1”,”“name”:” 张三”}
假设执行的 SQL 语句是 select 类型,继续向下看代码
在 mapperMethod.execute (), convertargstosqlcomandparman () 方法处理方法参数后,假设此时我们调用查询单个记录,那么下一步将执行 sqlSession.selectOne () 方法。
sqlSession.selectOne () 源码分析:
sqlSession.selectOne () 也是调的 sqlSession.selectList () 方法,只是返回 list 第一条数据。当 list 当有多个数据时,抛出异常。
@Overridepublic <T> T selectOne(String statement, Object parameter) { // 调用当前selectlist方法 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; }}
sqlSession.selectList () 方法
@Override public <E> List<E> selectList(String statement, Object parameter) { return this.selectList(statement, parameter, RowBounds.DEFAULT); }
继续看:
@Override public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { // configurationMappedStatements根据key(id的全路径)获取MappedStatement对象 MappedStatement ms = configuration.getMappedStatement(statement); // 实现类Baseexecutor的query()方法调用Executor return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
在 sqlSession.selectList () 在方法中,我们可以看到调用 executor.query ()假设我们打开了二次缓存,那么 executor.query () 调用的是 executor 的实现类 CachingExecutor 中的 query (),二次缓存的逻辑是 CachingExecutor 在这一类中实现。