带你零基础入门mybatis及原理

132 阅读9分钟

本文正在参加「技术专题19期 漫谈数据库技术」活动, 我是新人loserwang,希望大家多多支持和交流。本专题其他几篇也可以支持一下

基本用法

建立Table和Java类

创建user1表

CREATE TABLE user1(
                      `uid` INT PRIMARY KEY AUTO_INCREMENT,
                      `username` VARCHAR(30) NOT NULL,
                      `password` VARCHAR(30) NOT NULL
);

创建User.java

public class User {
    private int uid;
    private String username;
    private String password;

    public int getUid() {
        return uid;
    }

    public void setUid(int uid) {
        this.uid = uid;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "uid=" + uid +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

引入MyBatis依赖

光引入mybatis还不够,还需要进入Mysql的驱动jar包 mysql-connect-java .

 <dependencies>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.3</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>6.0.6</version>
        </dependency>

    </dependencies>

配置Configuration文件

建立 /resource/db.property  :

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mytest
username=root
password=password

/main/resource 目录下新建mybatis.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <!-- properties配置,用于加载外部的properties配置文件-->
    <properties resource="db.properties"></properties>

    <!-- enviroments 主要进行数据源的配置,
    可以配置多个数据源,通过default属性来指定当前项目运行过程中使用的是哪个数据源
    -->
    <environments default="development">
        <!--environment用于配置一个具体的独立的数据源,id属性用于给当前数据源定义一个名称,
                                            方便我们的项目指定
                -->
        <environment id="development">
           <!--
                        transactionManager用于配置事务管理,默认情况下使用的是JDBC事务管理
                        -->
            <transactionManager type="JDBC"/>
            <!-- dataSource具体数据源的连接信息,type=POOLED表示使用数据库连接池-->
            <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>

    <!-- 引入 映射配置文件 的路径 -->
    <mappers>
        <mapper resource="mapper/userMapper.xml"/>
    </mappers>

建立Mapper

XML配置

建立 /resource/mapper/userMapper.xml :

<?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">
<!-- namespace 命名空间,主要进行session级别的缓存管理
默认情况下,命名空间的值就是当前操作实体类的全名称  包.类名
-->
<mapper namespace="pojo.User">
    <select id="userlist" resultType="pojo.User">
    select * from user1
  </select>
</mapper>

然后在 mybatis.xml 中加入:

 <mappers>
   <mapper resource="mappers/userMapper.xml"/>
</mappers>

字段名不匹配时

现在将User中的username改为name,与数据库中的username字段不匹配。 通过设置userMapper.xml的resultMap来进行匹配。

<mapper namespace="pojo.User">
    <select id="userlist" resultMap="forUser">
        select * from user1
   </select>
    <!-- 自定义映射关系集合,主要包含对于一些自定义操作的配置,如不一致的属性和字段 -->
    <resultMap id="forUser" type="pojo.User">
        <!-- result配置,主要配置普通属性,culumn表示数据库字段名称, property为实体类的属性名称-->
        <result column="username" property="name"></result>

    </resultMap>
</mapper>

mybatis调用步骤

mybatis使用步骤:

  •  1. resource: mybatis-config.xml
  •  2. InputStream
  •  3. SqlSessionFactory
    1. SqlSession
    1. 执行我们配置好的 SQL语句

建立 /test/TestDemo.java :

public class TestDemo {
    @Test
    public void testDemo1() throws IOException {
        //初始化mybatis配置环境
        String resouce = "mybatis.xml";
        InputStream is = Resources.getResourceAsStream(resouce);
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);

        //打开和数据库之间的会话
        SqlSession session = factory.openSession();
        List<User> userList = session.selectList("userlist");
        for(User user:userList){
            System.out.println(user);
        }
        session.close();
    }
}

创建Util工具类

public class SqlSessionFactoryUtils {
    private static String RESOURCE = "mybatis-config.xml";
    //采用statis保证只有一个
    private static SqlSessionFactory sqlSessionFactory;
    private  static  ThreadLocal<SqlSession> threadLocal = new ThreadLocal<SqlSession>();

    /**
     * 创建一个初始化SqlSessionFactory的方法
     */
    public static void initSqlSessionFactory(){
        try {
            InputStream is = Resources.getResourceAsStream(RESOURCE);
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
        }catch (Exception e){
            e.printStackTrace();
        }

    }

