MyBatis源码解析

一、Mybatis架构:

 

1、接口层

接口层主要定义的是与数据库进行交互的方式。在Mybatis中,交互分为两种方式。

(1) Mybatis提供的API

使用Mybatis提供的API进行操作,通过获取SqlSession对象,然后根据Statement Id 和参数来操作数据库。

String statement = "com.viewscenes.netsupervisor.dao.UserMapper.getUserList";

List<User> result = sqlsession.selectList(statement);

(2) 使用Mapper接口

事实上这个才是经常使用的方式,面向接口编程嘛。每一个Mapper接口中的方法对应着mapper.xml文件中的一个select/insert/update/delete节点。节点中的ID就是接口中的方法名,在使用的时候直接调用接口方法即可。不过,值得注意的是,它最终执行的还是sqlSession.select()、sqlSession.delete()。

 

2、数据处理层

这是Mybatis的核心。它负责参数映射和动态SQL生成,生成之后Mybatis执行SQL语句,并将返回的结果映射成自定义的类型。关于参数映射和结果集转换,主要是靠typeHandlers。为便于理解,我们大概来看几个类型处理器。

类型处理器

Java类型

JDBC类型

StringTypeHandler

java.lang.String

CHAR, VARCHAR

DateTypeHandler

java.util.Date

TIMESTAMP

BooleanTypeHandler

java.lang.Boolean, boolean

数据库兼容的 BOOLEAN

IntegerTypeHandler

java.lang.Integer, int

数据库兼容的 NUMERIC 或 INTEGER

 

3、框架支撑层

  • 事务管理

对于ORM框架而言,事务管理是必不可少的一部分。不过,一般情况下,Mybatis都是和Spring搭配使用的,更多的是用Spring来接管事务管理。

  • 连接池

我们不能每次在执行SQL的时候才去创建数据库的连接。因为创建连接是一个相对比较耗时的操作,通常做法是用一个列表保存提前创建好的N个连接,用到的时候去拿,用完再还回去。关于数据库连接池,业界有很多开源实现。比如C3P0、DBCP、Tomcat Jdbc Pool、BoneCP、Druid等。

  • 缓存(一般不直接使用Mybatis的缓存,而是使用第三方的缓存)

为了提高数据利用率和减小服务器和数据库的压力,Mybatis 会对于一些查询提供会话级别的数据缓存,会将对某一次查询,放置到SqlSession 中,在允许的时间间隔内,对于完全相同的查询,MyBatis 会直接将缓存结果返回给用户,而不用再到数据库中查找。

一级缓存是SqlSession级别的缓存,在同一个sqlSession中两次执行相同的sql语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率,Mybtais默认开启一级缓存。

二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession去操作数据库得到数据会存在二级缓存区域,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。要开启二级缓存,需要在你的 SQL 映射文件中添加一行:<cache/>。它会将所有的select语句缓存,在执行insert,update 和 delete 语句时会刷新缓存,缓存根据LRU算法来回收。

 

4、引导层

引导层是配置和启动MyBatis 配置信息的方式。MyBatis 提供两种方式来引导MyBatis :基于XML配置文件的方式和基于Java API 的方式。

SQL配置方式

大部分时候我们都是通过XML方式来配置SQL,不过Mybatis也支持通过注解来配置,就像下面这样( 不过,不推荐这种方式)。

@Select({"<script>", "select * from user"

        "</script>"})

List<ConsultContent> getUserList();

 

主要构件

  • SqlSession

    作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能。

  • Executor

    MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护。

  • StatementHandler

    封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。

  • ParameterHandler

    负责对用户传递的参数转换成JDBC Statement 所需要的参数。

  • ResultSetHandler

    负责将JDBC返回的ResultSet结果集对象转换成List类型的集合。

  • TypeHandler

    负责java数据类型和jdbc数据类型之间的映射和转换。

  • MappedStatement

    MappedStatement维护了一条<select|update|delete|insert>节点的封装。

  • SqlSource

    负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回。

  • BoundSql

    表示动态生成的SQL语句以及相应的参数信息。

  • Configuration

    MyBatis所有的配置信息都维持在Configuration对象之中。

 


 

 

二、XML的解析

 

1.初始化

在使用时,我们以 SqlSessionFactoryBuilder 去创建 SqlSessionFactory:

String resource = "org/mybatis/example/mybatis-config.xml";

InputStream inputStream = Resources.getResourceAsStream(resource);

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

那么咱们先看看源码是怎么实现的:

首先,SqlSessionFactory是一个接口,它里面其实就两个方法:

  • openSession

  • getConfiguration。

package org.apache.ibatis.session;

public interface SqlSessionFactory {

  SqlSession openSession();

  SqlSession openSession(boolean autoCommit);

  SqlSession openSession(Connection connection);

  SqlSession openSession(TransactionIsolationLevel level);

  SqlSession openSession(ExecutorType execType);

  SqlSession openSession(ExecutorType execType, boolean autoCommit);

  SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);

  SqlSession openSession(ExecutorType execType, Connection connection);

  Configuration getConfiguration();

}

然后使用SqlSessionFactoryBuilder构建出一个具体的实现类DefaultSqlSessionFactory。

这个DefaultSqlSessionFactory类里面有一个Configuration对象,保存了mapper映射文件、SQL参数、返回值类型、缓存等属性。

再继续看SqlSessionFactoryBuilder类的build()方法,其实内部调用了XMLConfigBuilder这个类的parse()方法解析我们的配置文件构建出一个Configuration对象,然后传入了DefaultSqlSessionFactory的构造函数得到一个DefaultSqlSessionFactory对象。那就继续看XMLConfigBuilder,XMLConfigBuilder类继承自BaseBuilder,是XML配置构建器。

BaseBuilder中有是三个属性: Configuration, TypeAliasRegistry和TypeHandlerRegistry:

public abstract class BaseBuilder {

    protected final Configuration configuration;

    protected final TypeAliasRegistry typeAliasRegistry;

    protected final TypeHandlerRegistry typeHandlerRegistry;

}

 

XMLConfigBuilder中的解析流程主要就是parseConfiguration()这个方法:

/**

* mybatis 配置文件解析

*/

public class XMLConfigBuilder extends BaseBuilder {

  public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {

    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);

}

此方法就是解析configuration节点下的子节点。由此也可看出,我们在configuration下面能配置的节点为以下10个节点  

private void parseConfiguration(XNode root) {

    try {

      propertiesElement(root.evalNode("properties")); //issue #117 read properties first

      typeAliasesElement(root.evalNode("typeAliases"));

      pluginElement(root.evalNode("plugins"));

      objectFactoryElement(root.evalNode("objectFactory"));

      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));

      settingsElement(root.evalNode("settings"));

      environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631

      databaseIdProviderElement(root.evalNode("databaseIdProvider"));

      typeHandlerElement(root.evalNode("typeHandlers"));

      mapperElement(root.evalNode("mappers"));

    } catch (Exception e) {

      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);

    }

  }

  ……

}

 

