13 Nov 2016

深入浅出mybatis(二)操作数据库

深入浅出mybatis(二)操作数据库

目录

欢迎在文章下方评论

mybatis与JDBC

我们知道mybatis可以看成是对jdbc的一种封装。二为什么要封装和封装什么呢?如下

  1. 使用数据库连接池对连接进行管理
  2. SQL语句统一存放到配置文件
  3. SQL语句变量和传入参数的映射以及动态SQL
  4. 动态SQL语句的处理
  5. 对数据库操作结果的映射和结果缓存
  6. SQL语句的重复

在之后我们会一一来聊聊这些封装的具体细节!

Mybatis之SqlSession

谈到mybatis,一开始我们接触的应该就是SqlSession吧。当然,现在我们项目用的比较多的是SqlSessionTemplate(对SqlSession等做了封装)。

  • 首先:SqlSession的作用:
    1. 向SQL语句传入参数
    2. 执行SQL语句
    3. 获取执行SQL语句的结果
    4. 事物的控制
  • 如何得到SqlSession:

分三步: 1. 通过配置文件获取数据库连接相关信息:
Reader reader = Resources.getResourceAsReader("com/yezhihao/www/config/Configuration.xml");
2. 通过配置信息构建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessioFactoryBuilder.build(reader)
3. 通过SqlSessionFactory打开数据库会话
SqlSession sqlSession = sqlSeesionFactory.openSession()

在很多mybatis的初级教程中,上面的都是mybatis入门级代码,但对于我们来说,不能单单限于入门级。所以下面说下其原理。

过程的讲解

  • Reader reader = Resources.getResourceAsReader("com/yezhihao/www/config/Configuration.xml")这句话中就是配置文件的读入,任何框架的初始化,无非是加载自己运行时所需要的配置信息,源码没有聊的。
  • SqlSessionFactory sqlSessionFactory = new SqlSessioFactoryBuilder.build(reader)这句话中,建立了sqlSessionFactory,首先这是一种Builder模式应用,会根据情况提供不同的参数,创建不同的sqlSessionFactory。由于构造时参数不定,可以为其创建一个构造器Builder,将SqlSessionFactory的构建过程和表示分开。

关于Builder模式,推荐大家看《设计模式之禅》

  • SqlSession sqlSession = sqlSeesionFactory.openSession()在这里,我们就跟下源码,看看mybatis是操作数据库是一个怎么样的过程。

跟踪mybatis操作数据库

  1. 首先
 @Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

我们看到是调用openSessionFromDataSource()

  1. 然后,我们来看看openSessionFromDataSource()
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) {
      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)获取前面我们加载配置文件的环境信息,并且获取环境信息中配置的数据源。 (2)通过数据源获取一个连接,对连接进行包装代理(通过JDK的代理来实现日志功能)。 (3)设置连接的事务信息(是否自动提交、事务级别),从配置环境中获取事务工厂,事务工厂获取一个新的事务。 (4)传入事务对象获取一个新的执行器,并传入执行器、配置信息等获取一个执行会话对象。

说白了就是加载对应数据源,创建执行器。

  1. 我们继续跟踪sqlsession执行sql,以UserInfo user = (UserInfo) session.selectOne("User.selectUser", "1"); 为例。

进入源码,我们发现在sqlsession这个接口有三个实现类分别是DefaultSqlSession,SqlSessionManager,SqlSessionTemplate(之后会讲)。这里,我们能看到DefaultSqlSession里面有各种各样的SQL执行方法,主要用于SQL操作的对外接口,它会的调用执行器来执行实际的SQL语句。

进入源码,我们发现

 @Override
  public <T> T selectOne(String statement, Object parameter) {
    // Popular vote was to return null on 0 results and throw exception on too many.
    List<T> list = this.<T>selectList(statement, parameter);
    if (list.size() == 1) {
      return list.get(0);
    } else if (list.size() > 1) {
      throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
      return null;
    }
  }

