在Spring中实现使用mybatis执行原生字符串SQL语句功能

前言

这其实很早之前就应该写一写的,也就是在项目重构过程中,需要将原来的基于Spring JdbcTemplate执行拼接语句的sql写法,移植到mybatis上去。

分析

原来使用的是abstract Dao的方式,在abstract dao中,持有一个JdbcTemplate,然后使用durid连接池。现在需要改实现方式, 也就是使用mybatis的连接池。

实现

熟悉mybatis的同学都应该知道,mybatis在启动的时候,会首先解析mybatis-config.xml文件为Configuration对象,

public class Configuration {

  protected Environment environment;//环境,有三个属性,id(环境id),transactionFactory(有三种,jdbc,managed,springManaged),和dataSource
  protected boolean safeRowBoundsEnabled;// 下面这些开关都是全局开关,而MappedStatement中相同的开关会覆盖这个的开关
  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;
  protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
  protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
  protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));
  protected Integer defaultStatementTimeout;// 超时时间
  protected Integer defaultFetchSize;
  protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;// SIMPLE, REUSE, BATCH三种类型,都是继承于BaseExcutor,实现Excutor接口
  protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;//自动映射列到java实体属性
  protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;

  protected Properties variables = new Properties();
  protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
  protected ObjectFactory objectFactory = new DefaultObjectFactory();
  protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();

  protected boolean lazyLoadingEnabled = false;
  protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL

  protected String databaseId;
  /**
   * Configuration factory class.
   * Used to create Configuration for loading deserialized unread properties.
   *
   * @see <a href='https://code.google.com/p/mybatis/issues/detail?id=300'>Issue 300 (google code)</a>
   */
  protected Class<?> configurationFactory;
  // 存储: Mapper接口-->MapperProxyFactory代理工厂
  protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
  // 拦截器,用于存放插件,例如分页插件
  protected final InterceptorChain interceptorChain = new InterceptorChain();
  // 存储自己注册的和默认的TypeHandler
  protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
  // 类型别名,例如int Integer这样的
  protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
  protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();

  // 这个就厉害了,存储的key是Mapper的id,value是MappedStatement,也就是方法的解析
  protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
      .conflictMessageProducer((savedValue, targetValue) ->
          ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
  protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
  // 也就是xml中的ResultMap,key为namespace+ ResultMap的id,value就是解析出来的内容
  protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
  // 这个也是,xml中的ParameterMap,key为namespace+Parameter的id,value就是解析出来的内容
  protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
  // 存储主键生成器
  protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");
  // 已经解析的Mapper xml路径
  protected final Set<String> loadedResources = new HashSet<>();
  protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");

  protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>();
  protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>();
  protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>();
  protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>();

  /*
   * A map holds cache-ref relationship. The key is the namespace that
   * references a cache bound to another namespace and the value is the
   * namespace which the actual cache is bound to.
   */
  protected final Map<String, String> cacheRefMap = new HashMap<>();
 }

然后解析具体的Mapper xml变成一个个MappedStatement,
一个<select>,<delete>,<update>,<insert>标签,就会被解析为一个个MappedStatement

例如:

<select id="listAll" resultType="com.naison.mybatistest.entity.User" parameterType="int" useCache="true"
            flushCache="true" timeout="10000" fetchSize="1" databaseId="1">
        SELECT * FROM user
</select>
public final class MappedStatement {

  private String resource;//mapper xml文件路径
  private Configuration configuration;// 对外层configuration的引用
  private String id;//<select id = xxx> 就是这个xxx
  private Integer fetchSize;获取行数
  private Integer timeout;//等待数据库执行结果的时间
  private StatementType statementType;// statement<不可设置参数>,preparedStatement<可设置参数>,或者callableStatement<存储过程>
  private ResultSetType resultSetType;
  private SqlSource sqlSource;// 就是sql语句了,解析为一个对象
  private Cache cache;//二级缓存,也就是这个方法的缓存值
  private ParameterMap parameterMap;//xml中的ParameterMap,在外层configuration中有存
  private List<ResultMap> resultMaps;//xml中的ResultMap,在外层configuration中有存
  private boolean flushCacheRequired;//是否需要刷新缓存,如果是true,每次调用方法都会刷新,默认是false
  private boolean useCache;// 是否开启缓存,默认为true
  private boolean resultOrdered;
  private SqlCommandType sqlCommandType;
  private KeyGenerator keyGenerator;// 主键生成器
  private String[] keyProperties;
  private String[] keyColumns;
  private boolean hasNestedResultMaps;//是否有嵌套的resultMap
  private String databaseId;
  private Log statementLog;// 日志
  private LanguageDriver lang;
  private String[] resultSets;
  }