通过以上源码,我们就能看出,在mybatis的配置文件中:

  • configuration节点为根节点。

  • 在configuration节点之下,我们可以配置10个子节点, 分别为:properties、typeAliases、plugins、objectFactory、objectWrapperFactory、settings、environments、databaseIdProvider、typeHandlers、mappers。

 

2.解析配置文件中的properties

上次我们说过mybatis 是通过XMLConfigBuilder这个类在解析mybatis配置文件的,那么本次就接着看看XMLConfigBuilder对于properties和environments的解析:

properties的使用方法:

<configuration>

<!-- 方法一: 从外部指定properties配置文件, 除了使用resource属性指定外,还可通过url属性指定url

  <properties resource="dbConfig.properties"></properties>

  -->

  <!-- 方法二: 直接配置为xml -->

  <properties>

      <property name="driver" value="com.mysql.jdbc.Driver"/>

      <property name="url" value="jdbc:mysql://localhost:3306/test1"/>

      <property name="username" value="root"/>

      <property name="password" value="root"/>

  </properties>

 

首先看XMLConfigBuilder中的propertiesElement()方法

 //下面就看看解析properties的具体方法

    private void propertiesElement(XNode context) throws Exception {

        if (context != null) {

          //将子节点的 name 以及value属性set进properties对象

          //这儿可以注意一下顺序,xml配置优先, 外部指定properties配置其次

          Properties defaults = context.getChildrenAsProperties();

          //获取properties节点上 resource属性的值

          String resource = context.getStringAttribute("resource");

          //获取properties节点上 url属性的值, resource和url不能同时配置

          String url = context.getStringAttribute("url");

          if (resource != null && url != null) {

            throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");

          }

          //把解析出的properties文件set进Properties对象

          if (resource != null) {

            defaults.putAll(Resources.getResourceAsProperties(resource));

          } else if (url != null) {

            defaults.putAll(Resources.getUrlAsProperties(url));

          }

          //将configuration对象中已配置的Properties属性与刚刚解析的融合

          //configuration这个对象会装载所解析mybatis配置文件的所有节点元素,以后也会频频提到这个对象

          //既然configuration对象用有一系列的get/set方法, 那是否就标志着我们可以使用java代码直接配置?

          //答案是肯定的, 不过使用配置文件进行配置,优势不言而喻

          Properties vars = configuration.getVariables();

          if (vars != null) {

            defaults.putAll(vars);

          }

          //把装有解析配置propertis对象set进解析器, 因为后面可能会用到

          parser.setVariables(defaults);

          //set进configuration对象

          configuration.setVariables(defaults);

        }

    }

 

解析完成之后直接调用Configuration的setVariables()方法保存到Configuration的 variables 中:

public void setVariables(Properties variables) {

    this.variables = variables;

}


 

 

3.解析配置文件中的envirements

 

envirements元素节点的使用方法:

<environments default="development">

    <environment id="development">

      <transactionManager type="JDBC"/>

      <dataSource type="POOLED">

          <!--

          如果上面没有指定数据库配置的properties文件,那么此处可以这样直接配置

        <property name="driver" value="com.mysql.jdbc.Driver"/>

        <property name="url" value="jdbc:mysql://localhost:3306/test1"/>

        <property name="username" value="root"/>

        <property name="password" value="root"/>

         -->

         

         <!-- 上面指定了数据库配置文件, 配置文件里面也是对应的这四个属性 -->

         <property name="driver" value="${driver}"/>

         <property name="url" value="${url}"/>

         <property name="username" value="${username}"/>

         <property name="password" value="${password}"/>

         

      </dataSource>

    </environment>

    

    <!-- 我再指定一个environment -->

    <environment id="test">

      <transactionManager type="JDBC"/>

      <dataSource type="POOLED">

        <property name="driver" value="com.mysql.jdbc.Driver"/>

        <!-- 与上面的url不一样 -->

        <property name="url" value="jdbc:mysql://localhost:3306/demo"/>

        <property name="username" value="root"/>

        <property name="password" value="root"/>

      </dataSource>

    </environment>

    

  </environments>

environments元素节点可以配置多个environment子节点, 怎么理解呢?假如我们系统的开发环境和正式环境所用的数据库不一样(这是肯定的), 那么可以设置两个environment, 两个id分别对应开发环境(dev)和正式环境(final),那么通过配置environments的default属性就能选择对应的environment了, 例如,我将environments的deault属性的值配置为dev, 那么就会选择dev的environment。 

 

具体的解析方法是在XMLConfigBuilder中的environmentsElement()方法中:    

//下面再看看解析enviroments元素节点的方法

    private void environmentsElement(XNode context) throws Exception {

        if (context != null) {

            if (environment == null) {

                //解析environments节点的default属性的值

                //例如: <environments default="development">

                environment = context.getStringAttribute("default");

            }

            //递归解析environments子节点

            for (XNode child : context.getChildren()) {

                //<environment id="development">, 只有enviroment节点有id属性,那么这个属性有何作用?

                //environments 节点下可以拥有多个 environment子节点

                //类似于这样: <environments default="development"><environment id="development">...</environment><environment id="test">...</environments>

                //意思就是我们可以对应多个环境,比如开发环境,测试环境等, 由environments的default属性去选择对应的enviroment

                String id = child.getStringAttribute("id");

                //isSpecial就是根据由environments的default属性去选择对应的enviroment

                if (isSpecifiedEnvironment(id)) {

                    //事务, mybatis有两种:JDBC 和 MANAGED, 配置为JDBC则直接使用JDBC的事务,配置为MANAGED则是将事务托管给容器,

                    TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));

                    //enviroment节点下面就是dataSource节点了,解析dataSource节点(下面会贴出解析dataSource的具体方法)

                    DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));

                    DataSource dataSource = dsFactory.getDataSource();

                    Environment.Builder environmentBuilder = new Environment.Builder(id)

                          .transactionFactory(txFactory)

                          .dataSource(dataSource);

                    //老规矩,会将dataSource设置进configuration对象

                    configuration.setEnvironment(environmentBuilder.build());

                }

            }

        }

    }
//下面看看dataSource的解析方法

    private DataSourceFactory dataSourceElement(XNode context) throws Exception {

        if (context != null) {

            //dataSource的连接池

            String type = context.getStringAttribute("type");

            //子节点 name, value属性set进一个properties对象

            Properties props = context.getChildrenAsProperties();

            //创建dataSourceFactory

            DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();

            factory.setProperties(props);

            return factory;

        }

        throw new BuilderException("Environment declaration requires a DataSourceFactory.");

    }


    

还有一个问题, 上面我们看到,在配置dataSource的时候使用了 ${driver} 这种表达式, 这种形式是怎么解析的?其实,是通过PropertyParser这个类解析。在XNode类的parseAttributes()方法中调用了PropertyParser的parse()方法:

 

 

4.解析配置文件中的typeAliases

typeAliases节点主要用来设置别名,其实这是挺好用的一个功能, 通过配置别名,我们不用再指定完整的包名,并且还能取别名。例如: 我们在使用 com.demo.entity. UserEntity 的时候,我们可以直接配置一个别名user, 这样以后在配置文件中要使用到com.demo.entity. UserEntity的时候,直接使用User即可。就以上例为例,我们来实现一下,看看typeAliases的配置方法:  