    /**
     * 获取工厂对象
     * @return
     */
    public static SqlSessionFactory getSqlSessionFactory(){
        return  sqlSessionFactory;
    }

    public static void close(){
        SqlSession session = threadLocal.get();
        if(session != null){
            session.close();
            threadLocal.set(null);
        }
    }

}

在web项目中使用

启动时

在web容器启动时,通过监听器使用。

@WebListener
public class InitSqlSessionListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        //初始话我们的sqlSessionFactory对象
        SqlSessionFactoryUtils.initSqlSessionFactory();
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
        //关闭salSession对象
        SqlSessionFactoryUtils.close();
    }
}

Dao层

SqlSession生命周期:c.biancheng.net/view/4320.h…

public class UsersDao {
    private SqlSession session;
    private List<User>  list;

    private SqlSession getSession(){
          sqlSession = SqlSessionFactoryUtils.getSqlSessionFactory().openSession();
           return sqlSession
    }
     //返回所有用户,最好每个请求打开再关闭SqlSession
    public List<User> findAll(){
        list  = getSession().selectList("findAll");
        sqlSession.close();
        return list;
    }
    //根据id查询
    public User findById(Integer id){
        User user = getSession().selectOne("findById", id);
        sqlSession.close();
        return user;
    }

}

userMapper.xml中加上id为findAll的映射:

<mapper namespace="pojo.User">
    <select id="findAll" resultType="pojo.User">
        select * from user1
  </select>
   <select id="findById" resultType="pojo.User">
        select * from user1 where id = #{id}
  </select>
</mapper>

Servlet

public class UserFindServlet extends HttpServlet {
    private UsersDao usersDao = new UsersDao();


    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        List<User> list = usersDao.findAll();
        //dosomething
    }
} 

Dao层采用接口

www.cnblogs.com/lixuwu/p/59… 在Mybatis中,映射文件中的**namespace**是用于绑定Dao接口的,即面向接口编程。当你的namespace绑定接口后,就可以不用写接口实现类,Mybatis会通过该绑定自动找到对应要执行的SQL语句。

dao层:

@Mapper
public interface UserDaoMapper {
    public List<User> findAll();
    //根据id查询
    public User findById(Integer id);
}

在 Spring 程序中,Mybatis 需要找到对应的 mapper,在编译的时候动态生成代理类,实现数据库查询功能,所以我们需要在接口上添加 @Mapper  注解

mybatis-spring

mybatis.org/spring/zh/i…

使用思路

传统的 mybatis

mybatis.png

  1. mybatis-config.xml 中配置 <mapper></mapper> , 或者指定外部mapper的位置。
  2. 我们需要使用数据库操作时,通过 mybatis-config.xml  元文件配置产生 sqlSessionFactory , 然后sqlSessionFactory产生 SqlSession 。
  3. sqlSession根据 selectOne 、 selectAll 等方法和id找到对应的mapper,然后进行数据库操作。(一般为了将mybatis操作包装到 DaoImpl 类(实现Dao 接口),这一层+mapper文件 可以合称为** dao** 层 )

mybatis-spring

mybatis2.png 在mybatis-spring中,我们无需配置 mybatis-config.xml 文件,只需要在 SqlSessionFactoryBean 的 mapperLocations  属性值指定为mapper文件夹 。当我们通过 SqlSessionTemplate 进行数据库操作时,会自动从mapper文件夹中的所有xml文件寻找相应的匹配项。(在mybatis-config.xml 中需要分别指定每一个mapper文件)。

mybatis-spring注入器

与其在数据访问对象(DAO)中手工编写使用 SqlSessionDaoSupport 或 SqlSessionTemplate 的代码,还不如让 Mybatis-Spring 为你创建一个线程安全的映射器,这样你就可以直接注入到其它的 bean 中了。 mybatis3.png 映射器注册:

<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
  <property name="mapperInterface" value="org.mybatis.spring.sample.mapper.UserMapper" />
  <property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>

在service层使用:

public class FooServiceImpl implements FooService {

  private final UserMapper userMapper;

  public FooServiceImpl(UserMapper userMapper) {
    this.userMapper = userMapper;
  }

  public User doSomeBusinessStuff(String userId) {
    return this.userMapper.getUser(userId);
  }
}
<bean id="fooService" class="org.mybatis.spring.sample.service.FooServiceImpl">
  <constructor-arg ref="userMapper" />
</bean>

自动加载映射器