而其中的MappedStatement是最重要的,存储了xml中的<select>,<update>,<delete>,<insert>,以及是否使用缓存,对应的返回值,入参类型等信息。

@Autowired
private UserMapper userMapper;
List<User> = userMapper.listAll();

在这个过程中,我们知道mapper接口时没有实现类的,在调用@Autowried的时候,会注入一个动态代理对象,这个动态代理对象其实就是一个ProxyMapper,而这个ProxyMapper的创建是从ProxyMapperFactory来的,当调用listAll()方法时,会调用ProxyMapper的Invoke方法,因为一个Mapper接口有不止一个方法,因此这个MapperProxy内部持有一个methodCache<Method, MapperMethod>,而一个具体的方法就对应一个MapperMethod,

MapperProxy:

@Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    // 这里从cache中拿到MapperMethod,然后调用MapperMethod的execute方法,然后使用方法名称从configuration中获取到MappedStatement(还记得xml中的id和Mapper接口中方法名一致吗),然后使用sqlSession中的Executor,将MappedStatement作为参数,执行(其实就是把ms中的boundSql,以及参数,创建statement,然后执行,并且获取Result)
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }
SimpleExecutor:
  @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查询数据,BaseStatementHandler中持有typeHandlerRegistry,resultSetHandler,parameterHandler可以对参数以及结果进行转换
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

在BaseStatementHandler的实现类PreparedStatementHandler中,调用handleResultSets,将返回结果转化为java对象,并实现最终的查询。

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

从上面可以看见,MappedStatement很复杂,所以这里直接舍弃了将执行List selectOne(){}这样的方法解析为MappedStatement,而是直接使用mybatis的SqlSessionFactory,从而创建SqlSession --> connection --> preparedStatement,然后设置参数并执行,拿到ResultSet,利用spring的RowMaper解析为Java对象。

所以要做的就是更改SqlSession的创建,这里可以将SqlSessionFactory注册为Bean就可以。示例代码

@Repository
@SuppressWarnings({"unchecked", "rawtypes"})
public class BaseDao<T> {
    @Autowired
    private SqlSessionFactory sqlSessionFactory;

    public List<T> list(String sql, Object... args) throws SQLException {
        SqlSession sqlSession = sqlSessionFactory.openSession();

        Connection connection = sqlSession.getConnection();
        TypeHandlerRegistry typeHandlerRegistry = sqlSession.getConfiguration().getTypeHandlerRegistry();

        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        for (int i = 0; i < args.length; i++) {
            Class<?> javaType = ResolvableType.forClass(args[i].getClass()).getRawClass();
            TypeHandler typeHandler = typeHandlerRegistry.getTypeHandler(javaType);
            typeHandler.setParameter(preparedStatement, i + 1, args[i], null);
        }

        Class<T> tClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];

        ResultSet resultSet = preparedStatement.executeQuery();
        BeanPropertyRowMapper<T> rowMapper = new BeanPropertyRowMapper<>(tClass);

        DefaultConversionService defaultConversionService = new DefaultConversionService();
        // 这里加载类型转换器,由于没有使用mybatis的xml写法,所以也就没法儿使用MappedStatement,所以得自己解析,Spring有对应得解析器,但这里需要加入自定义得Converter
        // 所以前边定义得typeHandler的从ResultSet中getResult的方法其实是无法生效的,也就是说这时两套机制
        ServiceLoader.load(Converter.class).stream().forEach(converterProvider -> defaultConversionService.addConverter(converterProvider.get()));
        rowMapper.setConversionService(defaultConversionService);
        RowMapperResultSetExtractor<T> resultSetExtractor = new RowMapperResultSetExtractor<>(rowMapper);
        return resultSetExtractor.extractData(resultSet);
    }


}

以上,由于我们是直接执行的sql,所以没法儿使用mappedstatement,所有结果的解析,需要自己做,并且typehander的getResult,也是不生效的,这里需要改用spring 的converter来做。

Demo展示

Github地址

思考

其实如果要真正的融合,是应该将List selectOne(){}这样的方法解析为MappedStatement的,并且MappedStatement提供了builder模式,并且需要用到的组件也都提供了builder模式,已经大大减轻了自定义方法解析为MappedStatement的难度,并且Configuration的需要属性都提供了getter和setter方法,不需要使用诸如:反射或者代理这样的方式去做,也许后边会将此 demo 完善,以使用这样的方法。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值