Mybatis MappedStatement类核心原理详解
氵奄不死的鱼 人气:0MappedStatement
MappedStatement 类是 Mybatis 框架的核心类之一,它存储了一个 sql 对应的所有信息
Mybatis 通过解析 XML 和 mapper 接口上的注解,生成 sql 对应的 MappedStatement 实例,并放入 SqlSessionTemplate 中 configuration 类属性中
正真执行 mapper 接口中的方法时,会从 configuration 中找到对应的 mappedStatement,然后进行后续的操作
MyBatis通过MappedStatement描述<select|update|insert|delete>或者@Select、@Update等注解配置的SQL信息。在介绍MappedStatement组件之前,我们先来了解一下MyBatis中SQL Mapper的配置。不同类型的SQL语句需要使用对应的XML标签进行配置。这些标签提供了很多属性,用来控制每条SQL语句的执行行为。下面是标签中的所有属性:
<select
id="getUserById"
parameterType="int"
parameterMap="deprecated"
resultType="hashmap"
resultMap="userResultMap"
flushCache="false"
useCache="true"
timeout="10000"
fetchSize="256"
statementType="PREPARED"
resultSetType="FORWARD ONLY">
public final class MappedStatement { private String id; private Integer fetchSize; private Integer timeout; private StatementType statementType; private ResultSetType resultSetType; private SqlSource sqlSource; private Cache cache; private ParameterMap parameterMap; private List<ResultMap> resultMaps; private boolean flushCacheRequired; private boolean useCache; private boolean resultOrdered; private SqlCommandType sqlCommandType; private KeyGenerator keyGenerator; private String[] keyProperties; private String[] keyColumns; private boolean hasNestedResultMaps; private String databaseId; private Log statementLog; private LanguageDriver lang; private String[] resultSets;
对应字段含义
id:在命名空间中唯一的标识符,可以被用来引用这条配置信息。
parameterType:用于指定这条语句的参数类的完全限定名或别名。这个属性是可选的,MyBatis能够根据Mapper接口方法中的参数类型推断出传入语句的类型。
parameterMap:引用通过标签定义的参数映射,该属性已经废弃。
resultType:从这条语句中返回的期望类型的类的完全限定名或别名。注意,如果返回结果是集合类型,则resultType属性应该指定集合中可以包含的类型,而不是集合本身。
resultMap:用于引用通过标签配置的实体属性与数据库字段之间建立的结果集的映射(注意:resultMap和resultType属性不能同时使用)。
flushCache:用于控制是否刷新缓存。如果将其设置为true,则任何时候只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值为false。
useCache:是否使用二级缓存。如果将其设置为true,则会导致本条语句的结果被缓存在MyBatis的二级缓存中,对应标签,该属性的默认值为true。
timeout:驱动程序等待数据库返回请求结果的秒数,超时将会抛出异常。
fetchSize:用于设置JDBC中Statement对象的fetchSize属性,该属性用于指定SQL执行后返回的最大行数。
statementType:参数可选值为STATEMENT、PREPARED或CALLABLE,这会让MyBatis分别使用Statement、PreparedStatement或CallableStatement与数据库交互,默认值为PREPARED。
resultSetType:参数可选值为FORWARD_ONLY、SCROLL_SENSITIVE或SCROLL_INSENSITIVE,用于设置ResultSet对象的特征,具体可参考第2章JDBC规范的相关内容。默认未设置,由JDBC驱动决定。
databaseId:如果配置了databaseIdProvider,MyBatis会加载所有不带databaseId或匹配当前databaseId的语句。
resultOrdered:这个设置仅针对嵌套结果select语句适用,如果为true,就是假定嵌套结果包含在一起或分组在一起,这样的话,当返回一个主结果行的时候,就不会发生对前面结果集引用的情况。这就使得在获取嵌套结果集的时候不至于导致内存不够用,默认值为false。
resultSets:这个设置仅对多结果集的情况适用,它将列出语句执行后返回的结果集并每个结果集给一个名称,名称使用逗号分隔。
lang:该属性用于指定LanguageDriver实现,MyBatis中的LanguageDriver用于解析<select|update|insert|delete>标签中的SQL语句,生成SqlSource对象。
介绍jdbc的几个相关属性
resultSetType
在创建PreparedStatement时,resultSetType参数设置的是TYPE_SCROLL_INSENSITIVE或TYPE_SCROLL_SENSITIVE,
这两个参数的共同特点是允许结果集(ResultSet)的游标可以上下移动。而默认的TYPE_FORWARD_ONLY参数只允许结果集的游标向下移动。
如果PreparedStatement对象初始化时resultSetType参数设置为TYPE_FORWARD_ONLY,在从ResultSet(结果集)中读取记录的时,对于访问过的记录就自动释放了内存。而设置为TYPE_SCROLL_INSENSITIVE或TYPE_SCROLL_SENSITIVE时为了保证能游标能向上移动到任意位置,已经访问过的所有都保留在内存中不能释放。所以大量数据加载的时候,就OOM了。
statement = conn.prepareStatement(querySql,ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
resultSetType是设置ResultSet对象的类型标示可滚动,或者是不可滚动。取值如下:
默认只能向前滚动。
fetchSize
默认情况下pgjdbc driver会一次性拉取所有结果集,也就是在executeQuery的时候。对于大数据量的查询来说,非常容易造成OOM。这种场景就需要设置fetchSize,执行query的时候先返回第一批数据,之后next完一批数据之后再去拉取下一批。
场景与方案
场景:java端从数据库读取100W数据进行后台业务处理。
常规实现1:分页读取出来。缺点:需要排序后分页读取,性能低下。
常规实现2:一次性读取出来。缺点:需要很大内存,一般计算机不行。
非常规实现:建立长连接,利用服务端游标,一条一条流式返回给java端。
非常规实现优化:jdbc中有个重要的参数fetchSize(它对业务实现无影响,即不会限制读取条数等),优化后可显著提升性能。
public static void getAll(int fetchSize) { try { long beginTime=System.currentTimeMillis(); Connection connection = DriverManager.getConnection(MYSQL_URL); connection.setAutoCommit(false); //为了设置fetchSize,必须设置为false String sql = "select * from test"; PreparedStatement psst = connection.prepareStatement(sql,ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); psst.setFetchSize(fetchSize); ResultSet rs = psst.executeQuery(); int totalCount=0; while (rs.next()) { totalCount++; } rs.close(); psst.close(); connection.close(); long endTime=System.currentTimeMillis(); System.out.println("totalCount:"+totalCount+";fetchSize:"+fetchSize+";耗时:"+(endTime-beginTime)+"ms"); } catch (SQLException e) { e.printStackTrace(); } }
原理分析
1、先在服务端执行查询后将数据缓存在服务端。(耗时相对较长)
2、java端获取数据时,利用服务端游标进行指针跳动,如果fetchSize为1000,则一次性跳动1000条,返回给java端缓存起来。(耗时较短,跳动次数为N/1000)
3、在调用next函数时,优先从缓存中取数,其次执行2过程。(内存读取,耗时可忽略)
MappedStatement是怎么来的
还是以XML配置方式为例进行分析,简单说下源码查找的过程。Mapper对应的SQL语句定义在xml文件中,顺着源码会发现完成xml解析工作的是XMLMapperBuilder,其中对xml中“select|insert|update|delete”类型元素的解析方法为buildStatementFromContext;buildStatementFromContext使用了XMLStatementBuilder类对statement进行解析,并最终创建了MappedStatement。
所以,XMLStatementBuilder#parseStatementNode方法就是我们分析的重点。但是,在此之前需要有一点准备工作要做。由于MappedStatement最终是由MapperBuilderAssistant构建的,它其中存储了一些Mapper级别的共享信息并应用到MappedStatement中。所以,先来简单了解下它的由来:
public class XMLMapperBuilder extends BaseBuilder { private final XPathParser parser; private final MapperBuilderAssistant builderAssistant; //省略部分字段和方法 public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) { this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()), configuration, resource, sqlFragments); } private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) { super(configuration); this.builderAssistant = new MapperBuilderAssistant(configuration, resource); this.parser = parser; this.sqlFragments = sqlFragments; this.resource = resource; } //省略部分字段和方法 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"); } builderAssistant.setCurrentNamespace(namespace); //省略部分代码 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#parseStatementNode的分析了。为了节省篇幅,我直接通过代码注释的方式进行说明了,部分我认为不关键或不常用的内容没有多说。
/** * parseStatementNode方法是对select、insert、update、delete这四类元素进行解析,大体分为三个过程: * 1、解析节点属性:如我们最常用的id、resultMap等; * 2、解析节点内的sql语句:首先把sql语句中包含的<include></include>等标签转为实际的sql语句,然后执行静态或动态节点处理; * 3、根据以上解析到的内容,使用builderAssistant创建MappedStatement,并加入Configuration中。 * <p> * 以上过程中最关键的是第二步,它会根据实际使用的标签,把sql片段转为不同的SqlNode,以链表方式存储到SqlSource中。 */ public void parseStatementNode() { //获取标签的id属性,如selectById,对应Mapper接口中的方法名称 String id = context.getStringAttribute("id"); //获取databaseId属性,我们一般都没有写。 String databaseId = context.getStringAttribute("databaseId"); /** * 这段代码虽然不起眼,但是一定要进去看一下:其内部完成了对id的再次赋值, * 处理的方式是:id=namespace+"."+id,也就是当前Mapper的完全限定名+"."+id, * 比如我们之前例子中的com.raysonxin.dao.CompanyDao.selectById * 这也是Mapper接口中不能存在重载方法的根本原因。 * */ if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { return; } /** * 下面这块代码会依次获取fetchSize、timeout、resultMap等属性, * 需要注意的是,有些属性虽然我们没有设置,但是mybatis会设置默认值, * 具体可以查看mybatis的官方说明。 */ 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"); String lang = context.getStringAttribute("lang"); //默认值:XMLLanguageDriver LanguageDriver langDriver = getLanguageDriver(lang); Class<?> resultTypeClass = resolveClass(resultType); String resultSetType = context.getStringAttribute("resultSetType"); //默认值:PREPARED StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); //默认值:DEFAULT 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 Fragments before parsing /** * 英文注释也说了,在sql解析前处理 include 标签,比如说,我们include了BaseColumns, * 它会把这个include标签替换为BaseColumns内的sql内容 * */ XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); includeParser.applyIncludes(context.getNode()); // Parse selectKey after includes and remove them. //处理selectKey,主要针对不同的数据库引擎做处理 processSelectKeyNodes(id, parameterTypeClass, langDriver); // Parse the SQL (pre: <selectKey> and <include> were parsed and removed) /** * 到了关键步骤了:就是通过这句代码完成了从xml标签到SqlSource的转换, * SqlSource是一个接口,这里返回的可能是DynamicSqlSource、也可能是RawSqlSource, * 取决于xml标签中是否包含动态元素,比如 <if test=""></if> * */ SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); String resultSets = context.getStringAttribute("resultSets"); //下面这些是针对selectKey、KeyGenerator等进行处理,暂时跳过了。 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创建MappedStatement, * 并保存到Configuration#mappedStatements。 * */ builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); }
我们在xml中定义的select等语句就是通过这个parseStatementNode方法解析为MappedStatement的,整体来看比较容易理解,核心就是SqlSource的创建过程,
SqlSource是什么
SqlSource是整个MappedStatement的核心,MappedStatement其他一大堆字段都是为了准确的执行它而定义的。SqlSource是个半成品的sql语句,因为对于其中的动态标签还没静态化,其中的参数也未赋值。正是如此,才为我们后续的调用执行提供了基础,接下来重点看看SqlSource的构建过程。为了先从整体上了解,我画了一个时序图来描述SqlSource的解析、创建过程。
sqlsource是mapped statement对象中的一个属性, 是一对一关系, 在创建mapped statement对象时创建,
sqlsource的主要作用就是创建一个sql语句
SqlSource主要有四种实现类, 其主要作用的就是以下三种 :
RawSqlSource : 存储的是只有“#{}”或者没有标签的纯文本sql信息
DynamicSqlSource : 存储的是写有“${}”或者具有动态sql标签的sql信息
StaticSqlSource : 是DynamicSqlSource和RawSqlSource解析为BoundSql的一个中间环节
BoundSql
private final String sql; private final List<ParameterMapping> parameterMappings; private final Object parameterObject;
oundSql类的作用就是生成我们最终执行的sql语句, 里面包含一些属性
- sql : 执行的sql语句 (包含 ?)
- parameterMappings : 映射关系
- parameterObject : 参数值
DynamicSqlSource
public class DynamicSqlSource implements SqlSource { private final Configuration configuration; private final SqlNode rootSqlNode; public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) { this.configuration = configuration; this.rootSqlNode = rootSqlNode; } @Override public BoundSql getBoundSql(Object parameterObject) { DynamicContext context = new DynamicContext(configuration, parameterObject); rootSqlNode.apply(context); SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); BoundSql boundSql = sqlSource.getBoundSql(parameterObject); context.getBindings().forEach(boundSql::setAdditionalParameter); return boundSql; } }
- field
configuration : 上下文, 里面包含了各种参数, 方便使用
- method
getBoundSql :
构建DynamicContext对象, 里边包含一个StringJoiner属性, 遍历sqlNode节点是用来拼接sql
将拼接的sql传入, 将#{}替换成? , 并创建一个StaticSqlSource对象
通过StaticSqlSource对象获取BoundSql对象
RawSqlSource
public class RawSqlSource implements SqlSource { private final SqlSource sqlSource; public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) { this(configuration, getSql(configuration, rootSqlNode), parameterType); } public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) { SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); Class<?> clazz = parameterType == null ? Object.class : parameterType; sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>()); } private static String getSql(Configuration configuration, SqlNode rootSqlNode) { DynamicContext context = new DynamicContext(configuration, null); rootSqlNode.apply(context); return context.getSql(); } @Override public BoundSql getBoundSql(Object parameterObject) { return sqlSource.getBoundSql(parameterObject); } }
- field
sqlSource : 此属性的类型为StaticSqlSource
- method
getBoundSql : 获取BoundSql对象
StaticSqlSource
只包含一个创建BoundSql对象的方法
public class StaticSqlSource implements SqlSource { private final String sql; private final List<ParameterMapping> parameterMappings; private final Configuration configuration; public StaticSqlSource(Configuration configuration, String sql) { this(configuration, sql, null); } public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) { this.sql = sql; this.parameterMappings = parameterMappings; this.configuration = configuration; } @Override public BoundSql getBoundSql(Object parameterObject) { return new BoundSql(configuration, sql, parameterMappings, parameterObject); }
加载全部内容