不需要一个个地注册你的所有映射器。你可以让 MyBatis-Spring 对类路径进行扫描来发现它们。

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
  <property name="basePackage" value="org.mybatis.spring.sample.mapper" />
</bean>

然后不需要再配置 MapperFactoryBean .

配置文件示例

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
     <!-- 引入外部配置文件-->
    <context:property-placeholder location="classpath:db.properties"></context:property-placeholder>
    <!-- 配置dataSource -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="${db.classdriver}"/>
        <property name="url" value="${db.url}"/>
        <property name="username" value="${db.username}" />
        <property name="password" value="${db.password}" />
     </bean>

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="mapperLocations" value="classpath*:mappers/*.xml">
        </property>
        <property name="typeAliasesPackage" value="com.isea533.mybatis.model"/>
        <property name="plugins"> <!--插件 -->
            <array>
                <bean class="com.mmall.intercept.FirstIntercept"></bean>
            </array>
        </property>
    </bean> 

    <!-- 注册映射器 -->
    <bean  class="org.mybatis.spring.mapper.MapperScannerConfigurer" >
        <property name="basePackage" value="com.mmall.dao"/>
    </bean>

    <!-- 自动扫描 Component -->
    <context:component-scan base-package="com.mmall"/>
    <!-- AOP -->
    <aop:aspectj-autoproxy/>
</beans>

MyBatis-genetator

Introduction to MyBatis Generator

MyBatis Generator (MBG) is a code generator for MyBatis MyBatis. It will generate code for all versions of MyBatis.

MyBatis Generator 是一个代码生成器。

It will introspect a database table (or many tables) and will generate artifacts that can be used to access the table(s).

它将对一个数据库表(或多个表)进行内部检查,并将生成可用于访问表的工件。

This lessens the initial nuisance of setting up objects and configuration files to interact with database tables.

这减少了设置对象和配置文件以与数据库表进行交互的最初麻烦。

MBG seeks to make a major impact on the large percentage of database operations that are simple CRUD (Create, Retrieve, Update, Delete). You will still need to hand code SQL and objects for join queries, or stored procedures.

MBG试图对简单CRUD(创建,检索,更新,删除)的大部分数据库操作产生重大影响。您仍将需要手工编写SQL和对象代码以进行联接查询或存储过程。

MyBatis-generator基本可以满足绝大多数场景下的开发。但是其操作仅仅为简单的一一映射关系,将一个表和一个POJO进行映射。但是如果要完成某些复杂的工作,如一对多、多对一、多对多,再比如某些元素的屏蔽、将某些数据的更新交给数据库(如DateTime),可以自己再修改generate的XML文件。所以不要放松对MyBatis的学习。

添加依赖

<!-- pom.xml中添加mybatis-generator -->
<dependency>
    <groupId>org.mybatis.generator</groupId>
    <artifactId>mybatis-generator-core</artifactId>
    <version>1.3.5</version>
</dependency>


<!-- 当然也要引入mysql-connector-java,要使mysql-connector-java的版本和本地的mysql版本能够对应-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
     <version>${mysql.version}</version>
</dependency>

添加插件

<plugin>
    <groupId>org.mybatis.generator</groupId>
    <artifactId>mybatis-generator-maven-plugin</artifactId>
     <version>1.3.5</version>
     <configuration>
         <!--如果不配置configurationFile,默认为 resources/generatorConfig.xml-->
         <configurationFile>./src/main/resources/generatorConfig.xml</configurationFile>
         <verbose>true</verbose>
         <overwrite>true</overwrite>
      </configuration>
 </plugin>

创建generatorConfig.xml文件