调用的是本类的selectList()方法 看看:

 @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      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的ID到配置信息中找对应的MappedStatement,在之前配置被加载初始化的时候我们看到了系统会把配置文件中的SQL块解析并放到一个MappedStatement里面,并将MappedStatement对象放到一个Map里面进行存放,Map的key值是该SQL块的ID。 调用执行器的query方法,传入MappedStatement对象、SQL参数对象、范围对象(此处为空)和结果处理方式。

  1. 再然后,我们看看query()方法的执行

    对我来说,mybatis的源码不是一般的复杂(哭。。。)找了好久,

首先在BaseExecutor,找到相应query()方法,

@SuppressWarnings("unchecked")
  @Override
  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;
  }

在这里,mybatis先会看看在其缓存中有没有相应的数据,如果有就返回,没有的话,通过调用queryFromDatabase(),很明显,命名上就是从数据库查找。进去看下,发现是通过调用doQuery()来最终执行sql看下doquery()

  @Override
  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());
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

最后,在PreparedStatementHandler()中找到了query

 @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.<E> handleResultSets(ps);
  }

终于,我们发现了在jdbc中熟悉的代码,在这里,执行了直接操作数据库的语句。

在跟源码的时候发现,mybatis真的封装的很好,看看能不能出个mybatis的整体架构的博文,努力。

Mybatis之SqlSession template

  SqlSessionTemplate是MyBatis-Spring的核心。这个类负责管理MyBatis的SqlSession,调用MyBatis的SQL方法。SqlSessionTemplate是线程安全的,可以被多个DAO所共享使用。   我们在结合spring的时候,我们会发现SqlSessionTemplate是单例的。通过源码我们何以看到 SqlSessionTemplate 实现了SqlSession接口,也就是说我们可以使用SqlSessionTemplate 来代理以往的DefailtSqlSession完成对数据库的操作,但是DefailtSqlSession这个类不是线程安全的,所以这个类不可以被设置成单例模式的。在我们就疑惑了,怎么判断一个类是否为线程安全的。可以移到我的博客线程安全基础

mybatis接口式编程

首先先看下接口式编程的要求

  1. 类的映射:使用接口类的全限定名(包括包名)作为配置文件的namespace,完成类与配置文件的对应关系。
  2. 方法名:接口方法名与配置文件中将要执行的sql语句的id一样,这样就完成了方法调用的映射。
  3. 参数类型:接口方法的输入参数类型和配置文件中sql的parameterType的类型相同
  4. 返回值类型:接口方法的返回值类型和配置文件的resultMap的类型相同

这样就可以对操作数据库了,是不是很神奇呢?我想你一定会想知道它是怎么实现的吧!不急,在下面我们就好好说说它的原理。

接口式编程的要求和规范说完之后,我们就来说下它的原理吧。

接口式编程原理

下面,我们就以下面的代码(没有spring结合)作为入口,讲下源码。

User user = sqlSession.getMapper(User.class);
userList = user.queryUserList();
  1. 首先是getMapper()方法,经过系列的中间类,我们终于在MapperRegistry类中找到真正的getMapper的类。代码如下:
@SuppressWarnings("unchecked")
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

很明显,首先是根据配置和传入接口产生代理工厂,其中mapperProxy是处理类,然后生成一个代理类,然后返回。为什么sqlSession.getMapper(.class)可以根据传入的参数,返回一个对应的类型,因为泛型,mybatis已经为我们完成了强转。

  1. 在mapperProxy处理类中是怎么进行一系列操作的。先看下源码:
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
      try {
        return method.invoke(this, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

上面代码中,首先是无法进入if的,因为没有实现类。然后是调用 final MapperMethod mapperMethod = cachedMapperMethod(method);在这里得到queryUserList()方法的代理并在下一行mapperMethod.execute(sqlSession, args);被调用。

最后:Mybatis加载文件时,利用namespace加载了一个class,然后把这个class与代码中传入接口的class进行匹配,方法执行所需要的信息就是来自于已经匹配成功的配置文件中,当结果与配置文件对应上后,调用接口的方法执行sql语句。在这里没什么好学,都是一些简单业务,就不一一演示了。自己跟下源码吧。MapperMethod->SqlCommand->...更下去自然会发现。


Tags:
Stats:
comments


Share: