MyBatis 4大核心对象

1,303 阅读7分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动 利用MyBatis来完成一次数据库操作需要经过以下步骤:

/**
 * 查询所有学生信息
 */
@Test
public void queryAllStudentTest() throws IOException {
    String res = "Mybatis-Config.xml";
    // 读取 mybatis-config 配置文件
    InputStream in = Resources.getResourceAsStream(res);
    // 创建 SqlSessionFactory 对象
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
    // 创建 SqlSession 对象
    SqlSession sqlSession = sqlSessionFactory.openSession(true);
    StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
    
    List<Student> studentList = studentMapper.queryAllStudent();
    System.out.println(studentList);
    sqlSession.close();
    }

1、加载配置文件

2、获取 SqlSessionFactoryBuiler 对象

3、通过 SqlSessionFactoryBuiler 和配置文件流来获取 SqlSessionFactory 对象

4、利用 SqlSessionFactory 对象来打开一个 SqlSession

5、通过 SqlSession 来获得对应的Mapper对象

6、通过 Mapper 对象调用对应接口来查询数据库

完成后关闭 SqlSession

从这些步骤我们可以看到,MyBatis完成一次数据库操作主要有4大核心对象:SqlSessionFactoryBuiler,SqlSessionFactory,SqlSession 和 Mapper。

一、SqlSessionFactoryBuilder(构造器)

  • SqlSessionFactoryBuilder 的唯一作用就是用来创建 SqlSessionFactory,创建完成之后就不会用到它了,所以SqlSessionFactoryBuiler生命周期极短。

  • SqlSessionFactoryBuiler中提供了9个方法,返回的都是SqlSessionFactory对象。

  • SqlSessionFactoryBuiler会根据配置或者代码来生成 SqlSessionFactory,采用的是分步构建的 Builder 模式。

(2+1)方法分析

虽然说提供了9个方法,但是实际上我们可以看成(2+1)个方法。前面三个方法最终都调用了第4个方法,所以实际上就是一个方法:

public SqlSessionFactory build(Reader reader) {
    return build(reader, null, null);
}

public SqlSessionFactory build(Reader reader, String environment) {
    return build(reader, environment, null);
}

public SqlSessionFactory build(Reader reader, Properties properties) {
    return build(reader, null, properties);
}

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());   
      ......
  }

在看下面这个其实也是一回事,有三个方法最终也是调用了同一个方法:

public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
}

public SqlSessionFactory build(InputStream inputStream, String environment) {
    return build(inputStream, environment, null);
}

public SqlSessionFactory build(InputStream inputStream, Properties properties) {
    return build(inputStream, null, properties);
}

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    return build(parser.parse());
	......
}

所以这两个方法就对应了2+1方法中的2。下面这个就是2+1中的1:

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

在上面可以看到,这2个方法在将配置文件解析成一个Java配置类Configuration之后,又同时调用了另一个方法build(Configuration config),而这个方法也没做什么事,就是把配置类传进去返回了一个DefaultSqlSessionFactory对象,这个对象就是SqlSessionFactory的实现类.

所以说2+1中的1没什么好分析的,那么这个2其实就是支持了2种读取文件的方式,比如说我们获取一个SqlSessionFactory对象最常用的是通过如下方式:

String res = "Mybatis-Config.xml";
// 读取 mybatis-config 配置文件
InputStream in = Resources.getResourceAsStream(res);
// 创建 SqlSessionFactory 对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);

但是其实我们同样可以采用下面的方式才获取SqlSessionFactory对象:

Reader reader = Resources.getResourceAsReader(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);

这两个方法的唯一区别就是在这里,另外两个参数都是一样的。

  • String environment:

    这个其实就是用来决定加载哪个环境的。 下面就是一个mybatis-config文件,有定义了一个environment id,当我们需要支持多环数据源切换的时候可以定义多个id,然后根据不同环境传入不同id即可。

    <!-- 配置数据库信息 -->
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property value="${jdbc_driver}" name="driver"/>
                <property value="${jdbc_url}" name="url"/>
                <property value="${jdbc_username}" name="username"/>
                <property value="${jdbc_password}" name="password"/>
            </dataSource>
        </environment>
    </environments>
    
  • Properties properties:

  • 上面dataSource内的属性没有直接输入,而是采用了${jdbc.driver}来配置,而这些属性的值我们可以单独维护在一个properties文件内。

jdbc_driver=com.mysql.cj.jdbc.Driver
jdbc_url=jdbc:mysql://localhost:3306/school?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
jdbc_username=root
jdbc_password=xxxxxx

然后代码中就可以这么改写:

Reader reader = Resources.getResourceAsReader(resource);
Properties properties = Resources.getResourceAsProperties("db.properties");
SqlSessionFactory sSFactory = new SqlSessionFactoryBuilder().build(reader,properties);

采用这种方式之后,mybatis-config.xml文件可以直接用${}来取值了。当然其实还有一种更简单的方式,那就是直接在mybatis-config.xml引入properties文件,这样就不用在代码中去解析properties文件了:

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

build()方法的功能就是去读取XML文件,并将读取到的信息封装到Configuration中。

通过上面的分析我们也很容易的知道,既然提供了一个方法可以直接传入Configuration来获取SqlSessionFactory对象,那么其实我们也可以不采用XML形式,转而直接通过Java代码的方式来直接构建Configuration,然后直接获取SqlSessionFactory对象。如下:

Configuration congiguration = new Configuration();
congiguration.setDatabaseId("xxx");
congiguration.setCacheEnabled(false);
SqlSessionFactory sqlSessionFactory1 = new SqlSessionFactoryBuilder().build(congiguration);

当然,这种方式如果要修改的话每次都要改代码,会比较麻烦,而且看起来比较不直观,所以还是推荐使用xml方式来进行MyBatis的基础属性配置。

InputStream和Reader

Reader和InputStream都是Java中I/O库提供的两套读取文件方式。Reader用于读入16位字符,也就是Unicode 编码的字符;而 InputStream 用于读入ASCII字符和二进制数据。

二、SqlSessionFactory(工厂接口)

  • SqlSessionFactory是一个接口,默认的实现类,主要是用来生成SqlSession对象,而SqlSession对象是需要不断被创建的,所以SqlSessionFactory是全局都存在的,也没有必要重复创建,所以这是一个单例对象。

  • SqlSessionFactory看名字就可以很容易想到,用到的是工厂设计模式。

  • SqlSessionFactory只有两个实现类:DefaultSqlSessionFactory和SqlSessionManager。

DefaultSqlSessionFactory

DefaultSqlSessionFactory 类提供了8个方法用来获取SqlSession对象。方法列举如下:

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

这8个方法中TransactionIsolationLevel和autoCommit都比较好理解,就是事务隔离级别和是否开启自动提交。 前面6个方法最终实际上调用了openSessionFromDataSource方法,而后面2个又是调用了openSessionFromConnection方法。

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
        // 获取配置好的环境
        final Environment environment = configuration.getEnvironment();
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        final Executor executor = configuration.newExecutor(tx, execType);
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
        // may have fetched a connection so lets call close()
        closeTransaction(tx); 
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}
private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
    try {
        boolean autoCommit;
        try {
            // 获取事务是否开启自动提交
            autoCommit = connection.getAutoCommit();
        } catch (SQLException e) {
          // Failover to true, as most poor drivers
          // or databases won't support transactions
          autoCommit = true;
      }
      // 获取配置环境
      final Environment environment = configuration.getEnvironment();
      // 获取事务对象
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      final Transaction tx = transactionFactory.newTransaction(connection);
      // 获取执行器
      final Executor executor = configuration.newExecutor(tx, execType);
      // 构造一个SqlSession对象返回
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

从上面的两个方法中很明显可以看到,这两个方法基本一样,唯一的区别是openSessionFromConnection方法是从Connection对象中获取当前会话是否需要开启自动提交。

Executor

MyBatis中最终去执行sql语句是通过Executor对象实现的,Executor只有三种类型:SIMPLE, REUSE, BATCH。

  • 默认使用的是SIMPLE类型对应的SimpleExecutor执行器。SimpleExecutor表示普通的执行器,不会做任何处理。

  • REUSE类型对应的是ReuseExecutor执行器,会复用预处理语句。

  • BATCH类型对应的是BatchExecutor执行器,一般用来执行批量操作。

Executor是属于SqlSession的四大对象之一,后面会有一篇博客来专门分析,在这里我们暂时不深入展开。

SqlSessionManager

SqlSessionManager其实也不是什么新东西,只是同时把SqlSessionFactory和SqlSession的职责放到了一起,所以这个我们不过多的讨论。

public class SqlSessionManager implements SqlSessionFactory, SqlSession {
    private final SqlSessionFactory sqlSessionFactory;
    private final SqlSession sqlSessionProxy;
    .....
}

三、SqlSession(会话)

一个既可以发送 SQL 执行返回结果,也可以获取 Mapper 的接口。在现有的技术中,一般我们会让其在业务逻辑代码中“消失”,而使用的是 MyBatis 提供的 SQL Mapper 接口编程技术,它能提高代码的可读性和可维护性。

SqlSession是用来操作xml文件中我们写好的sql语句,每次操作数据库我们都需要一个SqlSession对象,SqlSession是用来和数据库中的事务进行对接的,所以SqlSession里面是包含了事务隔离级别等信息的。

SqlSession实例是线程不安全的,故最佳的请求范围是请求(request)或者方法(method)。

SqlSession也是一个接口,有两个实现类:DefaultSqlSession和SqlSessionManager。SqlSessionManager上面有提到,这里就不重复了。

四、Mapper(映射器)

Mapper是一个接口,没有任何实现类。主要作用就是用来映射Sql语句的接口,映射器的接口实例从SqlSession对象中获取,所以说Mapper实例作用域是和SqlSession相同或者更小。Mapper实例的作用范围最好是保持在方法范围,否则会难以管理。

Mapper接口的名称要和对应sql语句的xml文件同名,Mapper接口中定义的方法名称对应了xml文件中的语句id。

四大对象生命周期

SqlSessionFactoryBuiler只需要在创建SqlSessionFactory对象的时候使用,创建完成之后即可被丢弃。SqlSessionFactory全局唯一,是一个单例对象,但是需要全局存在。SqlSession一般对应了一个request,Mapper一般控制在方法内。

对象生命周期
SqlSessionFactoryBuiler方法局部(Method)使用完成即可被丢弃
SqlSessionFactory应用级别(Application),全局存在,是一个单例对象
SqlSession请求或方法(Request / Method)
Mapper方法(Method)