jdbc驱动jar的位置 使用绝对位置即可,因为mybatis-generator只有在项目开始需要使用,之后直接上传生成的文件即可。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <!-- 引入外部配置文件, 可以使用 url 或者 resource,
    resource直接从classpath文件夹开始找, url从整个磁盘找
    -->
    <properties resource="db.properties" ></properties>

    <!-- 指定特定数据库的jdbc驱动jar包的位置-->
    <classPathEntry location="C:\Users\loserwang\.m2\repository\mysql\mysql-connector-java\8.0.14\mysql-connector-java-8.0.14.jar"></classPathEntry>


    <context id="default" targetRuntime="MyBatis3">

        <!-- jdbc的数据库连接 -->
        <jdbcConnection driverClass="${db.classdriver}"
                        connectionURL="${db.url}"
                        userId="${db.username}"
                        password="${db.password}">
        </jdbcConnection>

        <!-- 非必需,类型处理器,在数据库类型和java类型之间的转换控制-->
        <javaTypeResolver >
            <property name="forceBigDecimals" value="false" />
        </javaTypeResolver>

        <!-- Model模型生成器,用来生成包含主键key的类,记录类,以及查询Example类
            targetPackage   指定生成的model生成所在的包名
            targetProject  指定在该项目下所在的路径
        -->
        <javaModelGenerator targetPackage="com.mmall.pojo" targetProject="./src/main/java">
            <property name="enableSubPackages" value="false" />
            <property name="trimStrings" value="true" />
            <property name="immutable" value="false" />
        </javaModelGenerator>

        <!-- mapper 映射文件生成所在的目录,为每一个数据库的表生成对应的 SqlMap文件-->
        <sqlMapGenerator targetPackage="mappers"  targetProject="./src/main/resources">
            <property name="enableSubPackages" value="false" />
        </sqlMapGenerator>

        <!--客户端代码,生成易于使用的针对Model对象和XML配置文件的 代码
                type="ANNOTATEDAMAPPER",生成Java Model 和基于注解的Mapper对象,
                type="MIMEDMAPPER",生成基于注解Java Model 和相应的Mapper对象,
                type="XMLMAPPER",生成SQLMap XML文件和独立的Mapper接口,
        -->
        <!-- targetPackage: mapper接口dao生成的位置-->
        <javaClientGenerator type="XMLMAPPER" targetPackage="com.mmall.dao"  targetProject="./src/main/java">
            <property name="enableSubPackages" value="false" />
        </javaClientGenerator>

        <!--将表和pojo联系起来-->
        <table tableName="cake" domainObjectName="Cake"></table>
        <table tableName="users" domainObjectName="User">
        <!--
            <columnOverride column="detail" jdbcType="VARCHAR" />
            <columnOverride column="sub_images" jdbcType="VARCHAR" />
        -->
        </table>
    </context>
</generatorConfiguration>
# resource/db.properties
db.classdriver=com.mysql.jdbc.Driver
#GMT%2B8中的 %2B 为URL编码
db.url=jdbc:mysql://localhost:3306/mall?characterEncoding=utf-8&serverTimezone=GMT%2B8&useSSL=false
db.username=root
db.password=root

运行mybatis-generator插件

idea右侧maven标签中的Plugins中运行 mybatis-generator:genetator  即可生成相应文件。

mybatis插件

知识点

具体知识点看: www.cnblogs.com/chenpi/p/10…

例子

 <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="mapperLocations" value="classpath*:mappers/*.xml">
        </property>
        <property name="typeAliasesPackage" value="com.isea533.mybatis.model"/>
        <property name="plugins">
            <array>
                <bean class="com.mmall.intercept.FirstIntercept"></bean>
            </array>
        </property>
 </bean>
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class,Integer.class})})
public class FirstIntercept implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        bebofe(invocation);
        Object object = invocation.proceed();
        after(invocation);
        return object;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // do noting
    }
    private void bebofe(Invocation invocation){
        System.out.println("-------before--------");
        System.out.println(invocation.getTarget());
    }
    private void after(Invocation invocation){
        System.out.println("-------after----------");
    }
}

Mybatis源码(简易过一下流程)

1. 下载源码并拉取依赖

  • 下载mybatis源码

    > git clone https://github.com/mybatis/mybatis-3.git
    

    查看mybatis3pom.xml文件,发现父项目为 mybatis-parent:

    <parent>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-parent</artifactId>
      <version>33</version>
      <relativePath />
    </parent>
    
  • 下载mybatis-parent源码

 > git clone https://github.com/mybatis/parent
 > ll 
total 0
drwxr-xr-x  19 loserwang  staff   608B  8 11 17:53 mybatis-3
drwxr-xr-x  16 loserwang  staff   512B  8 11 17:59 parent
  • 编译 parent 项目, 为mybatis提供依赖

    cd parent
    mvn clean install -Dmaven.test.skip
    
  • 编译 mybatis 项目, 将pom.xml中的parent改成上面mybatis-parents包的版本

    cd mybatis
    mvn clean install -Dmaven.test.skip
    

编译成功,可以进行debug操作。

2. 建立项目

xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <!-- properties配置,用于加载外部的properties配置文件-->
    <properties resource="db.properties"></properties>

    <!-- enviroments 主要进行数据源的配置,
    可以配置多个数据源,通过default属性来指定当前项目运行过程中使用的是哪个数据源
    -->
    <environments default="development">
        <!--environment用于配置一个具体的独立的数据源,id属性用于给当前数据源定义一个名称,
                                            方便我们的项目指定
                -->
        <environment id="development">
           <!--
                        transactionManager用于配置事务管理,默认情况下使用的是JDBC事务管理
                        -->
            <transactionManager type="JDBC"/>
            <!-- dataSource具体数据源的连接信息,type=POOLED表示使用数据库连接池-->
            <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>

    <!-- 引入 映射配置文件 的路径 -->
    <mappers>
        <mapper namespace="pojo.User">
                <select id="userlist" resultType="pojo.User">
                    select * from user1
                  </select>
                </mapper>
    </mappers>

  </configuration>

java

public class TestDemo {
    @Test
    public void testDemo1() throws IOException {
        //1. mybatis配置文件
        String resouce = "mybatis.xml";

        //2. 生成SqlSessionFactory工厂类
        InputStream is = Resources.getResourceAsStream(resouce);
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);

        //3. 工厂类生成 与数据库交互 的session
        SqlSession session = factory.openSession();

        //4. 通过session 增删改查 数据
        List<User> userList = session.selectList("userlist");
        for(User user:userList){
            System.out.println(user);
        }
        session.close();
    }
}
  1. 入口为new SqlSessionFactoryBuilder().build(is), 解析传入的配置文件,生成SqlSessionFactory
  2. 通过SqlSessionFactory生成SqlSession
  3. 通过 SqlSession 进行相应的数据库操作。

Untitled Diagram.png

3. new SqlSessionFactoryBuilder().build(is)

先从入口new SqlSessionFactoryBuilder().build(is)开始看:

// XMLConfigBuilder.java
public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
 }

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      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.
      }
    }
  }

 public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

//DefaultSqlSessionFactory.java
 public DefaultSqlSessionFactory(Configuration configuration) {
    this.configuration = configuration;
  }

XMLConfigBuilder将传入的xml输入流解析为Configuration(包含各类配置信息)。然后将Configuration传入工厂类。

接下来看看是如何解析的:

public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
 }


private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    super(new Configuration());
    ErrorContext.instance().resource("SQL Mapper Configuration");
    this.configuration.setVariables(props);
    this.parsed = false;
    this.environment = environment;
    this.parser = parser;
  }

public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      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);
    }
  }
  1. XPathParser将XML文件解析成树,结点为XNode

  2. 依次遍历propertiessettingstypeAliasespluginsobjectFactoryobjectWrapperFactoryreflectorFactoryenvironmentsdatabaseIdProvidertypeHandlersmappers,设置 **configuration**

    Configuration内部的数据较多,等到用到时,再看如何具体设置。

4. factory.openSession()

查看 SqlSession session = factory.openSession():

// DefaultSqlSessionFactory.java
  public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
  }

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
       //从configuration中获取环境
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
       //根据DataSource、autoCommit、隔离等级来获取事务
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
       // 生成executor,对数据库进行操作
       final Executor executor = configuration.newExecutor(tx, execType);
      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();
    }
  }
  1. configuration中获取执行环境Environment, Environment中有事务工厂TransactionFactory和数据源DataSource
  2. TransactionFactory根据DataSource、autoCommit、隔离等级来生成事务Transaction。
  3. 工厂方法生成executor: 参数为 事务Transaction、执行类型execType。executor封装了对数据库操作的方法。
  4. 生成DefaultSqlSession : 参数为 configuration,executor,autoCommit。

Environment

Environment包含了数据库的相关信息, 代码如下:

public final class Environment {
  private final String id;
  private final TransactionFactory transactionFactory;
  private final DataSource dataSource;
  // Getter and Setter
  // ......

  // 内部构造器,用来生产 Environment
  public static class Builder {
    private final String id;
    private TransactionFactory transactionFactory;
    private DataSource dataSource;

    public Builder(String id) {
      this.id = id;
    }

    public Builder transactionFactory(TransactionFactory transactionFactory) {
      this.transactionFactory = transactionFactory;
      return this;
    }

    public Builder dataSource(DataSource dataSource) {
      this.dataSource = dataSource;
      return this;
    }

    public String id() {
      return this.id;
    }

    public Environment build() {
      return new Environment(this.id, this.transactionFactory, this.dataSource);
    }

  }
}

可以看到,Environment内部有个构造器Builder,来生成Environment。