<configuration>

    <typeAliases>

      <!--

      通过package, 可以直接指定package的名字, mybatis会自动扫描你指定包下面的javabean,

      并且默认设置一个别名,默认的名字为: javabean 的首字母小写的非限定类名来作为它的别名。

      也可在javabean 加上注解@Alias 来自定义别名, 例如: @Alias(user)

      <package name="com.dy.entity"/>

       -->

      <typeAlias alias="UserEntity" type="com.dy.entity.User"/>

  </typeAliases>

  ......

  

</configuration>

 

先从XMLConfigBuilder的typeAliasesElement()方法开始:

/**

* 解析typeAliases节点

*/

private void typeAliasesElement(XNode parent) {

    if (parent != null) {

      for (XNode child : parent.getChildren()) {

        //如果子节点是package, 那么就获取package节点的name属性, mybatis会扫描指定的package

        if ("package".equals(child.getName())) {

          String typeAliasPackage = child.getStringAttribute("name");

          //TypeAliasRegistry 负责管理别名, 这儿就是通过TypeAliasRegistry 进行别名注册, 下面就会看看TypeAliasRegistry源码

          configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);

        } else {

          //如果子节点是typeAlias节点,那么就获取alias属性和type的属性值

          String alias = child.getStringAttribute("alias");

          String type = child.getStringAttribute("type");

          try {

            Class<?> clazz = Resources.classForName(type);

            if (alias == null) {

              typeAliasRegistry.registerAlias(clazz);

            } else {

              typeAliasRegistry.registerAlias(alias, clazz);

            }

          } catch (ClassNotFoundException e) {

            throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);

          }

        }

      }

    }

  }



这有个很重要的类TypeAliasRegistry,TypeAliasRegistry内部维护了一个TYPE_ALIASES属性,它是一个HashMap,所以别名其实就是通过一个HashMap来实现, key为别名, value就是别名对应的类型(class对象),里面已经默认添加了很多的类型别名。。





解析完成然后调用TypeAliasRegistry类的registerAlias()方法注册到容器中。



public class TypeAliasRegistry {

    private final Map<String, Class<?>> typeAliases = new HashMap<>();



    public void registerAlias(Class<?> type) {

       String alias = type.getSimpleName();

       Alias aliasAnnotation = type.getAnnotation(Alias.class);

       if (aliasAnnotation != null) {

         alias = aliasAnnotation.value();

       }

       String key = alias.toLowerCase(Locale.ENGLISH);

       TYPE_ALIASES.put(key, type);

    }

    ......

}

 

 

在使用时,除了在xml中配置aliases,还可以使用@Alias注解来设置别名的名称。

 

5.解析配置中的typeHanlder

 

5.1 typeHanlder的解析

它是一个类型的处理器。在数据库查询出结果后,应该转换成Java中的什么类型?由它来决定。如果Mybatis里面没有你想要的,就可以在这里自定义一个处理器。例如:

 

现在,先来看下它的默认处理器。

 

 

看一下IntegerTypeHanlder:

/**

* @author Clinton Begin

*/

/**

* Integer类型处理器

* 调用PreparedStatement.setInt, ResultSet.getInt, CallableStatement.getInt

*

*/

public class IntegerTypeHandler extends BaseTypeHandler<Integer> {



  @Override

  public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType)

      throws SQLException {

    ps.setInt(i, parameter);

  }



  @Override

  public Integer getNullableResult(ResultSet rs, String columnName)

      throws SQLException {

    return rs.getInt(columnName);

  }



  @Override

  public Integer getNullableResult(ResultSet rs, int columnIndex)

      throws SQLException {

    return rs.getInt(columnIndex);

  }



  @Override

  public Integer getNullableResult(CallableStatement cs, int columnIndex)

      throws SQLException {

    return cs.getInt(columnIndex);

  }

}

 

 

看一下XMLConfigBuilder中的typeHandlerElement()方法:

/**

* 解析typeHandlers节点

*/

private void typeHandlerElement(XNode parent) throws Exception {

    if (parent != null) {

      for (XNode child : parent.getChildren()) {

        //子节点为package时,获取其name属性的值,然后自动扫描package下的自定义typeHandler

        if ("package".equals(child.getName())) {

          String typeHandlerPackage = child.getStringAttribute("name");

          typeHandlerRegistry.register(typeHandlerPackage);

        } else {

          //子节点为typeHandler时, 可以指定javaType属性, 也可以指定jdbcType, 也可两者都指定

          //javaType 是指定java类型

          //jdbcType 是指定jdbc类型(数据库类型: 如varchar)

          String javaTypeName = child.getStringAttribute("javaType");

          String jdbcTypeName = child.getStringAttribute("jdbcType");

          //handler就是我们配置的typeHandler

          String handlerTypeName = child.getStringAttribute("handler");

          //resolveClass方法就是我们上篇文章所讲的TypeAliasRegistry里面处理别名的方法

          Class<?> javaTypeClass = resolveClass(javaTypeName);

          //JdbcType是一个枚举类型,resolveJdbcType方法是在获取枚举类型的值

          JdbcType jdbcType = resolveJdbcType(jdbcTypeName);

          Class<?> typeHandlerClass = resolveClass(handlerTypeName);

          //注册typeHandler, typeHandler通过TypeHandlerRegistry这个类管理

          if (javaTypeClass != null) {

            if (jdbcType == null) {

              typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);

            } else {

              typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);

            }

          } else {

            typeHandlerRegistry.register(typeHandlerClass);

          }

        }

      }

    }

}

 

解析完成之后都是注册到XMLConfigBuilder的typeHandlerRegistry属性中。TypeHandlerRegistry这个类和上面的TypeAliasRegistry有点类似,内部也是维护了一个HashMap,key为Java类型,value为自定义的Handler类型。

 

 

5.2 自定义TypeHandler

 

(1) 案例分析

在日常开发中,我们肯定有对日期类型的操作。比如订单时间、付款时间等,通常这一类数据在数据库以datetime类型保存。如果需要在页面上展示此值,在Java中以什么类型接收它呢?

在不执行任何二次操作的情况下:

用java.util.Date接收,在页面展示的就是Tue Oct 16 16:05:13 CST 2018。

用java.lang.String接收,在页面展示的就是2018-10-16 16:10:47.0。

显然,我们不能显示第一种。第二种似乎可行,但大部分情况下不能出现毫秒数。当然了,不管哪种方式,在显示的时候format一下当然是可行的。有没有更好的方式呢?那就是typeHanlders

 

(2) typeHandlers

无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型。

在数据库中,datetime和timestamp类型含义是一样的,不过timestamp存储空间小, 所以它表示的时间范围也更小。

下面来看几个Mybatis默认的时间类型处理器。

JDBC 类型

Java 类型

类型处理器

DATE

java.util.Date

DateOnlyTypeHandler

DATE

java.sql.Date

SqlDateTypeHandler

DATE

java.time.LocalDate

LocalDateTypeHandler

DATE

java.time.LocalTime

LocalTimeTypeHandler

