-
2022-01-11 17:12:47
参考自:
MyBatis原理分析(通俗易懂)_Coder Wang-CSDN博客_mybatis原理
从源码一层一层进行分析,写得清晰易懂。我再这里就不赘述了,就写一下自己的大白话总结吧。
Mybatis的意义:
Mybatis存在的全部意义在于:将开发者从繁琐的JDBC API中解放出来,开发者不需要亲自去调用API去做:获取Connection,构造Statement,执行sql,然后将ResultSet放入结果对象中。这些Mybatis都做了,开发者只需要制订接口(也就说准备好元数据)即可,mybatis会根据这些必要的元数据转化为具体的JDBC操作,然后与数据库进行交互。
大概视图:
API方式:
public static void main(String[] args) { InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml"); SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession sqlSession = factory.openSession(); String name = "tom"; List<User> list = sqlSession.selectList("com.demo.mapper.UserMapper.getUserByName",params); }
接口方式:
public static void main(String[] args) { //前三步都相同 InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml"); SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession sqlSession = factory.openSession(); //这里不再调用SqlSession 的api,而是获得了接口对象,调用接口中的方法。 UserMapper mapper = sqlSession.getMapper(UserMapper.class); List<User> list = mapper.getUserByName("tom"); }
初始化工作:
大概流程:
SqlSessionFactoryBuilder --> SqlSessionFactory --> SqlSession --> SqlSession执行CRUD
细化流程:
1、读取mybatis-config.xml配置文件,解析其内容为Configuration;
2、在SqlSessionFactoryBuilder中根据Configuration去创建SqlSessionFactory;
3、SqlSessionFacotry.openSession()构造SqlSession,指定了configuration, executor, autoCommit 等;
4、SqlSession用指定的executor去执行具体的CRUD操作。
执行SQL:
1、入参statement,parameter,根据statement在Configuration的mappedStatements中找到对应的MappedStatement;
2、MappedStatement根据parameter找到BoundSql;
3、获取connection,从dataSource中获取的connection,同时指定transactionIsolation, autoCommit ;
4、Connection.createStatement(),构造Statement();
5、Statement.execute(boundsql.getSql());
6、从Statement获取ResultSet,封装到结果对象中。
可以看到,其实底层调用的都是JDBC的API。
名词解释:
MappedStatement: 对应于mybatis-config.xml中的<Mappers>下的<Mapper>元素
BoundSql: 含有?的sql语句,以及parameters,以及parameterMappings
更多相关内容 -
mybatis原理.xmind
2020-06-09 12:41:02该思维导图主要是对MyBatis原理知识进行了整理,通过对底层的分析,能够实现手写一个实现核心功能的简单MyBats,内容包括MyBatis整体架构和流程的分析、SQL的解析过程、手写解析流程、手写执行流程、看源码、MyBatis... -
深入理解mybatis原理
2018-03-29 23:28:59MyBatis 是目前非常流行的 ORM 框架,它的功能很强大,然而其实现却 比较简单、优雅。 -
【mybatis原理】
2022-03-26 15:38:15mybatismybatis原理mybatis框架分层架构核心接口和对象mapper接口与xml的映射mybatis执行过程...mybatis原理 mybatis框架分层架构 核心接口和对象 mapper接口与xml的映射 mybatis执行过程 mybatis执行时序图mybatis
mybatis原理
mybatis框架分层架构
核心接口和对象
mapper接口与xml的映射
mybatis执行过程
mybatis执行时序图
一级缓存和二级缓存
一级缓存
一级缓存执行时序图:
Mybatis对缓存提供支持,但是在没有配置的默认情况下,它只开启一级缓存,一级缓存只是相对于同一个SqlSession而言。所以在参数和SQL完全一样的情况下,我们使用同一个SqlSession对象调用一个Mapper方法,往往只执行一次SQL,因为使用SelSession第一次查询后,MyBatis会将其放在缓存中,以后再查询的时候,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession都会取出当前缓存的数据,而不会再次发送SQL到数据库。
为什么要使用一级缓存,不用多说也知道个大概。但是还有几个问题我们要注意一下。1、一级缓存的生命周期有多长?
a、MyBatis在开启一个数据库会话时,会 创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象。Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
b、如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用。
c、如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用。
d、SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用
2、怎么判断某两次查询是完全相同的查询?
mybatis认为,对于两次查询,如果以下条件都完全一样,那么就认为它们是完全相同的两次查询。
2.1 传入的statementId
2.2 查询时要求的结果集中的结果范围
2.3. 这次查询所产生的最终要传递给JDBC java.sql.Preparedstatement的Sql语句字符串(boundSql.getSql() )
2.4 传递给java.sql.Statement要设置的参数值
一级缓存总结:
MyBatis一级缓存的生命周期和SqlSession一致。
MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺。
MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。二级缓存
MyBatis的二级缓存是Application级别的缓存,它可以提高对数据库查询的效率,以提高应用的性能。
MyBatis的缓存机制整体设计以及二级缓存的工作模式:
SqlSessionFactory层面上的二级缓存默认是不开启的,二级缓存的开席需要进行配置,实现二级缓存的时候,MyBatis要求返回的POJO必须是可序列化的。 也就是要求实现Serializable接口,配置方法很简单,只需要在映射XML文件配置就可以开启缓存了,如果我们配置了二级缓存就意味着:
- 映射语句文件中的所有select语句将会被缓存。 映射语句文件中的所欲insert、update和delete语句会刷新缓存。
- 缓存会使用默认的Least Recently Used(LRU,最近最少使用的)算法来收回。 根据时间表,比如No Flush Interval,(CNFI没有刷新间隔),缓存不会以任何时间顺序来刷新。
- 缓存会存储列表集合或对象(无论查询方法返回什么)的1024个引用
- 缓存会被视为是read/write(可读/可写)的缓存,意味着对象检索不是共享的,而且可以安全的被调用者修改,不干扰其他调用者或线程所做的潜在修改。
mybatis核心流程
mybatis的核心流程,包括:初始化阶段、代理阶段、数据读写阶段
1、初始化阶段
初始化阶段就是mybatis解析xml文件(mybatis-config.xml和xxxmapper.xml),将xml里面的内容解析到Configuration对象中(全局单例模式)。
在将解析的流程之前先介绍一下几个对象
XmlConfigBuilder:解析mybatis-config.xml
XmlMapperBuilder:解析xxxMapper.xml
XmlStatementBuilder:解析xxxMapper.xml中的增删改查的sql
初始化过程的总结:
1、将xml的内容解析到configuration中
2、configuration中的关键的属性对应到xml的内容(1)Configuration属性填充
(2)resultMap解析
(3)MappedStatement内容图解
2、代理阶段
早些年在使用ibatis时候,其实是没有这个代理阶段的过程,我们使用如下的方式进行编程(面向sqlsession编程):
@Test public void queryUser() { SqlSession sqlSession = sqlSessionFactory.openSession(); TUser user = sqlSession .selectOne("com.taolong.mybatis.test.TUserMapper.slectUserById", 1); System.out.println(user); }
当apache对ibatis进行了修改之后编程了mybatis后,我们使用下面的方式进行编程(面向接口编程,达到解耦,程序员更喜欢的方式):
@Test public void queryUser2() { SqlSession sqlSession = sqlSessionFactory.openSession(); TUserMapper mapper = sqlSession.getMapper(TUserMapper.class); TUser user = mapper.queryUserById(1); System.out.println(user); }
从ibatis到mybatis的过程如下图所示,所以今天将的mybatis核心流程中的代理阶段和数据读写阶段就是如下图中的翻译的过程:
这里有个问题,就是TUserMapper是一个接口,并没有具体的实现类,那么mybatis是如何通过TUserMapper的接口来调用方法呢?带着这个问题,我们来阅读源码。同样通过debug的方式来跟踪源代码
进入getMapper,查看具体源码:
defaultSqlsession.getMapper():从configuration中动态获取Mapper
configuration.getMapper(): 从configuration中的注册mapper中心获取mapper对象
mapperRegistry.getMapper():@SuppressWarnings("unchecked") public <T> T getMapper(Class<T> type, SqlSession sqlSession) { //代理工厂,用于生成代理对象 final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) throw new BindingException("Type " + type + " is not known to the MapperRegistry."); try { //通过工厂模式生成mapper对象 return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } }
mapperProxyFactory.newInstance():
public T newInstance(SqlSession sqlSession) { //MapperProxy是代理类,该类实现了InvocationHandler,所以可以看出是使用了动态代理模式 final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); }
newInstance():
@SuppressWarnings("unchecked") protected T newInstance(MapperProxy<T> mapperProxy) { //返回动态代理生成的代理对象 return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); }
上面就是通过动态代理生成了一个mapper的对象进行增强,所以第二阶段的绑定阶段就是通过动态代理产生一个Mapper的对象,其实返回就是MapperProxy,当调用mapper的方法时,其实就是调用了MapperProxy中的invoke方法,这个动态代理的地方建议大家深入的了解一下,重点看一下MapperProxyFactory类、MapperProxy、InvocationHandler、Proxy如何生成动态代理。到这里,我们已经能够解释上面的一个问题(就是InternalToolMapper是一个接口,并没有具体的实现类,那么mybatis是如何通过InternalToolMapper的接口来调用方法呢?)因为这里的InternalToolMapper是生成的一个动态代理类来代理InternalToolMapper,而非真正的InternalToolMapper。
代理阶段流程梳理:
1、先从Configuration配置类MapperRegistry对象中获取mapper接口和对应的代理对象工厂信息(MapperProxyFactory)。
2、利用代理对象工厂MapperProxyFactory创建实际代理类(MapperProxy)。
3、在MapperProxy类中通过MapperMethod类对象内保存的中对应方法的信息,以及对应的sql语句的信息进行分析,最终确定对应的增强方法进行调用。3、数据读写阶段
既然知道TUserMapper是生成的动态代理类,那么当调用int user = mapper.updateBySql(“sql”);时应该是调用了MapperProxy的invoke方法,我们继续跟着流程走,在继续流程之前我们带着第二个问题思考:sqlsession中有很多方法(select One,selectList,selectMap等)在ibatis中我们在代码中注明了要调用哪个方法,但是在mybatis没有注明,那么mybatis是如何知道调用的是哪个方法呢?
(1)MapperProxy.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); } else if (isDefaultMethod(method)) { //判断是否是默认方法,不增强 return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } //MapperMethod方法就是封装了mapper接口中对应方法的信息,它是mapper接口和sql语句的桥梁 final MapperMethod mapperMethod = cachedMapperMethod(method); //如果是我们定义在mapper的操作数据库的方法,则执行下面代码 return mapperMethod.execute(sqlSession, args); } private MapperMethod cachedMapperMethod(Method method) { MapperMethod mapperMethod = methodCache.get(method); if (mapperMethod == null) { mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()); methodCache.put(method, mapperMethod); } return mapperMethod; }
这里mappedMethod非常重要,因为它封装了mapper接口中的方法信息,它是mapper接口和sql语句的桥梁,是通过它来确定调用sqlsession的具体的哪个方法,大家可以先看一下它的数据结构,MappedMethod中的SqlCommand里面封装了SqlCommandType(insert、update、delete、select),里面的name封装了对应的mapper接口名和方法名;MappedMethod中的MethodSignature封装了接口方法的返回值类型,以及ParamNameResolver可以解析出接口的入参。所以通过MappedMethod就可以知道他是调用sqlsession的哪个方法(sqlcommandType可以知道是增删改查的哪一个,再看它的返回类型是list还是一个对象就知道是调用sqlsession的selectOne还是selectList…)以及xml中的具体的哪个方法.。
sqlSession原子的操作最终就在mapperMethod.execute()出现了。也就是当我们通过mapper对象调用接口方法时,方法被路由到MapperProxy中,最终通过MapperMethod核心类包装进行当前会话的原子CRUD操作。
(2)mapperMethod.execute()
public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); break; } case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } case SELECT: //根据methodSignature,判断是调用执行的是哪个方法, if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { //这些方法最终都是会调用sqlsession.select方法 result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); //其实selectOne其实最终也会调用selectList,然后去集合的第一个内容而已 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; }
(3)DefaultSqlSession.selectOne()
public <T> T selectOne(String statement, Object parameter) { // Popular vote was to return null on 0 results and throw exception on too many. //还是会调用selectList方法 List<T> list = this.<T>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; } }
(4)selectList()
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { //通过接口名+方法名获取(namespace+方法名)MappedStatement MappedStatement ms = configuration.getMappedStatement(statement); //调用executor的query方法,查询 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(); } }
这里注意最后一句executor.query()方法,这里是使用了代理设计模式,BaseExecutor和CachingExecutor都是实现了Executor方法,那么这里是进入了CachingExecutor方法,这里面会涉及到mybatis中二级缓存的一些逻辑
(5)cachingExcutor.query()
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { //获取mappedStatement的Cache Cache cache = ms.getCache(); if (cache != null) { //是否需要清空cache(在xml文件中的cache标签设置,比如flushInterval时间到期) flushCacheIfRequired(ms); //判断是否使用cache,xml文件中设置 if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); //直接从缓存中获取结果 @SuppressWarnings("unchecked") List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); //查询的结果保存到缓存中 tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } //如果没有设置缓存,那么就直接调用被代理的对象方法查询 return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
这里就是处理mybatis的二级缓存的逻辑,这是在一级缓存之前处理的,所以如果同时配置二级缓存和一级缓存那么会先使用二级缓存。另外判断是否使用二级缓存需要在mybatis-config.xml中配置属性cacheEnable和在相应的xml中配置cache标签属性。最后就是使用代理设计模式调用BaseExecutor.query()(baseExecutor的子类)
(6)baseExecutor.query()
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); if (closed) { throw new ExecutorException("Executor was closed."); } //判断是否需要清空一级缓存 if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); } List<E> list; try { queryStack++; //从一级缓存中取出结果,返回 list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; if (list != null) { handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { //从数据中查询结果 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { queryStack--; } if (queryStack == 0) { for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } // issue #601 deferredLoads.clear(); if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } } return list; }
这里是涉及到mybatis的一级缓存的逻辑,能从一级缓存中获取结果就取出结果,否则就查询数据库。有两个需要注意的地方**:1,一级缓存中的CacheKey是计算时非常严格的它是由mappedStatement,parameter,rowBounds和boundSql一起生成的一个值**;2,如果有update、insert、delete这些操作,缓存是会清空的。感兴趣的可以深入了解一下。接着流程往下
(7)queryFromDatabase()
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; localCache.putObject(key, EXECUTION_PLACEHOLDER); try { //查询结果,这里使用了模板设计模式, list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { localCache.removeObject(key); } //查询的结果加入到缓存中,方便下次使用 localCache.putObject(key, list); //这下面应该是存储过程相关的,如果是存储过程那么类型就是Callable if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; }
接着往下看,我们是调用doQuery()方法,但是BaseExecutor是没有实现doQuery(),这里实际上使用了模板设计模式,将操作延迟到子类中(BatchExecutor,CloseExecutor,ReuseExecutor,SimpleExecutor),
SimpleExecutor:默认配置,使用statement对象访问数据库,每次访问都要创建statement对象
ReuseExecutor:使用预编译PrepareStatement对象访问数据库,访问时,会重用缓存中的statement对象
BatchExecutor:实现批量操作多条sql的能力
不同的子类有不同的实现,如果想了解更多关于模板设计模式,请参考(模板设计模式),我们这里看SimpleExecutor.doQuery()
(8)simpleExecutor.doQuery()
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); //这里面生成的是statementHandler的实现类 RoutingStatementHandler StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); stmt = prepareStatement(handler, ms.getStatementLog()); //调用 RoutingStatementHandler.query() return handler.<E>query(stmt, resultHandler); } finally { closeStatement(stmt); } }
这里为什么要使用RoutingStatementHandler.query()方法呢,其实是使用了静态代理设计模式,请继续往下看
(9)RoutingStatementHandler.query()
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { return delegate.<E>query(statement, resultHandler); }
我们来看看RoutingStatementHandler中被代理的对象是哪个
(9-1)RoutingStatementHandler()
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { //其实从这里判断是选择代理哪个statement switch (ms.getStatementType()) { //默认的statement case STATEMENT: delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; //预编译的statement case PREPARED: delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; //存储过程的statement case CALLABLE: delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; default: throw new ExecutorException("Unknown statement type: " + ms.getStatementType()); } }
其实这里就是使用静态的代理模式判断到底需要代理哪一个statement,不得不赞叹mybatis的代码写的非常优雅,一个看似非常简单的地方,如果换作是我们直接在就在上面使用if else判断得了。我们这里显然是用了PreparedStatement,因为会预编译
(10)PreparedStatement.query()
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; //执行查询 ps.execute(); //处理结果集 return resultSetHandler.<E> handleResultSets(ps); }
其实到这里就已经完成了一次数据库的查询操作。
MapperRegistry : mapper接口和对应的代理对象工厂的注册中心;MapperProxyFactory:用于生成mapper接口动态代理的实例对象;
MapperProxy:实现了InvocationHandler接口,它是增强mapper接口的实现;
MapperMethod:封装了Mapper接口中对应方法的信息,以及对应的sql语句的信息;它是mapper接口与映射配置文件中sql语句的桥梁,MapperMethod对象不记录任何状态信息,所以它可以在多个代理对象之间共享;
mybatis如何获取数据源
<environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> </dataSource> </environment> </environments> // 获取配置文件 String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); // 构建SqlSessionFactory对象 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { // 创建 XMLConfigBuilder 对象 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); // 执行 XML 解析 // DefaultSqlSessionFactory对象 return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } }
mybatis如何获取执行SQL
与获取数据库源类似,只要解析Mapper配置文件中的对应标签,就可以获得对应的sql语句。之前我们讲过,SqlSessionFactory中的configuration属性保存数据库源信息,事实上这个configuration属性将整个配置文件的信息都给封装成一个类来保存了。解析的前半部分与之前一样,分歧点在之前提到的parseConfiguration方法,其中在environmentsElement方法下面还有一个mapperElement方法。
配置文件中mappers标签加载mapper文件的方式共有四种:resource、url、class、package。代码中的if-else语句块分别判断四种不同的加载方式,可见package的优先级最高。parent是配置文件中mappers标签中的信息,通过外层的循环一个一个读取多个Mapper文件。这里使用的方式是resource,所以会执行光标所在行的代码块,进入**mapperParser.parse()**方法。
我们要的是 mapper 标签的内容,因此我们关注 configurationElement(parser.evalNode("/mapper")) 这一句,进入 configurationElement 方法。
context 就是我们解析整个 Mapper 文件 mapper 标签中的内容,既然现在得到了内容,那只需再找到对应的标签就能获得sql语句了。注意 buildStatementFromContext(context.evalNodes(“select|insert|update|delete”)),我们看到了熟悉的 select、insert、update、delete,这些标签里就有我们写 sql 语句。进入 buildStatementFromContext 方法:
list 保存了我们在 Mapper 文件中写的所有含有 sql 语句的标签元素,用一个循环遍历 list 的每一个元素,分别将每一个元素的信息保存到 statementParser 中。进入 parseStatementNode 方法。
这个方法代码内容很多,仅摘出节选,里面定义了很多局部变量,这些变量用来保存sql语句标签(例如)的参数信息(例如缓存useCache)。再把所有参数传到addMappedStatement中。进入addMappedStatement方法。
MappedStatementstatement=statementBuilder.build(),使用build方法得到MappedStatement实例,这个类封装了每一个含有sql语句标签中所有的信息,再是configuration.addMappedStatement(statement),保存到configuration中。MyBatis 如何执行 sql 语句?
既然有了SqlSessionFactory,我们可以从中获得SqlSession的实例。开启session的语句是SqlSessionsession=sessionFactory.openSession(),进入openSession方法。
最终会执行openSessionFromDataSource方法。在之前environment已经有了数据库源信息,调用configuration.newExecutor方法。
Executor叫做执行器,Mybatis一共有三种执行器,用一个枚举类ExecutorType保存,分别是SIMPLE,REUSE,BATCH,默认就是SIMPLE。if-else语句判断对应的类型,创建不同的执行器。在代码末端处有个if判断语句,如果cacheEnabled为true,则会创建缓存执行器,默认是为true,即默认开启一级缓存。回到openSessionFromDataSource方法,最终返回一个DefaultSqlSession实例。得到session我们就可以执行sql语句了。SqlSession提供了在数据库执行SQL命令所需的所有方法。你可以通过SqlSession实例来直接执行已映射的SQL语句,以selectOne方法为例,进入该方法后发现,最终会调用到selectList方法。
public static void main(String[] args) throws IOException { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //开启session SqlSessionsession=sessionFactory.openSession() //SqlSession接口的selectOne(...)方法运行自己写的方法如下:第一个参数是接口方法的全限定命名,第二个参数是方法需要的参数 User one =sqlSession.selectOne("com.it55.mybatis.mapper.MyMapper.getOne", 1); }
configuration.getMappedStatement(statement)得到了我们之前保存的MappedStatement对象,再调用executor.query 方法,调用 query 方法之前会执行 wrapCollection 方法,保存 sql 语句中用户传入的参数。进入 query 方法。
configuration.getMappedStatement(statement)得到了我们之前保存的MappedStatement对象,再调用executor.query 方法,调用 query 方法之前会执行 wrapCollection 方法,保存 sql 语句中用户传入的参数。进入 query 方法。
boundSql 里面就有我们要执行的 sql 语句,CacheKey 是用来开启缓存的。执行父类 BaseExecutor 中的 createCacheKey 方法,通过 id,offsetid,limited,sql 组成一个唯一的 key,调用下一个 query 方法。
Cache cache = ms.getCache() 是二级缓存,二级缓存为空,直接调用 query 方法。
list = resultHandler == null ? (List) localCache.getObject(key) : null 传入 key 值在本地查询,如果有返回证明 key 已经缓存到本地,直接从本地缓存获取结果。否则 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql),去数据库查询。
localCache.putObject(key, EXECUTION_PLACEHOLDER) 首先将 key 缓存至本地,下一次查询就能找到这个 key 了。进入 doQuery 方法。
stmt = prepareStatement(handler, ms.getStatementLog()),得到一个 Statement。进入 prepareStatement 方法。
我们看到了一个熟悉的 Connection 对象,这个就是原生 JDBC 的实例对象。回到 doQuery 方法,进入 handler.query(stmt, resultHandler) 方法。
statement 强转型为 PreparedStatement 类型,这下我们又得到了 PreparedStatement 的类型实例了,调用 execute 方法,这个方法也是属于原生 JDBC。执行完成后 return resultSetHandler.handleResultSets(ps),进入 handleResultSets 方法。参考:
https://tech.meituan.com/2018/01/19/mybatis-cache.html -
Mybatis原理
2022-04-21 16:39:49文章目录- 什么是Mybatis?- Mybaits的优点:- MyBatis框架的缺点:- MyBatis与Hibernate有哪些不同?- 架构MyBatis缓存一级缓存- mybatis技巧和细节1、#{}和${}的区别是什么?2、通常一个Xml映射文件,都会写一个...文章目录
- - 什么是Mybatis?
- - Mybaits的优点:
- - MyBatis框架的缺点:
- - MyBatis与Hibernate有哪些不同?
- - 架构
- MyBatis缓存
- - mybatis技巧和细节
- 1、#{}和${}的区别是什么?
- 2、通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗?
- 3、Mybatis是如何进行分页的?分页插件的原理是什么?
- 4、Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式?
- 5、Mybatis动态sql有什么用?执行原理?有哪些动态sql?
- 6、Xml映射文件中,除了常见的select|insert|updae|delete标签之外,还有哪些标签?
- 7、如何获取自动生成的(主)键值?
- 8、Mybatis的Xml映射文件中,不同的Xml映射文件,id是否可以重复?(结合4)
- 9、MyBatis实现一对一有几种方式?具体怎么操作的?
- 10、MyBatis实现一对多有几种方式,怎么操作的?
- 11、Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?
- 12、什么是MyBatis的接口绑定?有哪些实现方式?
- 13、使用MyBatis的mapper接口调用时有哪些要求?
- 14、Mapper编写有哪几种方式?
- 15、简述Mybatis的插件运行原理,以及如何编写一个插件。
- 什么是Mybatis?
MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。
1、Mybatis是一个半ORM(对象关系映射)框架,底层封装了JDBC,是程序员在开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。使得程序员可以花更多的精力放到业务开发中。另外,程序员直接编写原生态sql,严格控制sql执行性能,灵活度高。
2、MyBatis 可以使用简单的 XML文件 或注解方式来配置和映射原生信息,将 POJO映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。
3、通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和 statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。(从执行sql到返回result的过程)。- Mybaits的优点:
1、基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。
2、与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;
3、很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。
4、能够与Spring很好的集成;
5、提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。- MyBatis框架的缺点:
1、SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。
2、SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。- MyBatis与Hibernate有哪些不同?
1、Mybatis是一个半自动的ORM框架,在查询关联对象或关联集合对象时,需要手动编写sql语句来完成;Hibernate是全自动ORM映射工具,查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,不需要编写sql.
2、Mybatis直接编写原生态sql,可以严格控制sql执行性能,灵活度高,非常适合对性能要求高,需求变化频繁的项目;但是如果涉及到较多的字段或者关联多表时,sql语句编写量大且对开发人的sql语句编写功底要求高。
3、Hibernate对象/关系映射能力强,数据库无关性好,适合需求变化不大的项目,使用hibernate开发可以节省很多代码,提高效率。MyBatis Hibernate 半自动 自动 灵活度高 灵活度低 代码量大 代码量小 - 架构
我们把Mybatis的功能架构分为三层:
(1)API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。
(2)数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。
(3)基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。
1、SqlSessionFactoryBuilder【全局的对象】
每一个MyBatis的应用程序的入口是:SqlSessionFactoryBuilder。它的作用是通过XML配置文件创建Configuration对象(当然也可以在程序中自行创建),然后通过build方法创建SqlSessionFactory对象。2、SqlSessionFactory【全局的对象】
每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为中心的。SqlSessionFactory是由SqlSessionFactoryBuilder 从 XML 配置文件或通过Java的方式构建出 的实例,主要功能是创建SqlSession(会话)对象(MybatisSqlSessionFactoryBean);SqlSessionFactory对象一个必要的属性是Configuration对象;SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,建议使用单例模式或者静态单例模式。一个SqlSessionFactory对应配置文件中的一个环境(environment),如果你要使用多个数据库就配置多个环境分别对应一个SqlSessionFactory。3、 SqlSession
作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能。SqlSession通过调用api的Statement ID找到对应的MappedStatement对象。SqlSession是一个接口,它有2个实现类,分别是DefaultSqlSession(默认使用)以及SqlSessionManager;默认使用DefaultSqlSession,它有两个必须配置的属性:Configuration和Executor,SqlSession通过内部存放的执行器(Executor)来对数据进行CRUD。由于不是线程安全的,所以SqlSession对象的作用域需限制方法内;每一次操作完数据库后都要调用close对其进行关闭,官方建议通过try-finally来保证总是关闭SqlSession。4、 Executor
MyBatis执行器,是MyBatis 调度的核心。Executor对象在创建Configuration对象的时候创建,并且缓存在Executor对象里。Executor:负责SQL语句的生成,调用StatementHandler访问数据库,查询缓存的维护;Executor(负责动态SQL的生成和查询缓存的维护)将MappedStatement对象进行解析,sql参数转化、动态sql拼接,生成jdbc Statement对象;Executor(执行器)接口有两个实现类,其中BaseExecutor有三个继承类分别是BatchExecutor(重用语句并执行批量更新),ReuseExecutor(重用预处理语句prepared statements),SimpleExecutor(普通的执行器)。5、 StatementHandler
封装了JDBC Statement操作,负责对JDBCstatement的操作,如设置参数、将Statement结果集转换成List集合,是真正访问数据库的地方,并调用ResultSetHandler处理查询结果。6、ResultSetHandler
ParameterHandler 负责将用户传递的参数转换成JDBC Statement 所需要的参数
ResultSetHandler负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;处理查询结果。
TypeHandler负责java数据类型和jdbc数据类型之间的映射和转换7、 MappedStatement :
MappedStatement就是用来存放我们SQL映射文件中的信息包括sql语句,输入参数,输出参数等等。一个SQL节点对应一个MappedStatement对象。借助MappedStatement中的结果映射关系,将返回结果转化成HashMap、JavaBean等存储结构并返回。8、 SqlSource
负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回。
BoundSql表示动态生成的SQL语句以及相应的参数信息
ConfigurationMyBatis所有的配置信息都维持在Configuration对象之中MyBatis缓存
如果要在同一个会话里面共享一级缓存,这个对象肯定是在SqlSession 里面创建的,作为SqlSession 的一个属性。DefaultSqlSession 里面只有两个属性,Configuration 是全局的,所以缓存只可能放在Executor 里面维护缓存。Executor执行查询时先判断缓存,若缓存命中,直接返回缓存中的结果,否则继续之后的步骤查询数据库。
MyBatis使用底层为HashMap的Cache对象,通过各种装饰类对其层层装饰,以实现各种功能和隔离不同功能逻辑
MyBatis缓存分为一级缓存和二级缓存,默认开启一级缓存
数据查询优先级:二级缓存 -> 一级缓存 -> 数据库一级缓存
缓存默认Session级别(可设为Statement),同一会话的重复Sql语句可使用缓存
不同Session间缓存相互独立,分布式查询可能读到脏数据
Executor收到Sql语句后,先查询自己的localCache(HashMap),缓存命中则直接返回结果,缓存命中失败则继续查询数据库后将结果写入localCache一级缓存和sqlsession之间的关系
一个sqlsession会有一个自己专属的map用作一级缓存,但是不同的会话里面,即使执行的SQL 一模一样(通过一个Mapper 的同一个方法的相同参数调用),也不能使用到一级缓存。
多数据源的情况下会连接不同的sqlsessionfactory,不同的sqlsessionfactory则对应不同的sqlsession,所以一级缓存不同,因此查询和更新的时候不会互相影响。一级缓存的生命周期有多长?
MyBatis在开启一个数据库会话时,会 创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用;
如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用;
SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用;SqlSession 一级缓存的工作流程:
对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果。
- 判断从Cache中根据特定的key值取的数据数据是否为空,即是否命中;
- 如果命中,则直接将缓存结果返回;
- 如果没命中:
- 去数据库中查询数据,得到查询结果;
- 将key和查询到的结果分别作为key,value对存储到Cache中;
- 将查询结果返回;
一级缓存的不足:
使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个会话或者分布式环境下,会存在脏数据的问题。例如:
(1)A线程内的SqlSession先获取id为1的News对象。
(2)CPU切换给B线程,B线程内的SqlSession修改id为1的News对象。
(3)A线程内的SqlSession再次获取id为1的News对象。
A线程内的SqlSession第二次获取id为1的News对象时,MyBatis不应该使用缓存——因为B线程已经更新了底层数据表中的数据,A线程内SqlSession内缓存的News对象是脏数据。为了避免MyBatis一级缓存产生这种脏数据,最佳实践有两个要点:
-
尽量使用短生命周期的SqlSession!
先说说第一种实践方式适合的场景:对于数据实时性要求没那么高(允许有一定的脏数据)的应用,只要项目避免使用长生命周期的SqlSession,即使MyBatis的一级缓存产生了脏数据,但由于SqlSession的生命周期特别短暂,这种脏数据也许处于可控范围之内。 -
避免使用SqlSession的一级缓存。
再说说第二种实践方式适合的场景:对于数据实时性要求非常高的引用,项目基本不允许使用脏数据,此时就应该避免使用MyBatis的一级缓存!但请记住:MyBatis并不允许关闭一级缓存,因为它需要一级缓存来处理循环引用等问题。
为了避免使用MyBatis一级缓存,程序有两种方式:
① 每个SqlSession永远只执行单次查询。如果要执行第二次查询,请重新打开另一个SqlSession!上面示例之所以产生脏数据,关键就因为程序用同一个SqlSession两次查询了id为1的News对象。如果每个SqlSession只执行单次查询,那么一级缓存几乎就不会产生作用了,这样可避免一级缓存产生脏数据。
② 将localCacheScope设为STATEMENT级,这样可避免在SqlSession范围内使用一级缓存,但这种方式依然有产生脏数据的风险。
可以手动清理缓存吗?
可以通过sqlSession.clearCache();操作实现
二级缓存
为解决跨SqlSession的缓存问题,MyBatis提供二级缓存功能。
二级缓存全局有效,划分到mapper级别(namespace定义),而非整个application使用同一缓存。
二级缓存使用CachingExecutor包装Executor,CachingExecutor负责与全局二级缓存交互
CachingExecutor优先查询二级缓存,如没有匹配则委托给它包装的Executor匹配的一级缓存
什么时候开启二级缓存?
一级缓存默认是打开的,二级缓存需要配置才可以开启。那么我们必须思考一个问题,在什么情况下才有必要去开启二级缓存?
因为所有的增删改都会刷新二级缓存,导致二级缓存失效,所以适合在查询为主的应用中使用,比如历史交易、历史订单的查询。否则缓存就失去了意义。二级缓存的脏数据
如果多个namespace 中有针对于同一个表的操作,比如Blog 表,如果在一个namespace 中刷新了缓存,另一个namespace 中没有刷新,就会出现读到脏数据的情况。所以,推荐在一个Mapper 里面只操作单表的情况使用。
如果要让多个namespace 共享一个二级缓存,应该怎么做?跨namespace 的缓存共享的问题,可以使用来解决:
cache-ref 代表引用别的命名空间的Cache 配置,两个命名空间的操作使用的是同一个Cache。在关联的表比较少,或者按照业务可以对表进行分组的时候可以使用。
注意:在这种情况下,多个Mapper 的操作都会引起缓存刷新,粒度过大,频繁刷新缓存,缓存的意义已经不大了。第三方缓存方案
除了上述自定义缓存的方式,你也可以通过实现你自己的缓存,或为其他第三方缓存方案创建适配器,来完全覆盖缓存行为。
MyBatis Cache是基于本地的,分布式环境下必然会读取脏数据,用户自定义实现MyBatis的Cache接口开发成本较高,直接使用Redis/Memcached等分布式缓存成本更低,安全性更高。- mybatis技巧和细节
1、#{}和${}的区别是什么?
#{}用来接收参数,可以是简单类型也可以是pojo 类型;使用#{}可以有效地防止SQL注入,提高系统安全性。会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
{}时占位符,就是把${}替换成变量的值。2、通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗?
Dao接口即Mapper接口。接口的全限名,就是映射文件中的namespace的值;接口的方法名,就是映射文件中Mapper的Statement的id值;接口方法内的参数,就是传递给sql的参数。
Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MapperStatement。在Mybatis中,每一个 、、、 标签,都会被解析为一个MapperStatement对象。
Mapper接口里的方法,是不能重载的,因为是使用 全限名+方法名 的保存和寻找策略。Mapper 接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Mapper接口生成代理对象proxy,代理对象会拦截接口方法,转而执行MapperStatement所代表的sql,然后将sql执行结果返回。3、Mybatis是如何进行分页的?分页插件的原理是什么?
Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页。可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。4、Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式?
第一种:通过 来映射字段名和实体类属性名的一一对应的关系。
第二种是使用sql列的别名功能,让字段名的别名和实体类的属性名一致。
有了列名与属性名的映射关系后,Mybatis通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。5、Mybatis动态sql有什么用?执行原理?有哪些动态sql?
Mybatis动态sql可以在Xml映射文件内,以标签的形式编写动态sql,执行原理是根据表达式的值 完成逻辑判断并动态拼接sql的功能。
Mybatis提供了9种动态sql标签: trim|where|set|foreach|if|choose|when|otherwise|bind。6、Xml映射文件中,除了常见的select|insert|updae|delete标签之外,还有哪些标签?
、、、、 ,加上动态sql的9个标签,其中 为sql片段标签,通过 标签引入sql片段, 为不支持自增的主键生成策略标签。
7、如何获取自动生成的(主)键值?
insert 方法总是返回一个int值 ,这个值代表的是插入的行数。如果采用自增长策略,自动生成的键值在 insert 方法执行完后可以被设置到传入的参数对象中。
select last_insert_id(); insert into stu2.user(username,birthday,sex,address) values(#{username},#{birthday},#{sex},#{address}); 9:在mapper中如何传递多个参数?1、DAO层的函数
2、使用 @param 注解:然后,就可以在xml像下面这样使用(推荐封装为一个map,作为单个参数传递给mapper):
3、多个参数封装成map8、Mybatis的Xml映射文件中,不同的Xml映射文件,id是否可以重复?(结合4)
不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复;
原因就是namespace+id是作为Map <String,MapperStatement> 的key使用的,如果没有namespace,就剩下id,那么,id重复会导致数据互相覆盖。有了namespace,自然id就可以重复,namespace不同,namespace+id自然也就不同。9、MyBatis实现一对一有几种方式?具体怎么操作的?
有联合查询和嵌套查询;联合查询是几个表联合查询,只查询一次, 通过在resultMap里面配置association节点配置一对一的类就可以完成(使用resultType);嵌套查询是先查一个表,根据这个表里面的结果的 外键id,去再另外一个表里面查询数据,也是通过association配置,但另外一个表的查询通过select属性配置(使用resultMap)。
10、MyBatis实现一对多有几种方式,怎么操作的?
有联合查询和嵌套查询。联合查询是几个表联合查询,只查询一次,通过在resultMap里面的collection节点配置一对多的类就可以完成(使用resultType);嵌套查询是先查一个表,根据这个表里面的 结果的外键id,去再另外一个表里面查询数据,也是通过配置collection,但另外一个表的查询通过select节点配置(使用resultMap)。
11、Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?
Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。
它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB.getName,拦截器invoke方法发现a.getB是值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB.getName方法的调用。这就是延迟加载的基本原理。12、什么是MyBatis的接口绑定?有哪些实现方式?
接口绑定,就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定, 我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置。
接口绑定有两种实现方式:一种是通过注解绑定,就是在接口的方法上面加上 @Select、@Update等注解,里面包含Sql语句来绑定;另外一种就是通过xml里面写SQL来绑定, 在这种情况下,要指定xml映射文件里面的namespace必须为接口的全路径名。当Sql语句比较简单时候,用注解绑定, 当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多。13、使用MyBatis的mapper接口调用时有哪些要求?
1、Mapper接口方法名和mapper.xml中定义的每个sql的id相同;
2、Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql 的parameterType的类型相同;
3、Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同;
4、Mapper.xml文件中的namespace即是mapper接口的类路径。14、Mapper编写有哪几种方式?
第一种:接口实现类继承SqlSessionDaoSupport:使用此种方法需要编写mapper接口,mapper接口实现类、mapper.xml文件。1、在sqlMapConfig.xml中配置mapper.xml的位置2、定义mapper接口3、实现类集成SqlSessionDaoSupport mapper方法中可以this.getSqlSession进行数据增删改查。4、spring 配置
第二种:使用 org.mybatis.spring.mapper.MapperFactoryBean :1、在sqlMapConfig.xml中配置mapper.xml的位置,如果mapper.xml和mappre接口的名称相同且在同一个目录,这里可以不用配置2、定义mapper接口:3、mapper.xml中的namespace为mapper接口的地址4、mapper接口中的方法名和mapper.xml中的定义的statement的id保持一致5、Spring中定义
第三种:使用mapper扫描器:1、mapper.xml文件编写:mapper.xml中的namespace为mapper接口的地址;mapper接口中的方法名和mapper.xml中的定义的statement的id保持一致;如果将mapper.xml和mapper接口的名称保持一致则不用在sqlMapConfig.xml中进行配置。2、定义mapper接口:注意mapper.xml的文件名和mapper的接口名称保持一致,且放在同一个目录3、配置mapper扫描器:4、使用扫描器后从spring容器中获取mapper的实现对象。
15、简述Mybatis的插件运行原理,以及如何编写一个插件。
答:Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke方法,当然,只会拦截那些你指定需要拦截的方法。
编写插件:实现Mybatis的Interceptor接口并复写intercept方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件。
16、SqlSession创建过程和相关对象生命周期?
SqlSessionFactoryBuilder—SqlSessionFactory—SqlSession—SqlMapper
- SqlSessionFactoryBuilder在构建SqlSessionFactory后即可销毁
- SqlSessionFactory存在于整个MyBatis应用中,类似数据库连接池,不断创建SqlSession
- SqlSession类似于数据库连接,在完成一次业务请求后回收
- SqlMapper由SqlSession创建,生命周期SqlMapper<=SqlSession
-
Springboot整合mybatis原理
2021-04-21 10:11:47加载过程 1、读取META-INF/spring.factories配置文件里需要自动装载的类 mybatis-spring-boot-starter依赖的作用实际是提供一个pom文件,该pom文件内有mybatis需要的所有依赖,其中比较重要的有mybatis-spring-boot-...文章目录
我们在Springboot中使用mybatis时只需要简单的几个配置就可以了:1、在pom文件中引入mybatis的starter。2、配置数据库连接池。3、在Springboot配置文件里配置mybatis相关参数。4、编写自己的dao以及mapper配置文件。那么在我们Spring项目里注入dao并进行CRUD时,mybatis是怎么被Springboot加载以及怎么执行jdbc操作的?这里先贴出配置:- 配置datasource,比如用druid链接池配置
spring: datasource: #druid相关配置 type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.jdbc.Driver #配置数据库连接 druid: url: jdbc:mysql://localhost:3306/test-db?useUnicode=true&allowMultiQueries=true&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 initial-size: 10 max-active: 100 min-idle: 10 max-wait: 60000 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 validation-query: SELECT 1 FROM DUAL test-while-idle: true test-on-borrow: false test-on-return: false connectionInitSqls: set names utf8mb4
- 配置mybatis参数,指定mapper文件路径
mybatis: mapper-locations: classpath:mapper/*.xml
- 按业务需要定义dao接口,并加上
@Mapper
注解
@Mapper public interface UserInfoDao { int insert(UserInfoDO userInfoDO) UserInfoDO getById(long id); int update(UserInfoDO userInfoDO); int delete(long id); }
- 配置mapper文件,写好sql与dao接口的映射关系,其中
namespace
是对应的dao接口完整类名
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.hj.dao.UserInfoDao"> <resultMap id="BaseResultMap" type="com.hj.DO.UserInfoDO"> <id column="id" property="id"/> <result column="userId" property="userId"/> <result column="name" property="name"/> </resultMap> <insert id="insert" parameterType="com.hhdd.DO.UserInfoDO"> insert ... </insert> <select id="getById" resultMap="BaseResultMap" parameterType="java.lang.Long"> select ... </select> <update id="update" parameterType="com.hj.DO.UserInfoDO"> update ... </update> <delete id="delete"> delete ... </delete> </mapper>
加载过程
1、读取
META-INF/spring.factories
配置文件里需要自动装载的类mybatis-spring-boot-starter
依赖的作用实际是提供一个pom文件,该pom文件内有mybatis
需要的所有依赖,其中比较重要的有mybatis-spring-boot-autoconfigure
,如下图:
在
mybatis-spring-boot-autoconfigure
这个包内包含META-INF/spring.factories
配置文件,Springboot就是通过该配置文件内定义的启动类来拉起mybatis的,如下图:
而Springboot触发读取这个配置文件的逻辑在
@EnableAutoConfiguration
注解上@Import
注解引入的AutoConfigurationImportSelector.class
类的selectImports
方法里,有兴趣的可以在这个方法里打个断点debug下流程。2、解析
MybatisAutoConfiguration
类里的注解信息,将需要管理的Bean注册到Spring容器在《Springboot之Bean的加载过程》中讲到将类解析成
BeanDefinition
并最终实例化成Bean的过程,这里会向Spring容器注册几个重要的类:2.1 注册
SqlSessionFactory
,并根据mapper配置文件解析出dao与具体jdbc操作、resultMap与实体类等的映射关系代码如下:
@Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); factory.setDataSource(dataSource); factory.setVfs(SpringBootVFS.class); if (StringUtils.hasText(this.properties.getConfigLocation())) { factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation())); } Configuration configuration = this.properties.getConfiguration(); if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) { configuration = new Configuration(); } if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) { for (ConfigurationCustomizer customizer : this.configurationCustomizers) { customizer.customize(configuration); } } factory.setConfiguration(configuration); ... if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) { factory.setMapperLocations(this.properties.resolveMapperLocations()); } return factory.getObject(); }
SqlSessionFactory
的实例化是通过SqlSessionFactoryBean.getObject()
实现的,该类会被注入DataSource
对象(负责管理数据库连接池,Session指的是一次会话,而这个会话是在DataSource
提供的Connection
上进行的),SqlSessionFactory.getObject()
方法里会根据我们mybatis相关配置(比如上面的mybatis.mapper-locations配置)找到并解析我们的mapper文件,解析出sql与dao方法里的映射、ResultMap与具体实体类的映射等,并放到SqlSessionFactory
的Configuration
中缓存下来,在后续调用过程中会通过这些信息来匹配jdbc操作。2.2 注册实现了CRUD操作的
SqlSessionTemplate
类该类实现了我们常用的CRUD操作,在执行CRUD时,会通过
SqlSessionFactory
对象获取Session来操作,所以会持有SqlSessionFactory
对象@Bean @ConditionalOnMissingBean public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { ExecutorType executorType = this.properties.getExecutorType(); if (executorType != null) { return new SqlSessionTemplate(sqlSessionFactory, executorType); } else { return new SqlSessionTemplate(sqlSessionFactory); } }
2.3 注册
AutoConfiguredMapperScannerRegistrar
类来扫描被@Mapper
标注的类该类负责遍历被@Mapper标注的类,并将这些扫描到的类解析成
BeanDefinition
注册到Spring容器中,核心逻辑在registerBeanDefinitions
中,需要注意的一点是,在扫描到被@Mapper标注的类时,会将这些类解析成beanClass为MapperFactoryBean
的BeanDefinition,同时会告知Spring容器在将这个BeanDefinition
实例化成Bean时,需要注入SqlSessionFactory
和SqlSessionTemplate
对象,截图如下:
从这里可以看出我们在代码中注入的dao实际上是一个动态代理类,由
MapperFactoryBean
这个FactoryBean
的getObject()
方法生成3 在注入dao时,触发该dao对应的
MapperFactoryBean.getObject()
方法来注入动态代理类MapperFactoryBean.getObject()
逻辑由BeanFactory的getBean(string beanName)
触发,getObject()
代码如下:public T getObject() throws Exception { return this.getSqlSession().getMapper(this.mapperInterface); }
getSqlSession()
获取的是SqlSessionTemplate
对象,this.mapperInterface
就是我们的dao层接口,比如开头demo里的被@Mapper
标注的UserInfoDao
,最终会通过MapperProxyFactory
来生成动态代理类,代码如下:public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); }
这里的参数
SqlSession
就是SqlSessionTemplate
对象,可以看出是通过基于接口的JDK的Proxy来生成动态代理,在我们代码中进行CRUD时候,最后都会通过MapperProxy
类(该类实现了InvocationHandler接口)的invoke(Object proxy, Method method, Object[] args)
方法来处理.调用过程
通过上面的加载过程,我们了解到最后注入到业务代码的是一个动态代理类,我们再看下这个动态代理类的调用过程,主要逻辑在
MapperProxy
类(该类实现了InvocationHandler接口)的invoke(Object proxy, Method method, Object[] args)
方法,我这里以select请求为例,代码如下:@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); }
SqlSessionTemplate
在加载阶段会根据配置的mapper文件解析出对应的映射关系,并封装好元数据信息(包括需要执行的sql、返回类型等),而MapperMethod
的构造器被调用时会通过SqlSessionTemplate
里的映射关系拿到这些元数据信息并封装成SqlCommand
对象,在执行jdbc操作时会通过SqlCommand
来获取jdbc信息来执行后续逻辑,debug截图如下:
最终通过
SqlSessionTemplate
类来实现jdbc操作,debug图如下:
而
sqlSession.selectOne
方法会调用到SqlSessionTemplate
的内部类SqlSessionInterceptor.invoke(Object proxy, Method method, Object[] args)
方法里,主要逻辑如下:-
通过
SqlSessionFactory
从DataSource
连接池中获取sqlSession
,这里会先从一个ThreadLocal
中获取,因为开启了事务的话,sqlSession
会通过ThreadLocal
来传递,如果没有开启事务,则从连接池中获取新的Session -
通过反射来调用获取到的
sqlSession
对象(这里获取到的是DefaultSqlSession
)的selectList
方法 -
以dao的接口名+方法名为key获取之前解析到的元数据信息,包括对应的sql、返回类型等,debug截图如下:
-
通过四大组件之一的Executor的实现类
CachingExecutor
类(因为mybatis默认开启缓存,所以会使用这个实现类)来执行jdbc操作,该类封装了cache相关操作,先解析出该方法需要执行的sql,debug图如下:
这里会首先去查询是否开启了二级缓存(需要在mapper文件里家在<cache/>配置,二级缓存是namespace粒度的),如果开启了缓存,则会直接从缓存中查询,debug图如下:
然后会查询一级缓存(session粒度),如果没有命中缓存则继续后续操作,debug图如下:
-
通过四大组件之一的statementHandler的实现类
RoutingStatementHandler
类来执行CRUD操作,这个类主要是封装类,不提供具体的实现,只是根据Executor的类型,创建不同的类型StatementHandler,默认创建带有预编译功能的PreparedStatementHandler
类,debug图如下:
-
通过四大组件之一的ParameterHandler来拼接sql中的参数,debug图如下:
-
通过四大组件之一的ResultSetHandler来处理返回值,将数据库返回值绑定到对应的实体类,debug图如下:
-
处理缓存信息、释放资源等逻辑
-
MyBatis原理分析(通俗易懂)
2019-06-07 16:10:20MyBatis原理分析MyBatis工作流程简述原生MyBatis原理分析初始化工作解析配置文件配置类方式执行SQLSqlSession API方式接口方式 真正掌握一个框架源码分析是少不了的~ 在讲解整合Spring的原理之前理解原生的MyBatis... -
MyBatis原理总结
2021-12-26 19:15:36MyBatis原理总结 mybatis是一个持久层的框架,它让程序员将主要精力放在sql语句上。 底层原理: 1.mybatis是根据全局配置文件,sql映射文件,初始化出Configuration对象, 2.创建DefaultSqlSessionFactory会话... -
【mybatis原理工作原理】
2022-04-26 17:17:23文章目录mybatis原理: mybatis原理: mybatis的工作原理就是:先封装sql,接着调用jdbc操作数据库,最后把数据库返回的表结果封装成java类。 通过代码实现jdbc查询操作: mybatis-config.xml类 <?xml version=... -
Spring 整合 Mybatis 原理
2022-06-21 08:24:01在 Mybatis 中,我们可以使用一个接口去定义要执行 sql,简化代码如下:定义一个接口,@Select 表示要执行查询 sql 语句。 以下为执行 sql 代码: Mybatis的目的是:使得程序员能够以调用方法的方式执行某个指定的 ... -
MyBatis原理分析
2020-11-22 11:27:00MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis可以使用简单的 XML 或注解来配置和映射原生类型、接口和 ... -
MyBatis原理
2017-10-18 19:42:21mybatis是什么? mybatis是一个持久层框架,mybatis是一个不完全的ORM框架。sql语句需要程序员 自己去编写,但是mybatis也有映射(输入参数映射、输出结果映射)。 mybatis入门门槛不高,学习成本低,让程序员把... -
mybatis原理概述入门教程
2020-09-01 17:12:17主要介绍了在今天这篇博文中,我将要介绍一下mybatis的框架原理,以及mybatis的入门程序,实现用户的增删改查,她有什么优缺点以及mybatis和hibernate之间存在着怎么样的关系,大家这些问题一起通过本文学习吧 -
mybatis原理(含图)
2021-03-10 10:12:23上面中流程就是MyBatis内部核心流程,每一步流程的详细说明如下文所述: (1)读取MyBatis的配置文件。mybatis-config.xml为MyBatis的全局配置文件,用于配置数据库连接信息。 (2)加载映射文件。映射文件即SQL映射... -
SpringBoot整合MyBatis原理
2019-05-25 11:26:53SpringBoot整合MyBatis原理 一. 自动配置 SpringBoot提供了MyBatis的自动配置类MybatisAutoConfiguration,可以自动注册SqlSessionFactory、SqlSessionTemplate等组件,开发人员只需在配置文件中指定相关属性即可。 ... -
mybatis 原理
2018-06-10 08:49:34问题:mybatis 是基于mapper接口开发的,mapper接口是执行SQL语句的呢?mybatis 对mapper代码的包装主要包含了4个类。1.首先mapper需要将接口进行注册,并且需要获得mapper代理工厂(mapperregistry)mapperregistry类将... -
【源码系列】MyBatis原理源码
2022-03-23 15:49:49Mybatis的核心是Jdk的动态代理和各个组件,整个运行可以分为两大步骤: 1. 准备阶段 1. 解析配置文件获取全局Configuration对象,该过程中为每个Mapper接口创建MapperProxyFactory对象, 2. 有了Configuration对象... -
spring 整合 mybatis原理
2018-12-15 09:46:07那么让我在回到之前提到的那个问题,如果有成百上千个dao接口呢,那我们岂不是要配置添加成百上千个bean,当然不是这样,spring还为MyBatis添加了拓展的功能,可以通过扫描包目录的方式,添加dao,让我看看具体使用... -
springboot整合mybatis原理
2021-12-09 10:05:52pom ...org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.0</version> </dependency> 在mybatis-sp -
阿里P7最常问的一道面试题,Spring整合Mybatis原理
2020-10-24 15:07:58在介绍Spring整合Mybatis原理之前,我们得先来稍微介绍Mybatis的工作原理。 Mybatis的基本工作原理 在Mybatis中,我们可以使用一个接口去定义要执行sql,简化代码如下: 定义一个接口,@Select表示要执行查询sql语句... -
《深入理解mybatis原理》 MyBatis的架构设计以及实例分析
2014-11-04 16:44:53MyBatis是目前非常流行的ORM框架,它的功能很强大,然而其实现却比较简单、优雅。本文主要讲述MyBatis的架构设计思路,并且讨论MyBatis的几个核心部件,然后结合一个select查询实例,深入代码,来探究MyBatis的实现... -
深入浅出MyBatis技术原理与实战和SpringBoot实战第4版
2018-05-11 17:48:42深入浅出MyBatis技术原理与实战.pdf 和 SpringBoot实战第4版 -
【Spring】总结Spring整合Mybatis的底层原理实现步骤
2020-12-21 07:44:42(鲁班学院公开课笔记) Dao:数据访问层,提供让Service层调用的接口,更大的时候,Dao层可以是一个项目。 sqlSession.getMapper();使用的是JDK的动态代理 使用时 自定注入的条件:要被Spring管理。... -
spring源码学习之整合Mybatis原理分析
2018-11-11 01:37:03本文主要解析spring是如何与mybatis进行整合,整合的过程中需要哪些组件的支持。以前面提到过的配置例子《spring源码学习之aop事物标签解析》 整合的过程中需要使用以下这个依赖包: <!-- mybatis-spring ... -
深入浅出Mybatis原理与实战
2018-12-08 09:47:10深入浅出Mybatis原理与实战 -
使用ImportBeanDefinitionRegistrar、JDK代理、FactoryBean模拟mybatis原理.zip
2021-05-13 19:16:10使用ImportBeanDefinitionRegistrar、JDK代理、FactoryBean模拟mybatis原理 -
sharding-jdbc+mybatis 原理
2019-09-24 19:50:30项目中使用 1). 项目中使用mybatis 2). 项目中使用sharding-jdbc mybatis+sharding-jdbc结合的入口 -
Mybatis原理理解
2021-02-06 14:11:46MyBatis是一个简单,小巧但功能非常强大的ORM开源框架,它的功能强大也体现在它的缓存机制上。MyBatis提供了一级缓存、二级缓存 这两个缓存机制,能够很好地处理和维护缓存,以提高系统的性能。本文主要讲述MyBatis...