• Java EE开发平台随手记3——Mybatis扩展2


      忙里偷闲,继续上周的话题,记录Mybatis的扩展。

      扩展5:设置默认的返回结果类型

      大家知道,在Mybatis的sql-mapper配置文件中,我们需要给<select>元素添加resultType或resultMap属性,这两个属性有且只能有一个。2013年我在做一个系统的时候,因为业务关系,查询出的结果集字段经常变化,为了简化处理,采用map作为返回数据的载体,然后不得不在绝大多数<select>元素上添加类似 resultType='java.util.HashMap'(Mybatis有HashMap的简写形式,这里为了更清晰,使用全限定符),于是催生了一个想法,能不能设置默认的返回结果类型?后面经过调试,继承SqlSessionFactoryBean添加如下代码实现:

     1 /**
     2  * 设置默认的查询结果返回类型
     3  * @param configuration
     4  * @param cls
     5  * @throws Exception 
     6  */
     7 private void setDefaultResultType(Configuration configuration, Class<?> cls) throws Exception{
     8     try {
     9         Field resultMaps = MappedStatement.class.getDeclaredField("resultMaps");
    10         resultMaps.setAccessible(true);
    11         for(Iterator<MappedStatement> i = configuration.getMappedStatements().iterator(); i.hasNext();){
    12             Object o = i.next();
    13             /**
    14              * 这里添加类型判断,是因为Mybatis实现中还存放了Ambiguity对象(sql-id的最后一段id重复情况下)
    15              */
    16             if(o instanceof MappedStatement){
    17                 MappedStatement ms = (MappedStatement)o;
    18                 if(SqlCommandType.SELECT.equals(ms.getSqlCommandType()) && ms.getResultMaps().isEmpty()){
    19                     ResultMap.Builder inlineResultMapBuilder = new ResultMap.Builder(configuration,ms.getId()+"-Inline",cls,new ArrayList<ResultMapping>(),null);
    20                     ResultMap resultMap = inlineResultMapBuilder.build();
    21                     List<ResultMap> rm = new ArrayList<ResultMap>();
    22                     rm.add(resultMap);
    23                     resultMaps.set(ms, Collections.unmodifiableList(rm));
    24                 }else{
    25                 }
    26             }
    27         }
    28     } catch (Exception e) {
    29         e.printStackTrace();
    30         throw e;
    31     }
    32 }
    View Code

    这个实现有很多写死的代码,也没有做足够完备的测试,不过到目前为止,总算还没有出错。

      我设置的默认返回结果类型是map,当然,这里其实可以更进一步扩展,添加一个SqlID模式和默认结果类型的映射接口,然后就根据需要去实现这个映射关系了。

      扩展6:自动扫描类型简称

      还是在SqlSessionFactoryBean继承类中,另外实现的一个扩展就是自动扫描类型简称。类型简称的用法如下:

    (1)在mybatis全局配置文件中添加别名

    1 <typeAliases>
    2         <typeAlias alias="RoleBean" type="com.forms.beneform4j.webapp.systemmanage.role.bean.RoleBean" />
    3     </typeAliases>
    View Code

    (2)在sql-mapper文件中使用alias。

      通过添加自动扫描类型简称,就可以将第一段配置去掉,而直接使用别名了。具体实现大概如下所示:

     1 private void scanTypeAliases(){
     2     if (this.autoScanTypeAliases && hasLength(this.typeAliasesScanPackage) && null != baseClass) {
     3         String[] typeAliasPackageArray = tokenizeToStringArray(this.typeAliasesScanPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
     4         List<Class<?>> list = new ArrayList<Class<?>>();
     5         List<String> alias = new ArrayList<String>();
     6         MetaObject meta = SystemMetaObject.forObject(this);
     7         Class<?>[] orig = (Class<?>[])meta.getValue("typeAliases");
     8         if(null != orig)
     9         {
    10             for(Class<?> t : orig){
    11                 list.add(t);
    12                 alias.add(t.getSimpleName().toLowerCase());
    13             }
    14         }
    15         for (String packageToScan : typeAliasPackageArray) {
    16             for(Class<?> type : CoreUtils.scanClassesByParentCls(packageToScan,  baseClass)){
    17                 String a = type.getSimpleName().toLowerCase();
    18               if (!alias.contains(a)) {
    19                   list.add(type);
    20                   alias.add(a);
    21               }else{
    22                   CommonLogger.warn("Mybatis在自动扫描注册别名时,发现有多个可简写为"+type.getSimpleName()+"的类,将取第一个类,忽略"+type);
    23               }
    24             }
    25         }
    26         super.setTypeAliases(list.toArray(new Class<?>[list.size()]));
    27     }
    28 }
    View Code

    这里属性autoScanTypeAliases表示是否需要自动扫描,typeAliasesScanPackage表示扫描的包,baseClass表示扫描的接口或父类。

      

      我们使用Mybatis,可以在父类中注入SqlSessionTemplate,然后子类调用相关方法,也可以通过写Dao的接口,让mybatis自动生成动态代理类,还可以编写一个静态帮助类,在这个帮助类中注入SqlSessionTemplate,然后提供相应的静态方法。这三种方法,以前用的多的是第三种方法,而现在,因为要让其他同事更容易接受,模块划分更清晰,采用了第二种方法。但这三种方法都有一个特点,那就是只使用Mybatis的SqlSession接口的原生方法。不能直接调用批处理、存储过程等,于是,我在SqlSession基础上,添加了一个IDaoTemplate接口:

      1 public interface IDaoTemplate{
      2 
      3     /**
      4      * 查询单笔数据
      5      * @param sqlId SQL-ID
      6      * @return 单个对象
      7      */
      8     public <T> T selectOne(String sqlId);
      9 
     10     /**
     11      * 查询单笔数据
     12      * @param sqlId     SQL-ID
     13      * @param parameter 参数对象
     14      * @return 单个对象
     15      */
     16     public <T> T selectOne(String sqlId, Object parameter);
     17 
     18     /**
     19      * 查询列表数据
     20      * @param sqlId SQL-ID
     21      * @return 对象列表
     22      */
     23     public <E> List<E> selectList(String sqlId);
     24 
     25     /**
     26      * 查询列表数据
     27      * @param sqlId     SQL-ID
     28      * @param parameter 参数对象
     29      * @return 对象列表
     30      */
     31     public <E> List<E> selectList(String sqlId, Object parameter);
     32     
     33     /**
     34      * 查询分页列表数据
     35      * @param sqlId  SQL-ID
     36      * @param page   分页对象
     37      * @return 指定页的对象列表
     38      */
     39     public <E> List<E> selectList(String sqlId, IPage page);
     40 
     41     /**
     42      * 查询分页列表数据
     43      * @param sqlId     SQL-ID
     44      * @param parameter 参数对象
     45      * @param page      分页对象
     46      * @return 指定页的对象列表
     47      */
     48     public <E> List<E> selectList(String sqlId, Object parameter, IPage page);
     49     
     50     /**
     51      * 流式查询
     52      * @param sqlId SQL-ID
     53      * @return 流式操作接口
     54      */
     55     public <E>IListStreamReader<E> selectListStream(String sqlId);
     56     
     57     /**
     58      * 流式查询
     59      * @param sqlId     SQL-ID
     60      * @param parameter 参数对象
     61      * @return 流式操作接口
     62      */
     63     public <E>IListStreamReader<E> selectListStream(String sqlId, Object parameter);
     64     
     65     /**
     66      * 流式查询
     67      * @param sqlId      SQL-ID
     68      * @param fetchSize  每次读取的记录条数(0, 5000]
     69      * @return 流式操作接口
     70      */
     71     public <E>IListStreamReader<E> selectListStream(String sqlId, int fetchSize);
     72 
     73     /**
     74      * 流式查询
     75      * @param sqlId      SQL-ID
     76      * @param parameter  参数对象
     77      * @param fetchSize  每次读取的记录条数(0, 5000]
     78      * @return 流式操作接口
     79      */
     80     public <E>IListStreamReader<E> selectListStream(String sqlId, Object parameter, int fetchSize);
     81     
     82     /**
     83      * 新增
     84      * @param sqlId SQL-ID
     85      * @return 影响的记录条数
     86      */
     87     public int insert(String sqlId);
     88 
     89     /**
     90      * 新增
     91      * @param sqlId     SQL-ID
     92      * @param parameter 参数对象
     93      * @return 影响的记录条数
     94      */
     95     public int insert(String sqlId, Object parameter);
     96 
     97     /**
     98      * 修改
     99      * @param sqlId SQL-ID
    100      * @return 影响的记录条数
    101      */
    102     public int update(String sqlId);
    103 
    104     /**
    105      * 修改
    106      * @param sqlId     SQL-ID
    107      * @param parameter 参数对象
    108      * @return 影响的记录条数
    109      */
    110     public int update(String sqlId, Object parameter);
    111     
    112     /**
    113      * 删除
    114      * @param sqlId SQL-ID
    115      * @return 影响的记录条数
    116      */
    117     public int delete(String sqlId);
    118 
    119     /**
    120      * 删除
    121      * @param sqlId     SQL-ID
    122      * @param parameter 参数对象
    123      * @return 影响的记录条数
    124      */
    125     public int delete(String sqlId, Object parameter);
    126     
    127     /**
    128      * 执行批量:一个SQL执行多次
    129      * @param sqlId      SQL-ID
    130      * @param parameters 参数对象数组
    131      * @return 批量执行的影响记录数组
    132      */
    133     public int[] executeBatch(String sqlId, List<?> parameters);
    134     
    135     /**
    136      * 执行批量:一次执行多个SQL
    137      * @param sqlIds  要执行的一组SQL-ID
    138      * @return 批量执行的影响记录数组
    139      */
    140     public int[] executeBatch(List<String> sqlIds);
    141 
    142     /**
    143      * 执行批量:一次执行多个SQL
    144      * @param sqlIds     要执行的一组SQL-ID
    145      * @param parameters 参数对象数组
    146      * @return 批量执行的影响记录数组
    147      */
    148     public int[] executeBatch(List<String> sqlIds, List<?> parameters);
    149     
    150     /**
    151      * 打开批量执行模式
    152      */
    153     public void openBatchType();
    154     
    155     /**
    156      * 恢复打开批量执行模式之前的执行模式
    157      */
    158     public void resetBatchType();
    159     
    160     /**
    161      * 获取批量执行结果
    162      * @return
    163      */
    164     public int[] flushBatch();
    165     
    166     /**
    167      * 调用存储过程
    168      * @param sqlId  SQL-ID
    169      * @return 存储过程返回结果接口
    170      */
    171     public ICallResult call(String sqlId);
    172       
    173     /**
    174      * 调用存储过程
    175      * @param sqlId     SQL-ID
    176      * @param parameter 参数对象
    177      * @return 存储过程返回结果接口
    178      */
    179     public ICallResult call(String sqlId, Object parameter);
    180 }
    View Code

    可以看到,其中部分是简单调用SqlSession接口,但也有部分是我们的扩展。

      扩展7:流式查询

      流式查询有四个重置方法,sql-Id是必须的参数,查询参数parameter和每次处理的记录条数fetchSize是可选的。流式查询的结果接口如下:

     1 public interface IListStreamReader<T> {
     2 
     3     /**
     4      * 读取当前批次的数据列表,如果没有数据,返回null
     5      * @return 当前批次的数据列表
     6      */
     7     public List<T> read();
     8     
     9     /**
    10      * 重置读取批次
    11      */
    12     public void reset();
    13 }
    View Code

    只有两个方法,其中关键方法是获取当前批次的数据结果集,辅助方法是重置读取批次。

      流式查询本质上并没有执行查询,而只是将查询需要的要素包装成为一个对象,当调用者调用这个对象的read方法时,才真正执行数据库查询,而执行查询又使用实现内中内置的分页对象,每次读取只读取当前批次(当前页数)的结果集,查询之后,就内置分页对象的当前页数指向下一页。

      流式查询适用于大数据量的查询处理,比如大数据量的数据需要生成Excel文件供客户端下载,一次性查询很容易内存溢出,使用流式查询就可以很好的解决这个问题。

      把流式查询结果对象的抽象实现贴在这里,应该更便于理解:

     1 public abstract class AbstractListStreamReader<T> implements IListStreamReader<T>{
     2     
     3     /**
     4      * 默认的每次读取记录数
     5      */
     6     private static final int defaultFetchSize = 1000;
     7     
     8     /**
     9      * 最大的每次读取记录数
    10      */
    11     private static final int maxFetchSize = 5000;
    12     
    13     /**
    14      * 实际的每次读取记录数
    15      */
    16     private final int fetchSize;
    17     
    18     /**
    19      * 分页对象
    20      */
    21     private final IPage page;
    22     
    23     /**
    24      * 是否完成的标志
    25      */
    26     private transient boolean finish = false;//是否完成
    27     
    28     /**
    29      * 无参构造函数
    30      */
    31     public AbstractListStreamReader() {
    32         this(defaultFetchSize);
    33     }
    34     
    35     /**
    36      * 使用指定读取数大小的构造函数
    37      * @param fetchSize 每次读取的记录条数
    38      */
    39     public AbstractListStreamReader(int fetchSize) {
    40         if(fetchSize <= 0 || fetchSize > maxFetchSize){
    41             Throw.throwRuntimeException(DaoExceptionCodes.BF020012, fetchSize, "(0, "+maxFetchSize+"]");
    42         }
    43         this.fetchSize = fetchSize;
    44         BasePage page = new BasePage();
    45         page.setPageSize(fetchSize);
    46         this.page = page;
    47     }
    48     
    49     /**
    50      * 读取当前批次的列表数据,读取的时候会加锁
    51      */
    52     @Override
    53     public synchronized List<T> read() {
    54         if(!finish){
    55             List<T> rs = doRead(page);//查询当前页数据
    56             if(page.hasNextPage()){//有下一页,游标指向下一页
    57                 page.setPageProperty(page.getTotalRecords(), page.getCurrentPage()+1, fetchSize);
    58             }else{//没有下一页,完成
    59                 finish = true;
    60             }
    61             return rs;
    62         }
    63         return null;
    64     }
    65     
    66     /**
    67      * 执行实际的读取操作
    68      * @param page 分页对象
    69      * @return 和分页对象相对应的数据记录列表
    70      */
    71     abstract protected List<T> doRead(IPage page);
    72     
    73     /**
    74      * 重置读取批次,重置过程中会加锁
    75      */
    76     @Override
    77     public synchronized void reset(){
    78         this.finish = false;
    79         this.page.setPageProperty(this.page.getTotalPages(), 1, fetchSize);
    80     }
    81 }
    View Code

    至于具体的实现,只要继承抽象实现,然后实现

    abstract protected List<T> doRead(IPage page);

    就可以了,而这个方法只是一个简单的分页查询,实现起来没有任何难度。

      扩展8:调用存储过程

      Mybatis中可以调用存储过程,但直接使用并不方便,我们将其封装如下:

     1 public interface IDaoTemplate{
     2 
     3     /**
     4      *  这里省略了其它方法
     5      */
     6     
     7     
     8     /**
     9      * 调用存储过程
    10      * @param sqlId  SQL-ID
    11      * @return 存储过程返回结果接口
    12      */
    13     public ICallResult call(String sqlId);
    14       
    15     /**
    16      * 调用存储过程
    17      * @param sqlId     SQL-ID
    18      * @param parameter 参数对象
    19      * @return 存储过程返回结果接口
    20      */
    21     public ICallResult call(String sqlId, Object parameter);
    22 }
    View Code

    这样调用就非常方便了,那么这里的ICallResult是什么呢?看一下它的定义:

     1 public interface ICallResult {
     2     
     3     /**
     4      * 获取存储过程返回值
     5      * @return
     6      */
     7     public <T> T getResult();
     8 
     9     /**
    10      * 根据参数名称返回输出参数
    11      * @param name 输出参数名称
    12      * @return 和输出参数名称相对应的返回结果,如果不存在输出参数,抛出平台运行时异常
    13      */
    14     public <T> T getOutputParam(String name);
    15     
    16     /**
    17      * 返回输出参数名称的迭代器
    18      * @return 输出参数名迭代器
    19      */
    20     public Iterator<String> iterator();
    21 }
    View Code

    使用过Mybatis调用存储过程的朋友,看了这个借口应该就能明白,但鉴于存储过程调用并不普通,这里举一个例子:

     1 <?xml version="1.0" encoding="UTF-8" ?>
     2 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
     3 <mapper namespace="com.forms.beneform4j.core.dao.mybatis.mapper.call.ICallDao">
     4 
     5     <select id="call" statementType="CALLABLE">
     6         {call BF_TEST_PACKAGE.BF_TEST_PROCEDURE(
     7             #{input, jdbcType=VARCHAR}, 
     8             #{output1, mode=OUT, jdbcType=VARCHAR}, 
     9             #{output2, mode=OUT, jdbcType=VARCHAR}, 
    10             #{rs1, mode=OUT, jdbcType=CURSOR}, 
    11             #{rs2, mode=OUT, jdbcType=CURSOR}
    12         )}
    13     </select>
    14 </mapper>
    View Code

    如上配置,传入sqlId和参数对象(含input属性)后,返回的ICallResult接口中,可以通过如下的方式获取存储过程的返回值(如果有)和输出参数:

     1 @Repository
     2 interface ICallDao {
     3 
     4     public ICallResult call(@Param("input")String input);
     5 }
     6 
     7 @Service
     8 public class ICallDaoTest {
     9 
    10     @Autowired
    11     private ICallDao dao;
    12     
    13     @Test
    14     public void call() throws Exception {
    15         ICallResult rs = dao.call("1");
    16         //直接访问返回结果和输出参数
    17         Object returnValue = rs.getResult();
    18         Object output1 = rs.getOutputParam("output1");
    19         List<Object> rs1 = rs.getOutputParam("rs1");
    20         
    21         //循环访问输出参数
    22         Iterator<String> i = rs.iterator();
    23         String name = "";
    24         while(i.hasNext()){
    25             name = i.next();
    26             System.out.println(name + "============" + rs.getOutputParam(name));
    27         }
    28     }
    29 }
    View Code

       说完了调用存储过程的用法,回过头来简单的提一下调用存储过程的实现:实际上很简单,只要添加一个Mybatis的拦截器即可,拦截结果处理接口ResultSetHandler的方法handleOutputParameters,然后将返回结果和输出参数包装到一个Map对象中即可,具体代码就不贴了。

      时间关系,今天写到这里。下次再继续写Dao接口中SqlID的重定向、IDaoTemplate接口中的批量处理相关的扩展。

  • 相关阅读:
    CloudStack+KVM环境搭建(步骤很详细,说明ClockStack是用来管理虚拟机的)
    CloudStack和OpenStack该如何选择(如果准备选择OpenStack,请做好hack的准备。CloudStack的底层功能已经做的很完善了,更适合商用)
    NancyFx And ReactiveX
    Apache Kafka® is a distributed streaming platform
    C/C++配置
    Win10 专业版 Hyper-V 主机计算服务无法启动
    使用事件和 CQRS 重写 CRUD 系统
    使用Skywalking分布式链路追踪系统
    GraphQL&DSL&API网关
    TomatoLog 是一个基于 .NETCore 平台的产品。
  • 原文地址:https://www.cnblogs.com/linjisong/p/5547498.html
Copyright © 2020-2023  润新知