TIMESTAMP

java.util.Date

DateTypeHandler

TIMESTAMP

java.time.Instant

InstantTypeHandler

TIMESTAMP

java.time.LocalDateTime

LocalDateTimeTypeHandler

TIMESTAMP

java.sql.Timestamp

SqlTimestampTypeHandler

它是什么意思呢?如果数据库字段类型为JDBC 类型,同时Java字段的类型为Java 类型,那么就调用类型处理器类型处理器。

 

 

(3) 自定义处理器

 

基于上面这个逻辑,我们可以增加一种处理器来处理我们开头所描述的问题。我们可以在Java中,以String类型接收数据库的DateTime类型数据。因为现在的接口以restful风格居多,用String类型方便传输。

最后的毫秒数通过自定义的处理器统一截取去除即可。

JDBC 类型

Java 类型

类型处理器

TIMESTAMP

java.lang.String

CustomTypeHandler

 

(1)xml中配置自定义类型处理器

<property name="typeHandlers">

    <array>

        <bean class="com.viewscenes.netsupervisor.util.CustomTypeHandler"></bean>

    </array>

</property>

 

(2)在对应的CustomTypeHandler类上加上注解

@MappedJdbcTypes注解表示JDBC的类型,@MappedTypes表示Java属性的类型。

@MappedJdbcTypes({ JdbcType.TIMESTAMP })@MappedTypes({ String.class })

public class CustomTypeHandler extends BaseTypeHandler<String>{

    @Override

    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)

            throws SQLException {

        ps.setString(i, parameter);

    }

    @Override

    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {

        return substring(rs.getString(columnName));

    }

    @Override

    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {

        return rs.getString(columnIndex);

    }

    @Override

    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {

        return cs.getString(columnIndex);

    }

    private String substring(String value) {

        if (!"".endsWith(value) && value != null) {

            return value.substring(0, value.length() - 2);

        }

        return value;

    }

}

通过以上方式,我们就可以放心的在Java中以String接收数据库的时间类型数据了。

 

 

6.objectFactory、plugins简介与配置解析

 

6.1 objectFactory是干什么的? 需要配置吗?

  MyBatis 每次创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成。默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认构造方法,要么在参数映射存在的时候通过参数构造方法来实例化。默认情况下,我们不需要配置,mybatis会调用默认实现的objectFactory。 除非我们要自定义ObjectFactory的实现, 那么我们才需要去手动配置。

  那么怎么自定义实现ObjectFactory? 怎么配置呢?

  自定义ObjectFactory只需要去继承DefaultObjectFactory(是ObjectFactory接口的实现类),并重写其方法即可。具体的,本处不多说,后面再具体讲解。

  写好了ObjectFactory, 仅需做如下配置:

<configuration>

    ......

    <objectFactory type="org.mybatis.example.ExampleObjectFactory">

        <property name="someProperty" value="100"/>

    </objectFactory>

    ......

  </configuration

 

6.2 plugins

可以配置一个或多个插件。插件功能很强大,在执行SQL之前,在返回结果之后,在插入数据时...都可以让你有机会插手数据的处理(拦截器)。这个部分最常用的是分页,在后续章节,笔者将通过分页和数据同步的实例单独讲解。

<property name="plugins">

    <array>

        <bean class="com.viewscenes.netsupervisor.interceptor.xxxInterceptor"></bean>

        <bean class="com.viewscenes.netsupervisor.interceptor.xxxInterceptor"></bean>

    </array>

</property>

XMLConfigBuilder中的objectFactoryElement()方法和pluginElement()方法:

/**

* objectFactory 节点解析

*/

private void objectFactoryElement(XNode context) throws Exception {

    if (context != null) {

      //读取type属性的值, 接下来进行实例化ObjectFactory, 并set进 configuration

      //到此,简单讲一下configuration这个对象,其实它里面主要保存的都是mybatis的配置

      String type = context.getStringAttribute("type");

      //读取propertie的值, 根据需要可以配置, mybatis默认实现的objectFactory没有使用properties

      Properties properties = context.getChildrenAsProperties();

      

      ObjectFactory factory = (ObjectFactory) resolveClass(type).newInstance();

      factory.setProperties(properties);

      configuration.setObjectFactory(factory);

    }

}

  

  /**

   * plugins 节点解析

   */

  private void pluginElement(XNode parent) throws Exception {

    if (parent != null) {

      for (XNode child : parent.getChildren()) {

        String interceptor = child.getStringAttribute("interceptor");

        Properties properties = child.getChildrenAsProperties();

        //由此可见,我们在定义一个interceptor的时候,需要去实现Interceptor, 这儿先不具体讲,以后会详细讲解

        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();

        interceptorInstance.setProperties(properties);

        configuration.addInterceptor(interceptorInstance);

      }

    }

  }

  

 

7.解析配置中的mappers

 

mapper映射文件的配置, 这是mybatis的核心之一,一定要学好。在mapper文件中,以mapper作为根节点,其下面可以配置的元素节点有: select, insert, update, delete, cache, cache-ref, resultMap, sql 。

 

7.1 mapper中的配置

看看 insert, update, delete 怎么配置, 能配置哪些元素吧:

<?xml version="1.0" encoding="UTF-8" ?>   

<!DOCTYPE mapper   

PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"  

"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">





<!-- mapper 为根元素节点, 一个namespace对应一个dao -->

<mapper namespace="com.dy.dao.UserDao">





    <insert

      <!-- 1. id (必须配置)

        id是命名空间中的唯一标识符,可被用来代表这条语句。

        一个命名空间(namespace) 对应一个dao接口,

        这个id也应该对应dao里面的某个方法(相当于方法的实现),因此id 应该与方法名一致 -->

      id="insertUser"

      

      <!-- 2. parameterType (可选配置, 默认为mybatis自动选择处理)

        将要传入语句的参数的完全限定类名或别名, 如果不配置,mybatis会通过ParameterHandler 根据参数类型默认选择合适的typeHandler进行处理

        parameterType 主要指定参数类型,可以是int, short, long, string等类型,也可以是复杂类型(如对象) -->

      parameterType="com.demo.User"

      

      <!-- 3. flushCache (可选配置,默认配置为true)

        将其设置为 true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:true(对应插入、更新和删除语句) -->

      flushCache="true"

      

      <!-- 4. statementType (可选配置,默认配置为PREPARED)

        STATEMENT,PREPARED 或 CALLABLE 的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。 -->

      statementType="PREPARED"

      

      <!-- 5. keyProperty (可选配置, 默认为unset)

        (仅对 insert 和 update 有用)唯一标记一个属性,MyBatis 会通过 getGeneratedKeys 的返回值或者通过 insert 语句的 selectKey 子元素设置它的键值,默认:unset。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。 -->

      keyProperty=""

      

      <!-- 6. keyColumn     (可选配置)

        (仅对 insert 和 update 有用)通过生成的键值设置表中的列名,这个设置仅在某些数据库(像 PostgreSQL)是必须的,当主键列不是表中的第一列的时候需要设置。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。 -->

      keyColumn=""

      

      <!-- 7. useGeneratedKeys (可选配置, 默认为false)

        (仅对 insert 和 update 有用)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系数据库管理系统的自动递增字段),默认值:false。  -->

      useGeneratedKeys="false"

      

      <!-- 8. timeout  (可选配置, 默认为unset, 依赖驱动)

        这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为 unset(依赖驱动)。 -->

      timeout="20">



    <update

      id="updateUser"

      parameterType="com.demo.User"

      flushCache="true"

      statementType="PREPARED"

      timeout="20">



    <delete

      id="deleteUser"

      parameterType="com.demo.User"

      flushCache="true"

      statementType="PREPARED"

      timeout="20">