对应XML配置如下:

 <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>

接下来,我们回到XMLConfigBuilder来看,在解析XML文件时,如何生成Environment

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
      if (environment == null) {
        environment = context.getStringAttribute("default");
      }

      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
        if (isSpecifiedEnvironment(id)) {
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
          DataSource dataSource = dsFactory.getDataSource();
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
          configuration.setEnvironment(environmentBuilder.build());
          break;
        }
      }
    }
  }


public String getStringAttribute(String name, String def) {
    String value = attributes.getProperty(name);
    return value == null ? def : value;
  }



private boolean isSpecifiedEnvironment(String id) {
    if (environment == null) {
      throw new BuilderException("No environment specified.");
    }
    if (id == null) {
      throw new BuilderException("Environment requires an id attribute.");
    }
    return environment.equals(id);
  }

TransactionFactory和DataSource以后再分析

Executor

Executor接口定义了对数据库的操作,代码如下:

public interface Executor {

  ResultHandler NO_RESULT_HANDLER = null;

  int update(MappedStatement ms, Object parameter) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

  <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;

  List<BatchResult> flushStatements() throws SQLException;

  void commit(boolean required) throws SQLException;

  void rollback(boolean required) throws SQLException;

  CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

  boolean isCached(MappedStatement ms, CacheKey key);

  void clearLocalCache();

  void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);

  Transaction getTransaction();

  void close(boolean forceRollback);

  boolean isClosed();

  void setExecutorWrapper(Executor executor);

}

可以看出, Executor接口定义了一系列对数据库进行操作的方法。

Executor相关的工厂方法如下:

// Configuration.java
public Executor newExecutor(Transaction transaction) {
    return newExecutor(transaction, defaultExecutorType);
}

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;

    //根据不同的执行类型,生成不同种类的线程池
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

BatchExecutor、ReuseExecutor、SimpleExecutor、CachingExecutor代码分析待定

5. session.selectList("userlist")

查看List<User> userList = session.selectList("userlist"):

//DefaultSqlSession.java
public <E> List<E> selectList(String statement) {
    return this.selectList(statement, null);
  }

  public <E> List<E> selectList(String statement, Object parameter) {
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
  }

  private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
  1. 根据xml文件中配置的mapper的id名,从**configuration**中获取相应的MappedStatement(封装了对数据库操作的相关配置数据)
  2. 调用**executor**对数据库进行查询操作。

Untitled Diagram.png

MappedStatement

MappedStatement包含对数据库操作的相关数据:

//MappedStatement.java
public final class MappedStatement {

  private String resource;
  private Configuration configuration;
  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;

  MappedStatement() {
    // constructor disabled
  }

  public static class Builder {
   ......
  }
  // Getter and Setter

}

可以看到,MappedStatement内部也有个构造器Builder,来生成MappedStatement。

对应的XML如下:

 <mappers>
        <mapper namespace="pojo.User">
                <select id="userlist" resultType="pojo.User">
                    select * from user1
                  </select>
                </mapper>
    </mappers>

接下来,我们回到XMLConfigBuilder来看,在解析XML文件时,如何生成MappedStatement

// XMLConfigBuilder.java 
// 解析Mappers,获取所有Mapper
private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        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");
           //获取外部mappers
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
              //解析mapper
              XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
              mapperParser.parse();
            }
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            try(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<?> 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.java
// 解析各Mapper
private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.isEmpty()) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      // 生产StateMent
      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);
    }
  }

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

executor.query

//Executor.java 
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;
  }

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);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

query是一个模板方法,调用了doQuery抽象方法,具体的实现由子类实现。

SimpleExecutor为例:

//SimpleExecutor.java
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        Statement stmt = null;

        List var9;
        try {
            Configuration configuration = ms.getConfiguration();
            StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
            stmt = this.prepareStatement(handler, ms.getStatementLog());
            var9 = handler.query(stmt, resultHandler);
        } finally {
            this.closeStatement(stmt);
        }

        return var9;
    }


//熟悉的JDBC
 private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
        Connection connection = this.getConnection(statementLog);
        Statement stmt = handler.prepare(connection);
        handler.parameterize(stmt);
        return stmt;
    }


//SimpleStatementHandler.java
 public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
        String sql = this.boundSql.getSql();
        statement.execute(sql);
        return this.resultSetHandler.handleResultSets(statement);
 }

本博客草草过了一遍流程,具体细节以后再梳理。