-
什么是mybatis?
- Mybatis是一款SQL映射框架,是对JDBC的封装实现。可以将SQL写在xml文件或通过注解方式来完成数据库结果集对象到java对象的转换,所以是半自动化的ORM框架。
-
mybatis中的一级缓存与二级缓存?
-
mybatis的二级缓存实现了SqlSession之间的数据共享,同时粒度到达namespace级别,也更加可控;但在生成环境不会采用缓存机制(最大的原因就是数据一致性的问题,只当做一个ORM框架进行使用)
-
mybatis提供了大致三类的配置
- setting配置 cacheEnabled与localCacheScope属性配置,cacheEnabled缓存生效的总开关;localCacheScope默认session级别(一级缓存),会缓存一个会话中执行的所有查询;STATEMENT级别会清空本地缓存;
<settings> <setting name="cacheEnabled" value="true"/> <setting name="localCacheScope" value="SESSION"/> </settings> - MappedStatement配置即XML映射文件select标签中的flushCache和useCache属性 flushCache会清空一级缓存和二级缓存,useCache会将本条执行语句结果缓存进二级缓存中
- cache标签或者cache-ref标签,对给定命名空间启用二级缓存配置
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
- setting配置 cacheEnabled与localCacheScope属性配置,cacheEnabled缓存生效的总开关;localCacheScope默认session级别(一级缓存),会缓存一个会话中执行的所有查询;STATEMENT级别会清空本地缓存;
-
一级缓存
- flushCache开关开启时,或清空一级缓存或者当缓存范围配置是STATEMEN时,会清空本地缓存(一级缓存PerpetualCache类实际是HashMap)详见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 { // 缓存中获取不到,则调用queryFromDatabase()方法从数据库中查询 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; } -
二级缓存
-
cacheEnabled开启时,创建Executor实例时,会生成该实例的装饰者对象CachingExecutor,当设置了cache标签时,就会开启二级缓存,所以二级缓存是基于namespace级别的
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { // 获取MappedStatement对象中维护的二级缓存对象 Cache cache = ms.getCache(); if (cache != null) { // 判断是否需要刷新二级缓存 flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); // 从MappedStatement对象对应的二级缓存中获取数据 @SuppressWarnings("unchecked") List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { // 如果缓存数据不存在,则从数据库中查询数据 list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // 將数据存放到MappedStatement对象对应的二级缓存中 tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
-
-
-
mybatis中的主键策略是怎么样的?
-
mybatis中提供了三种KeyGenerator接口的实现类分别是:Jdbc3KeyGenerator,SelectKeyGenerator,NoKeyGenerator
- Jdbc3KeyGenerator 主要用于数据库自增主键
- NoKeyGenerator 默认空实现,不对主键单独处理
- SelectKeyGenerator 主要解决插入数据不支持主键自动生成的问题
-
KeyGenerator中有两个方法,在创建StatementHandler实例时,在其父类的构造方法中会获取当前MappedStatement当中的主键策略对象KeyGenerator,并执行processBefore方法,完成主键的生成;当Statement执行完数据库操作时StatementHandler会获取当前MappedStatement当中的主键策略对象KeyGenerator,并执行processAfter方法;
public class NoKeyGenerator implements KeyGenerator { /** * A shared instance. * @since 3.4.3 */ public static final NoKeyGenerator INSTANCE = new NoKeyGenerator(); //processBefore在操作数据库之前时会调用该方法 @Override public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) { // Do Nothing } //在数据执行操作后,会调用该方法 @Override public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) { // Do Nothing } } -
如何实现自定义主键生成策略
-
通过实现Mybatis中Interceptor接口实现自定义拦截器插件对Executor.class的update方法进行拦截,对MappedStatement对象中对参数列表进行处理;
-
通过对Executor.class拦截实现
/** * 自定义主键生成器 */ @Intercepts({@Signature(type = Executor.class,method = "update",args = {MappedStatement.class, Object.class})}) //通过拦截Executor.update方法 @Component public class MyKeyGeneratorInterceptor implements Interceptor { //自定义主键生成策略 private IDStrategy idStrategy = new IDStrategy(); @Override public Object intercept(Invocation invocation) throws Throwable { Object[] args = invocation.getArgs(); if (args.length == 2 && args[1] != null) { //此处判断需要注入的主键的属性即可 if(args[1] instanceof Bank){ Bank bank = (Bank) args[1]; bank.setCoreMerNo(idStrategy.getMyId()); } } return invocation.proceed(); } @Override public Object plugin(Object target) { // 调用Plugin类的wrap()方法返回一个动态代理对象 return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } }
-
-
-
mybatis中动态SQL的原理是什么,以及#{}与${}的区别是什么?
-
#实现了预编译,会先把#{变量}编译成?然后在执行时取值替换,可以防止sql注入;$是直接进行字符串替换
-
${}主要应用于传入参数时sql片段的场景下,进行字符串替换,完成sql拼接;但不建议使用,sql注入问题存在风险;
-
#{}与${}符号的解析过程
- XMLStatementBuilder解析<select|update|...>标签创建MappedStatement对象时通过LanguageDriver进行sql解析并生成SqlSource对象;
public class XMLLanguageDriver implements LanguageDriver { @Override public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql); } @Override public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) { // 该方法用于解析XML文件中配置的SQL信息 // 创建XMLScriptBuilder对象 XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType); // 调用 XMLScriptBuilder对象parseScriptNode()方法解析SQL资源 return builder.parseScriptNode(); } @Override public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) { // 该方法用于解析Java注解中配置的SQL信息 // 字符串以<script>标签开头,则以XML方式解析 if (script.startsWith("<script>")) { XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver()); return createSqlSource(configuration, parser.evalNode("/script"), parameterType); } else { // 解析SQL配置中的全局变量 script = PropertyParser.parse(script, configuration.getVariables()); TextSqlNode textSqlNode = new TextSqlNode(script); // 如果SQL中是否仍包含${}参数占位符,则返回DynamicSqlSource实例,否则返回RawSqlSource if (textSqlNode.isDynamic()) { return new DynamicSqlSource(configuration, textSqlNode); } else { return new RawSqlSource(configuration, script, parameterType); } } } }-
通过XMLScriptBuilder来解析xml中的sql,先对XNode对象转换为SqlNode,如果存在${}或存在 if,where,trim等标签时则该Sql就为动态sql,如果子元素为<if、<where等标签,则使用对应的NodeHandler处理。
-
通过是否包含动态SQL,如果是创建DynamicSqlSource对象,否则创建RawSqlSource对象;
public SqlSource parseScriptNode() { // 调用parseDynamicTags()方法將SQL配置转换为SqlNode对象 MixedSqlNode rootSqlNode = parseDynamicTags(context); SqlSource sqlSource = null; // 判断Mapper SQL配置中是否包含动态SQL元素,如果是创建DynamicSqlSource对象,否则创建RawSqlSource对象 if (isDynamic) { sqlSource = new DynamicSqlSource(configuration, rootSqlNode); } else { sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType); } return sqlSource; } protected MixedSqlNode parseDynamicTags(XNode node) { List<SqlNode> contents = new ArrayList<SqlNode>(); NodeList children = node.getNode().getChildNodes(); // 对XML子元素进行遍历 for (int i = 0; i < children.getLength(); i++) { XNode child = node.newXNode(children.item(i)); // 如果子元素为SQL文本内容,则使用TextSqlNode描述该节点 if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) { String data = child.getStringBody(""); TextSqlNode textSqlNode = new TextSqlNode(data); // 判断SQL文本中包含${}参数占位符,则为动态SQL if (textSqlNode.isDynamic()) { contents.add(textSqlNode); isDynamic = true; } else { // 如果SQL文本中不包含${}参数占位符,则不是动态SQL contents.add(new StaticTextSqlNode(data)); } } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // 如果子元素为<if>、<where>等标签,则使用对应的NodeHandler处理 String nodeName = child.getNode().getNodeName(); //nodeHandlerMap mybatis中动态sql标签的map集合 NodeHandler handler = nodeHandlerMap.get(nodeName); if (handler == null) { throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement."); } handler.handleNode(child, contents); isDynamic = true; } } return new MixedSqlNode(contents); } -
通过SqlSource中的getBoundSql方法,调用StaticSqlSource对象的getBoundSql()方法,获得BoundSql实例;BoundSql是对Executor组件执行sql的再封装,里面存在解析后的sql语句以及Mapper参数映射信息,参数对象信息;
-
-
mybatis是怎么进行分页的
-
分页查询一般分两种,第一种数据库的分页语句进行物理分页,另外就是将数据全部查询出来在应用程序中进行内存进行;
-
mybatis可以通过插件的方式,修改sql执行语句从而进行物理分页;我们知道Executor组件是通过StatementHandler来进行JDBC的交互的,并且通过StatementHandler.prepare方法生成jdbc的Statement对象的,所以我们只需要拦截StatementHandler的prepare方法进行拦截后,获取到BoundSql对象后得到已经解析设置值后的sql后拼接分页sql片段完成自动分页原理;
/** * 自定义分页插件-mybatis实际是通过RoutingStatementHandler对象(策略模式) */ @Intercepts({@Signature(type = StatementHandler.class,method = "prepare",args = {PreparedStatement.class})}) //ParameterHandler.setParameters方法 @Component public class MyPageInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget(); BoundSql boundSql = handler.getBoundSql(); Object pageObject = boundSql.getParameterObject(); if (pageObject instanceof Bank) { //此处可以自定义分页对象即可 String sql = boundSql.getSql() + "limit 1,10"; //通过反射设置对应BoundSql属性 sql值 } return invocation.proceed(); } @Override public Object plugin(Object target) { // 调用Plugin类的wrap()方法返回一个动态代理对象 return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } }
-
-
mybatis是怎么实现接口mapper的绑定?
-
mybatis实现接口绑定有两种实现方式;基于Xml文件方式和注解方式
-
基于XML实现接口绑定原理
- 在XMLConfigBUilder解析mybatis的主配置文件时,会对<Mappers标签下的mapper进行解析,通过包名(package标签),mapper标签下的resource网络文件或class属性指定的接口路径进行扫描;并生成MapperProxyFctory对象存到Configretion容器中MapperRegistry的Map<Class,MapperProxyFactory> knownMappers里面;
private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { // 通过<package>标签指定包名 if ("package".equals(child.getName())) { String mapperPackage = child.getStringAttribute("name"); configuration.addMappers(mapperPackage); } else { String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); // 通过resource属性指定XML文件路径 if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); InputStream inputStream = Resources.getResourceAsStream(resource); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); mapperParser.parse(); } else if (resource == null && url != null && mapperClass == null) { // 通过url属性指定XML文件路径 ErrorContext.instance().resource(url); InputStream inputStream = Resources.getUrlAsStream(url); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); mapperParser.parse(); } else if (resource == null && url == null && mapperClass != null) { // 通过class属性指定接口的完全限定名 Class<?> mapperInterface = Resources.classForName(mapperClass); configuration.addMapper(mapperInterface); } else { throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); } } } } }- 调用XMLMapperBuilder.parse方法对XML文件当中的sql进行解析,解析顺序如下注释;最后XMLStatementBuilder.parseStatementNode完成select|update|insert|delete标签的解析,并生成MappedStatement对象放入configration容器的Map<<String,MappedStatement> mappedStatement map当中,key为namespace + 方法id;这也是为什么Mapper不能存在相同方法名称
private void configurationElement(XNode context) { try { // 获取命名空间 String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty"); } // 设置当前正在解析的Mapper配置的命名空间 builderAssistant.setCurrentNamespace(namespace); // 解析<cache-ref>标签 cacheRefElement(context.evalNode("cache-ref")); // 解析<cache>标签 cacheElement(context.evalNode("cache")); // 解析所有的<parameterMap>标签 parameterMapElement(context.evalNodes("/mapper/parameterMap")); // 解析所有的<resultMap>标签 resultMapElements(context.evalNodes("/mapper/resultMap")); // 解析所有的<sql>标签 sqlElement(context.evalNodes("/mapper/sql")); // 解析所有的<select|insert|update|delete>标签 buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e); } } //通过XMLStatementBuilder对象,对<select|update|insert|delete>标签进行解析 public void parseStatementNode() { String id = context.getStringAttribute("id"); String databaseId = context.getStringAttribute("databaseId"); if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { return; } // 解析<select|update|delete|insert>标签属性 Integer fetchSize = context.getIntAttribute("fetchSize"); Integer timeout = context.getIntAttribute("timeout"); String parameterMap = context.getStringAttribute("parameterMap"); String parameterType = context.getStringAttribute("parameterType"); Class<?> parameterTypeClass = resolveClass(parameterType); String resultMap = context.getStringAttribute("resultMap"); String resultType = context.getStringAttribute("resultType"); // 获取LanguageDriver对象 String lang = context.getStringAttribute("lang"); LanguageDriver langDriver = getLanguageDriver(lang); // 获取Mapper返回结果类型Class对象 Class<?> resultTypeClass = resolveClass(resultType); String resultSetType = context.getStringAttribute("resultSetType"); // 默认Statement类型为PREPARED StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); String nodeName = context.getNode().getNodeName(); SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); boolean useCache = context.getBooleanAttribute("useCache", isSelect); boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); // 將<include>标签内容,替换为<sql>标签定义的SQL片段 XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); includeParser.applyIncludes(context.getNode()); // 解析<selectKey>标签 processSelectKeyNodes(id, parameterTypeClass, langDriver); // 通过LanguageDriver解析SQL内容,生成SqlSource对象 SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); String resultSets = context.getStringAttribute("resultSets"); String keyProperty = context.getStringAttribute("keyProperty"); String keyColumn = context.getStringAttribute("keyColumn"); KeyGenerator keyGenerator; String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX; keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true); // 获取主键生成策略 if (configuration.hasKeyGenerator(keyStatementId)) { keyGenerator = configuration.getKeyGenerator(keyStatementId); } else { keyGenerator = context.getBooleanAttribute("useGeneratedKeys", configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; } builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); }-
当利用SqlSession获取某个Mapper对象时是通过MapperProxyFactory.newInstance生成MapperProxy动态代理对象(JDK的动态代理);所以当执行mapper方法时会调用MapperProxy.invoke方法去执行(实际通过MapperMethod.excute方法通过方法的全路径名称找到MappedStatement判断SQL执行类型后,去执行对应SqlSession的api方法,最后通过Execto执行器传入对应的MappedStatement完成一直sql执行)
public Object execute(SqlSession sqlSession, Object[] args) { Object result; // 其中command为MapperMethod构造是创建的SqlCommand对象 // 获取SQL语句类型 switch (command.getType()) { case INSERT: { // 获取参数信息 Object param = method.convertArgsToSqlCommandParam(args); // 调用SqlSession的insert()方法,然后调用rowCountResult()方法统计行数 result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); // 调用SqlSession对象的update()方法 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: if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { 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); result = sqlSession.selectOne(command.getName(), param); if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) { result = OptionalUtil.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; }
-
基于注解实现
- 当将Mapper接口Class对象存到存到Configretion容器中MapperRegistry的Map<Class,MapperProxyFactory> knownMappers里面是(addMapper方法时)会调用MapperAnnotationBuildr.parse方法对接口方法上的注解进行解析,最终为每个存在注解的方法生成MappedStament对象,存入configration容器的Map<<String,MappedStatement> mappedStatement map当中
-
结论:mybatis将mapper接口通过MapperProxyFactoy动态代理生成MapperProxy代理对象,同时将对应的SQL文件解析为MappedStatement对象存放着Configration容器中的map里,最有通过类的全路径名+方法名完成mapper接口方法的绑定,所以namespace + sql_id == class报名+方法名称实现sql与方法的绑定;
-
-
如何执行批量插入?
- 使用mybatis执行插入时可以有三种方式,1.单条插入循环执行;2.采用foreach标签构建批量插入语句;3.采用mybatis批处理模式进行插入;
-
单条插入循环执行
- 采用单条插入语句,在程序中循环执行插入
-
采用foreach标签构建批量插入语句
- 当Executor执行器为Simple(默认)时,会为每一个语句创建一个PreparedStatement对象;
- 且当数据量大时,解析后的sql语句会特别大会导致一次性插入的数据包特别大(超过max_allowed_packet)时会报错;
- 并且无法返回自动生成的主键
-
mybatis批处理模式进行插入
- 将Executor执行器设置为BATCH模式,在同一个SqlSession中会对Statement对象进行缓存,最终调用JDBC中的批处理模式addBatch()方法,减少了网络IO请求损耗。
-
mybatis是怎么整合spring?
-
mybati是怎么将Mapper的动态代理对象注册到Spring容器中的?
- 在日常开发中,Mapper是作为一个单例的bean注册进spring容器中的;mybatis提供了插件包完成了与spring框架的整合mybatis-spring;
- mybatis是通过实现自己的MapperFactoryBean实现FactoryBean接口完成mapper注入的,然后通过扫描ClassPathMapperScanner扩展了spring的ClassPathBeanDefinitionScanner扫描器,完成了包扫描,并将Mapper代理对象MapperProxy通过注册BeanDefinition对象完成了容器注入;------ 见后续spring学习
-
mybatis整合spring是怎么实现事务管理的?
- spring提供了两种事务管理机制;1声明式事务管理(主要通过aop实现一个切面完成)和编程式事务管理。
- mybatis中可以通过Transaction组件完成获取JDBC的Connection对象和对事务进行提交或者回滚操作;Mybatis中有两个Transaction的实现,分别是JdbcTransaction和ManagedTransaction;
- JdbcTransaction事务管理器通过JDBC的方式进行简单的事务提交和回滚并不处理异常,所以需要程序自己手动处理这些异常;
- ManagedTransaction管理器表明mybatis自己不进行事务管理,事务管理交由其他框架进行处理;
- mybatis整合spring事务时,最主要的时怎么保证操作数据库的Connection和事务提交或回滚的是同一个;而在mybatis-spring插件包中时,spring事务管理器中,Connection对象的获取和释放都是通过DataSourceUtils工具类获取Connection对象,所以可以通过SpringManagedTransaction来整合spring的事务管理;------ 见后续spring学习
-