</mapper>

 

此外,如果我们在使用mysql的时候,想在数据插入后返回插入的id, 我们也可以使用 selectKey 这个元素:

<!-- 对应userDao中的insertUser方法,  -->

   <insert id="insertUser" parameterType="com.dy.entity.User">

           <!-- oracle等不支持id自增长的,可根据其id生成策略,先获取id

           

        <selectKey resultType="int" order="BEFORE" keyProperty="id">

              select seq_user_id.nextval as id from dual

        </selectKey>

        

        -->

        

        <!-- mysql插入数据后,获取id -->

        <selectKey keyProperty="id" resultType="int" order="AFTER" >

               SELECT LAST_INSERT_ID() as id

        </selectKey>

          

        insert into user(id, name, password, age, deleteFlag)

               values(#{id}, #{name}, #{password}, #{age}, #{deleteFlag})

   </insert>

selectKey的具体用法:

<selectKey

        <!-- selectKey 语句结果应该被设置的目标属性。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。 -->

        keyProperty="id"

        <!-- 结果的类型。MyBatis 通常可以推算出来,但是为了更加确定写上也不会有什么问题。MyBatis 允许任何简单类型用作主键的类型,包括字符串。如果希望作用于多个生成的列,则可以使用一个包含期望属性的 Object 或一个 Map。 -->

        resultType="int"

        <!-- 这可以被设置为 BEFORE 或 AFTER。如果设置为 BEFORE,那么它会首先选择主键,设置 keyProperty 然后执行插入语句。如果设置为 AFTER,那么先执行插入语句,然后是 selectKey 元素 - 这和像 Oracle 的数据库相似,在插入语句内部可能有嵌入索引调用。 -->

        order="BEFORE"

        <!-- 与前面相同,MyBatis 支持 STATEMENT,PREPARED 和 CALLABLE 语句的映射类型,分别代表 PreparedStatement 和 CallableStatement 类型。 -->

        statementType="PREPARED">

 

 

还有一个resultMap 这个东西, mybatis的resultMap功能可谓十分强大,能够处理复杂的关系映射, 那么resultMap 该怎么配置呢? resultMap的配置:

<!--

        1.type 对应类型,可以是javabean, 也可以是其它

        2.id 必须唯一, 用于标示这个resultMap的唯一性,在使用resultMap的时候,就是通过id指定

     -->

    <resultMap type="" id="">

    

        <!-- id, 唯一性,注意啦,这个id用于标示这个javabean对象的唯一性, 不一定会是数据库的主键(不要把它理解为数据库对应表的主键)

            property属性对应javabean的属性名,column对应数据库表的列名

            (这样,当javabean的属性与数据库对应表的列名不一致的时候,就能通过指定这个保持正常映射了)

        -->

        <id property="" column=""/>

        

        <!-- result与id相比, 对应普通属性 -->    

        <result property="" column=""/>

        

        <!--

            constructor对应javabean中的构造方法

         -->

        <constructor>

            <!-- idArg 对应构造方法中的id参数 -->

            <idArg column=""/>

            <!-- arg 对应构造方法中的普通参数 -->

            <arg column=""/>

        </constructor>

        

        <!--

            collection,对应javabean中容器类型, 是实现一对多的关键

            property 为javabean中容器对应字段名

            column 为体现在数据库中列名

            ofType 就是指定javabean中容器指定的类型

        -->

        <collection property="" column="" ofType=""></collection>

        

        <!--

            association 为关联关系,是实现N对一的关键。

            property 为javabean中容器对应字段名

            column 为体现在数据库中列名

            javaType 指定关联的类型

         -->

        <association property="" column="" javaType=""></association>

    </resultMap>

 

其实就是查询结果为一个列表。

 

 

7.2 mapper的解析

首先调用XMLConfigBuilder的mapperElement()方法:

这里又涉及到另一个类XMLMapperBuilder,它也是继承自BaseBuilder。

通过解析里面的select/insert/update/delete节点,每一个节点生成一个MappedStatement对象。最后注册到Configuration对象的    。key为mapper的namespace+节点id。

 

在XMLMapperBuilder类的configurationElement()方法中:

public class XMLMapperBuilder extends BaseBuilder {

    private void configurationElement(XNode context) {

        //命名空间 即mapper接口的路径

        String namespace = context.getStringAttribute("namespace");

        if (namespace == null || namespace.equals("")) {

            throw new BuilderException("Mapper's namespace cannot be empty");

        }

        //设置当前mapper文件的命名空间

        builderAssistant.setCurrentNamespace(namespace);

        //引用缓存

        cacheRefElement(context.evalNode("cache-ref"));

        //是否开启二级缓存

        cacheElement(context.evalNode("cache"));

        //参数

        parameterMapElement(context.evalNodes("/mapper/parameterMap"));

        //返回值

        resultMapElements(context.evalNodes("/mapper/resultMap"));

        //解析sql节点

        sqlElement(context.evalNodes("/mapper/sql"));

        //SQL语句解析

        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));

    }}

 

 

7.3 resultMap、parameterMap的解析

它们表示的是查询结果集中的列与Java对象中属性的对应关系。其实,只有在数据库字段与JavaBean不匹配的情况下才用到,通常情况下推荐使用resultType/parameterType,也就是直接利用实体类即可。这种方式很简便,同时遵循约定大于配置,代码出错的可能较小。

XMLMapperBuilder中有个mapperBuilderAssistants属性,在解析mapper的过程中,会调用MapperBuilderAssistant类的addParameterMap方法,然后将resultMap或者parameterMap都注册到configuration中:


 

public void addParameterMap(ParameterMap pm) {

    parameterMaps.put(pm.getId(), pm);

}



public void addResultMap(ResultMap rm) {

    resultMaps.put(rm.getId(), rm);

}

 

 


 

三、mapper中SQL的使用和解析

1.例子:

(1) if

<select id="findUserById" resultType="user">

           select * from user where

           <if test="id != null">

               id=#{id}

           </if>

            and deleteFlag=0;

</select>

上面例子: 如果传入的id 不为空, 那么才会SQL才拼接id = #{id}。 

但是如果传入的id为null,  那么你这最终的SQL语句不就成了 select * from user where and deleteFlag=0,  这语句有问题!。这时候,mybatis的 where 标签就该隆重登场啦,改造一下:

 

(2) where

<select id="findUserById" resultType="user">

           select * from user            <where>

               <if test="id != null">

                   id=#{id}

               </if>

               and deleteFlag=0;

           </where>

</select>

有些人就要问了: “你这都是些什么玩意儿! 跟上面的相比, 不就是多了个where标签嘛! 那这个还会不会出现  select * from user where and deleteFlag=0 ?”

的确,从表面上来看,就是多了个where标签而已, 不过实质上, mybatis是对where做了处理,当它遇到AND或者OR这些,它可以自动去除and或者or前后的语句。

 

(3) trim

其实我们可以手动通过 trim 标签去自定义这种处理规则。例如:

<trim prefix="WHERE" prefixOverrides="AND |OR ">

  ...

</trim>

它的意思就是: 当WHERE后紧随AND或则OR的时候,就去除AND或者OR。 除了WHERE以外, 其实还有一个比较经典的实现,那就是SET。

 

 

(4) set

<update id="updateUser" parameterType="com.dy.entity.User">

           update user set

           <if test="name != null">

               name = #{name},

           </if>

           <if test="password != null">

               password = #{password},

           </if>

           <if test="age != null">

               age = #{age}

           </if>

           <where>

               <if test="id != null">

                   id = #{id}

               </if>

               and deleteFlag = 0;

           </where>

</update>

 

问题又来了: “如果我只有name不为null,  那么这SQL不就成了 update set name = #{name}, where ........ ?  你那name后面那逗号会导致出错啊!”

是的,这时候,就可以用mybatis为我们提供的set 标签了。下面是通过set标签改造后:

<update id="updateUser" parameterType="com.dy.entity.User">

           update user        <set>

          <if test="name != null">name = #{name},</if>

             <if test="password != null">password = #{password},</if>

             <if test="age != null">age = #{age},</if>

        </set>

           <where>

               <if test="id != null">

                   id = #{id}

               </if>

               and deleteFlag = 0;

           </where>

</update>

这个用trim 可表示为:

<trim prefix="SET" suffixOverrides=",">

  ...

</trim>

总结一下,WHERE是使用的 prefixOverrides(前缀), SET是使用的 suffixOverrides (后缀), 看明白了吧!

 

(5) foreach

<select id="selectPostIn" resultType="domain.blog.Post">

  SELECT *

  FROM POST P

  WHERE ID in

  <foreach item="item" index="index" collection="list"

      open="(" separator="," close=")">

        #{item}

  </foreach>

</select>

 

将一个 List 实例或者数组作为参数对象传给 MyBatis,当这么做的时候,MyBatis 会自动将它包装在一个 Map 中并以名称为键。List 实例将会以“list”作为键,而数组实例的键将是“array”。同样, 当循环的对象为map的时候,index其实就是map的key。

 

(6) choose+when

Java中有switch,  mybatis有choose。

<select id="findActiveBlogLike"

     resultType="Blog">

  SELECT * FROM BLOG WHERE state = ‘ACTIVE’

  <choose>

    <when test="title != null">

      AND title like #{title}

    </when>

    <when test="author != null and author.name != null">

      AND author_name like #{author.name}

    </when>

    <otherwise>

      AND featured = 1

    </otherwise>

  </choose>

</select>

以上例子中: 当title和author都不为null的时候, 那么选择二选一(前者优先), 如果都为null, 那么就选择 otherwise中的, 如果tilte和author只有一个不为null, 那么就选择不为null的那个。

 

 

2.动态SQL的解析

 

 

  • SQL标签

SQL标签可将重复的sql提取出来,使用时用include引用即可,最终达到sql重用的目的。它的解析很简单,就是把内容放入sqlFragments容器。id为命名空间+节点ID

 

  • 动态SQL

动态SQL的解析是Mybatis的核心所在。之所以是动态SQL,源自它不同的动态标签,比如Choose、ForEach、If、Set等,而Mybatis把它们都封装成不同的类对象,它们共同的接口是SqlNode。

每一种标签又对应一种处理器。

 

一个动态SQL会分为不同的子节点,我们以一个UPDATE语句为例,尝试跟踪下它的解析过程。比如下面的UPDATE节点会分为三个子节点。两个静态节点和一个SET动态节点,而SET节点又分为两个IF动态节点。

<update id="updateUser" parameterType="user">

    update user

    <set>

        <if test="username!=null">

            username = #{username},

        </if>

        <if test="password!=null">

            password = #{password},

        </if>

    </set>

    where uid = #{uid}

</update>

 

[

    [#text: update user ],

    [set: null],

    [#text: where uid = #{uid}]

]

 

 

在解析mapper的过程中,获取到每个mapper之后,调用XMLStatementBuilder的parseStatementNode()方法:

然后这个方法内部会调用XMLLanguageDriver的createSqlSource()方法:

 

SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

createSqlSource()方法的内容:

所以具体的解析过程是在XMLScriptBuilder类中:

 

public class XMLScriptBuilder extends BaseBuilder {

    

    //参数node即为当前UDATE节点的内容

    protected MixedSqlNode parseDynamicTags(XNode node) {

        List<SqlNode> contents = new ArrayList<SqlNode>();

        //children分为3个子节点

        //2个静态节点(update user和where id=#{id})

        //1个动态节点set

        NodeList children = node.getNode().getChildNodes();

        for (int i = 0; i < children.getLength(); i++) {

          XNode child = node.newXNode(children.item(i));

          //如果是静态节点,将内容封装成StaticTextSqlNode对象

          if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {

            String data = child.getStringBody("");

            TextSqlNode textSqlNode = new TextSqlNode(data);

            if (textSqlNode.isDynamic()) {

              contents.add(textSqlNode);

              isDynamic = true;

            } else {

              contents.add(new StaticTextSqlNode(data));

            }

          }

          //动态节点

          else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628

            //获取节点名称 比如SET/IF

            String nodeName = child.getNode().getNodeName();

            //获取节点标签对应的处理类 比如SetHandler

            NodeHandler handler = nodeHandlerMap.get(nodeName);

            handler.handleNode(child, contents);

            isDynamic = true;

          }

        }

        return new MixedSqlNode(contents);

    }

}

 

第一个是静态节点update user。根据上面的源码,它将被封装成StaticTextSqlNode对象,加入contents集合。

第二个是动态节点SET,他将调用到SetHandler.handleNode()。在第二次回调到parseDynamicTags方法时,这时候的参数为SET节点里的2个IF子节点。同样,它们会将当做动态节点解析,调用到IfHandler.handleNode()。

就这样,递归的调用parseDynamicTags方法,直到传进来的参数Node为一个静态节点,返回StaticTextSqlNode对象,并加入集合中。

第三个是静态节点where id=#{id},封装成StaticTextSqlNode对象,加入contents集合。

最后,contents集合就是UPDATE节点对应的各种sqlNode。

对照一下原来的mapper.xml:

 

 

如果是动态SQL,返回DynamicSqlSource对象:

public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {

    this.configuration = configuration;

    this.rootSqlNode = rootSqlNode;

}


 

 

 

3.静态SQL的解析

静态SQL,就是不带上面那些动态标签的节点。它比较简单,可能就是一个单独的select,update,insert或者delete的sql语句,最后就是将SQL内容封装到StaticTextSqlNode对象。MixedSqlNode对象里面有个List,封装的就是StaticTextSqlNode对象,而StaticTextSqlNode对象只有一个属性text,即SQL内容。

 

解析SQL内容:

protected MixedSqlNode parseDynamicTags(XNode node) {

    //SQL内容

    String data = child.getStringBody("");

    //生成TextSqlNode判断是否为动态SQL

    TextSqlNode textSqlNode = new TextSqlNode(data);

    if (textSqlNode.isDynamic()) {

      contents.add(textSqlNode);

      isDynamic = true;

    } else {

      contents.add(new StaticTextSqlNode(data));

    }

    return new MixedSqlNode(contents);

}

StaticTextSqlNode类:

 

 

如果是静态SQL,将SQL语句中的#{}转为?,返回StaticSqlSource对象 :

public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {

    this.sql = sql;

    this.parameterMappings = parameterMappings;

    this.configuration = configuration;

}

 

 

 

可以看到DynamicSqlSource和StaticSqlSource都有一个getBoundSql()方法返回一个BoundSql对象,这个BoundSql里面包含了一个完整的可执行的sql语句。

 

 

  • MappedStatement对象

mapper文件中的每一个SELECT/INSERT/UPDATE/DELETE节点对应一个MappedStatement对象。

 

在XMLStatementBuilder的parseStatementNode()方法中,解析到sqlSource,resultMap,parameterMap等内容后,调用builderAssistant的addMappedStatement()方法:

 

生成一个MappedStatement对象,并注册到configuration中:

public MappedStatement addMappedStatement() {

    

    //全限定类名+方法名

    id = applyCurrentNamespace(id, false);

    //是否为查询语句

    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    

    //配置各种属性

    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)

        .resource(resource)

        .fetchSize(fetchSize)

        .timeout(timeout)

        .statementType(statementType)

        .keyGenerator(keyGenerator)

        .keyProperty(keyProperty)

        .keyColumn(keyColumn)

        .databaseId(databaseId)

        .lang(lang)

        .resultOrdered(resultOrdered)

        .resultSets(resultSets)

        .resultMaps(getStatementResultMaps(resultMap, resultType, id))

        .resultSetType(resultSetType)

        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))

        .useCache(valueOrDefault(useCache, isSelect))

        .cache(currentCache);

    

    //参数类型

    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);

    if (statementParameterMap != null) {

      statementBuilder.parameterMap(statementParameterMap);

    }



    //将MappedStatement对象注册到configuration

    //注册其实就是往Map中添加。mappedStatements.put(ms.getId(), ms);

    MappedStatement statement = statementBuilder.build();

    configuration.addMappedStatement(statement);

    return statement;

  }

 


 

四、Annotation的支持

 

 

1、注解式的SQL定义

除了在mapper文件中配置SQL,Mybatis还支持注解方式的SQL。通过@Select,标注在Mapper接口的方法上。

public interface UserMapper {   

    @Select("select * from user ")

    List<User> AnnotationGetUserList();

}

 

或者你想要的是动态SQL,那么就加上<script>:


 

public interface UserMapper {

    

    @Select("select * from user ")

    List<User> AnnotationGetUserList();

    

    @Select("<script>"

            + "select * from user "

            + "<if test='id!=null'>"

            + "where id=#{id}"

            + "</if>"

            + "</script>")

    List<User> AnnotationGetUserById(@Param("id")String id);

}

 

以上这两种方式都不常用,如果你真的不想用mapper.xml文件来定义SQL,那么以下方式可能适合你。你可以通过@SelectProvider来声明一个类的方法,此方法负责返回一个SQL的字符串:

public interface UserMapper {

    @SelectProvider(type=SqlProvider.class,method="getUserById")

    List<User> AnnotationProviderGetUserById(String id);

}

 

types指定了类的Class,method就是类的方法。其实这种方式也很不错,动态SQL的生成不仅仅依靠Mybatis的动态标签,在程序中可以随便搞:

public class SqlProvider {

    public String getUserById(String id) {

        String sql = "select * from user ";

        if (id!=null) {

            sql += "    where id="+id;

        }

        return sql;

}


 

注解先通过getSqlSourceFromAnnotations()方法拿到注解上的值解析成SqlSource对象,然后也是生成MappedStatement对象,然后调用configuration.addMappedStatement(statement)方法注册到configuration中,这个流程是与XML的方式一样,不会变的。

 

 


 

 

五、SQL语句的执行过程

 

1. SqlSessionFactory 与 SqlSession.

  通过前面的章节对于mybatis 的介绍及使用,大家都能体会到SqlSession的重要性了吧, 没错,从表面上来看,咱们都是通过SqlSession去执行sql语句(注意:是从表面看,实际的待会儿就会讲)。那么咱们就先看看是怎么获取SqlSession的吧:

 

 

(1)首先,SqlSessionFactoryBuilder去读取mybatis的配置文件,然后build一个DefaultSqlSessionFactory。

 

(2)当我们获取到SqlSessionFactory之后,就可以通过SqlSessionFactory去获取SqlSession对象。源码如下:

/**

   * 通常一系列openSession方法最终都会调用本方法

   * @param execType

   * @param level

   * @param autoCommit

   * @return

   */

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {

    Transaction tx = null;

    try {

      //通过Confuguration对象去获取Mybatis相关配置信息, Environment对象包含了数据源和事务的配置

      final Environment environment = configuration.getEnvironment();

      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);

      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);

      //之前说了,从表面上来看,咱们是用sqlSession在执行sql语句, 实际呢,其实是通过excutor执行, excutor是对于Statement的封装

      final Executor executor = configuration.newExecutor(tx, execType);

      //关键看这儿,创建了一个DefaultSqlSession对象

      return new DefaultSqlSession(configuration, executor, autoCommit);

    } catch (Exception e) {

      closeTransaction(tx); // may have fetched a connection so lets call close()

      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);

    } finally {

      ErrorContext.instance().reset();

    }

  }

通过以上步骤,咱们已经得到SqlSession对象了。接下来,当然是执行sql语句咯。

SqlSession咱们也拿到了,咱们可以调用SqlSession中一系列的select...,  insert..., update..., delete...方法轻松自如的进行CRUD操作了。

 

(3) 我们在使用的时候,还要通过sqlSession的getMapper()方法获得一个Mapper接口,然后再调用这个接口的crud方法:

@Test

public void testInterface() {

    SqlSessionFactory sqlSessionFactory = SqlSessionFactoryUtil.getSqlSessionFactory();

    SqlSession session = sqlSessionFactory.openSession();

    //可以直接映射到在命名空间中同名的 Mapper 类 (只要映射接口类就可以了,不需要实现)

    UpmsUserDao upmsUserDao = session.getMapper(UpmsUserDao.class);

    List<UpmsUser> upmsUsers = upmsUserDao.selectUser();

    for (UpmsUser upmsUser : upmsUsers) {

        System.out.println(upmsUser.toString());

    }

}

 

这个getMapper()方法的执行流程其实是这样的:

DefaultSqlSession的getMapper()方法其实调用了Configuraiton类的getMapper()方法:

然后Configuration的内部又调用了MapperRegistry类的getMapper()方法:

然后MapperRegistry的内部其实是实例化了一个MapperProxyFactory对象:

MapperProxyFactory:

  @SuppressWarnings("unchecked")

  protected T newInstance(MapperProxy<T> mapperProxy) {

    //动态代理我们写的dao接口

    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);

  }

  

  public T newInstance(SqlSession sqlSession) {

    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);

    return newInstance(mapperProxy);

  }

 

所以SqlSession的getMapper()就是通过生成的这个动态代理MapperProxy对象,调用我们在Mapper接口中定义的方法了。

 

那么调用了接口之后,怎么具体执行sql语句的呢?

 

(4)执行Sql语句

上面,咱们拿到了MapperProxy, 每个MapperProxy对应一个dao接口, 那么咱们在使用的时候,MapperProxy是怎么做的呢?源码:

/**

   * MapperProxy在执行时会触发此方法

   */

  @Override

  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

    if (Object.class.equals(method.getDeclaringClass())) {

      //并不是任何一个方法都需要执行调用代理对象进行执行,如果这个方法是Object中通用的方法(toString、hashCode等)无需执行

      try {

        return method.invoke(this, args);

      } catch (Throwable t) {

        throw ExceptionUtil.unwrapThrowable(t);

      }

    }

    //如果是自己这个类定义的方法,二话不说,主要交给MapperMethod自己去管

    final MapperMethod mapperMethod = cachedMapperMethod(method);

    return mapperMethod.execute(sqlSession, args);

  }

也就是动态代理的invoke方法,内部又是交给了MapperMethod的execute()方法去执行:

/**

   * 看着代码不少,不过其实就是先判断CRUD类型,然后根据类型去选择到底执行sqlSession中的哪个方法,绕了一圈,又转回sqlSession了

   * @param sqlSession

   * @param args

   * @return

   */

  public Object execute(SqlSession sqlSession, Object[] args) {

    Object result;

    if (SqlCommandType.INSERT == command.getType()) {

      Object param = method.convertArgsToSqlCommandParam(args);

      result = rowCountResult(sqlSession.insert(command.getName(), param));

    } else if (SqlCommandType.UPDATE == command.getType()) {

      Object param = method.convertArgsToSqlCommandParam(args);

      result = rowCountResult(sqlSession.update(command.getName(), param));

    } else if (SqlCommandType.DELETE == command.getType()) {

      Object param = method.convertArgsToSqlCommandParam(args);

      result = rowCountResult(sqlSession.delete(command.getName(), param));

    } else if (SqlCommandType.SELECT == command.getType()) {

      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 {

        Object param = method.convertArgsToSqlCommandParam(args);

        result = sqlSession.selectOne(command.getName(), param);

      }

    } else {

      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;

  }

既然上面的这些crud方法又回到SqlSession里面执行了, 那么咱们就看看SqlSession的CRUD方法了,为了省事,还是就选择其中的一个方法来做分析吧。这儿,咱们选择了selectList方法:

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {

    try {

      MappedStatement ms = configuration.getMappedStatement(statement);

      //CRUD实际上是交给Excetor去处理, excutor其实也只是穿了个马甲而已,小样,别以为穿个马甲我就不认识你嘞!

      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();

    }

  }

这时候可以看到咱们之前解析每条sql语句对应的MappedStatement对象就排上用场了。

 

其实是执行了executor的query()方法,然后,通过一层一层的调用,最终会来到doQuery方法, 这儿咱们就随便找个Excutor看看doQuery方法的实现吧,我这儿选择了SimpleExecutor:

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 handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);

      stmt = prepareStatement(handler, ms.getStatementLog());

      //StatementHandler封装了Statement, 让 StatementHandler 去处理

      return handler.<E>query(stmt, resultHandler);

    } finally {

      closeStatement(stmt);

    }

  }

返回结果是交给handler.<E>query(stmt, resultHandler)去处理的,那继续看看StatementHandler 的一个实现类 PreparedStatementHandler(这也是我们最常用的,封装的是PreparedStatement), 看看它使怎么去处理的:

public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {

     //到此,原形毕露, PreparedStatement, 这个大家都已经滚瓜烂熟了吧

    PreparedStatement ps = (PreparedStatement) statement;

    ps.execute();

    //结果交给了ResultSetHandler 去处理

    return resultSetHandler.<E> handleResultSets(ps);

}

 

PreparedStatement是jdbc自带的一个类,执行完它的execute之后,把结果交给resultSetHandler.<E> handleResultSets(ps);去处理,我们看一下DefaultResultSetHandler这个具体的实现类,它的handleResultSets()方法就是以往我们自己写的那套处理ps结果的代码:

 

 

 

 


 

 

 

Configuration类:

Configuration,你可以把它当成一个数据的大管家。MyBatis所有的配置信息都维持在Configuration对象之中,基本每个对象都会持有它的引用。下面是部分属性:

比如mapper_id 和 mapper文件的映射,mapper_id和返回值的映射,mapper_id和参数的映射。还有mapper注册表等等

public class Configuration {

    //环境

    protected Environment environment;

    protected boolean safeRowBoundsEnabled;

    protected boolean safeResultHandlerEnabled = true;

    protected boolean mapUnderscoreToCamelCase;

    protected boolean aggressiveLazyLoading;

    protected boolean multipleResultSetsEnabled = true;

    protected boolean useGeneratedKeys;

    protected boolean useColumnLabel = true;

    protected boolean cacheEnabled = true;

    protected boolean callSettersOnNulls;

    protected boolean useActualParamName = true;

    protected boolean returnInstanceForEmptyRow;

    

    //日志信息的前缀

    protected String logPrefix;

    

    //日志接口

    protected Class<? extends Log> logImpl;

    

    //文件系统接口

    protected Class<? extends VFS> vfsImpl;

    

    //本地Session范围

    protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;

    

    //数据库类型

    protected JdbcType jdbcTypeForNull = JdbcType.OTHER;

    

    //延迟加载的方法

    protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(

            Arrays.asList(new String[] { "equals", "clone", "hashCode", "toString" }));

    

    //默认执行语句超时

    protected Integer defaultStatementTimeout;

    

    //默认的执行器

    protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;

    

    //数据库ID

    protected String databaseId;

    

    //mapper注册表

    protected final MapperRegistry mapperRegistry = new MapperRegistry(this);

    

    //拦截器链

    protected final InterceptorChain interceptorChain = new InterceptorChain();

    

    //类型处理器

    protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();

    

    //类型别名

    protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();

    

    //语言驱动

    protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();





    //mapper_id 和 mapper文件的映射

    protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>(

            "Mapped Statements collection");

            

    //mapper_id和缓存的映射

    protected final Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");

    

    //mapper_id和返回值的映射

    protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");

    

    //mapper_id和参数的映射

    protected final Map<String, ParameterMap> parameterMaps = new StrictMap<ParameterMap>("Parameter Maps collection");

    

    //资源列表

    protected final Set<String> loadedResources = new HashSet<String>();

    

    .......

}

 

 

总结一下,整个的处理流程:

 

 


 

参考:

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值