Administrator
发布于 2023-01-03 / 127 阅读
0
0

Mybatis Theory

Mybatis整体架构

执行流程图

image-20230103095301899

主要构件及其相互关系

构件 描述
SqlSession 作为MyBatis⼯作的主要顶层API,表示和数据库交互的会话,完成必要数 据库增删改查功能
Executor MyBatis执⾏器,是MyBatis调度的核⼼,负责SQL语句的⽣成和查询缓 存的维护
StatementHandler 封装了JDBC Statement操作,负责对JDBC statement的操作,如设置参 数、将Statement结果集转换成List集合。
ParameterHandler 负责对⽤户传递的参数转换成JDBC Statement所需要的参数,
ResultSetHandler 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;
TypeHandler 负责java数据类型和jdbc数据类型之间的映射和转换
MappedStatement MappedStatement维护了⼀条<select | update | delete | insert>节点的封 装
SqlSource 负责根据⽤户传递的parameterObject,动态地⽣成SQL语句,将信息封 装到BoundSql对象中,并返回
BoundSql 表示动态⽣成的SQL语句以及相应的参数信息

·

层次结构

image-20230103095729385

总体流程

(1) 加载配置并初始化
触发条件:加载配置⽂件
配置来源于两个地⽅,⼀个是配置⽂件(主配置⽂件conf.xml,mapper⽂件*.xml),—个是java代码中的 注解,将主配置⽂件内容解析封装到Configuration,将sql的配置信息加载成为⼀个mappedstatement 对象,存储在内存之中


(2) 接收调⽤请求
触发条件:调⽤Mybatis提供的API
传⼊参数:为SQL的ID和传⼊参数对象
处理过程:将请求传递给下层的请求处理层进⾏处理。


(3) 处理操作请求
触发条件:API接⼝层传递请求过来
传⼊参数:为SQL的ID和传⼊参数对象
处理过程:
(A) 根据SQL的ID查找对应的MappedStatement对象。
(B) 根据传⼊参数对象解析MappedStatement对象,得到最终要执⾏的SQL和执⾏传⼊参数。
© 获取数据库连接,根据得到的最终SQL语句和执⾏传⼊参数到数据库执⾏,并得到执⾏结果。
(D) 根据MappedStatement对象中的结果映射配置对得到的执⾏结果进⾏转换处理,并得到最终的处理 结果。
(E) 释放连接资源。
(4) 返回处理结果
将最终的处理结果返回。

各个文件的作用 和 要存储哪些信息

sqlMapConfig.xml:因为如果使用jdbc,那么数据库配置信息就需要硬编码在代码中,解决这个问题的方案就是,将数据库配置信息 写在配置文件中 。sqlMapConfig.xml中存储的就是数据库配置信息,包括数据驱动类,数据库地址,用户名和密码

userMapper.xml:如果使用jdbc,那么sql语句就需要硬编码在代码中,解决这个问题的方案就是,将sql语句写在配置文件中,userMapper.xml中存储的就是sql语句,包括sql语句的参数、结果集提取出的javaBean类型等

Configuration:包含dataSource和MappedStatement。这个Configuration最终会传递jdbc代码中,在jdbc代码中被用于 得到Connection 和 PreparedStatement。

MappedStatement:对userMapper.xml解析的结果存储在这里,包含id、要设置的参数、返回类型、sql语句

XmlConfigParser用于解析sqlMapConfig.xml,得到一个dataSource(连接池dataSource的实现类)


XmlMapperParser 用于解析userMapper.xml,得到MappedStatement


GenericTokenParser用于将#{}解析成?,且取出#{}中的参数名称

SqlSessionFactoryBuilder:启动类,将xml解析成configuration,然后将configuration传递到SqlSessionFactory,实现类是DefaultSqlSessionFactory

SqlSessionFactory:用于构建SqlSession,将configuration传递到SqlSession中

SqlSession:与数据库交互的会话,写一些select/updte/delete/insert代码,DefaultSqlSession

SqlExecutor:这个是写一些jdbc代码,包含获取连接,预编译,执行sql,返回结果集等。SqlSession中的增删改查,就是调用SqlExecutor中的方法来完成的。实现类是SimpleSqlExecutor

SqlSession中有一个getMapper,这个方法的作用是,给指定接口生成jdk动态代理类

动态sql的解析流程

SqlSessionFactoryBuilder(build) ----->  XMLConfigBuilder(parseConfiguration

--->mapperElement------>mapperParser.parse) ---->XMLMapperBuilder(parse--->configurationElement-

---->buildStatementFromContext)----->XMLStatementBuilder(parseStatementNode)----->XMLLanguageDriver(createSqlSource)

----->XMLScriptBuilder(parseScriptNode--->parseDynamicTags)----->IfHandler(handleNode)
    

sql的执行流程

1.创建sqlSessionFactory的时候,会将sql映射文件读到内存中,解析得到MappedStatement,同时会将mybatis的配置文件解析存储到Configuration中

2.当调用sqlSession.getMapper的时候,会先创建mapperProxyFactory,然后通过mapperProxyFactory.newInstance创建动态代理类。

3.当调用动态代理类的方法时,会先执行MapperProxy的invoke,接着调用DefaultSqlSession的selectList,接着调用SimpleExecutor的doQuery方法,接着调用PreparedStatementHandler的parameterize方法,接着调用
DefaultParameterHandler的setParameters方法,接着调用TypeHandler的setParameter,然后执行数据库查询,得到resultSet,将这个resultSet交给DefaultResultSetHandler的handleResultSets方法,得到对象。

具体调用关系如下:

当调用动态代理类的方法时,会先执行MapperProxy(invoke)--->MapperMethod(execute)--->DefaultSqlSession(selectList)---->BaseExecutor(query---->queryFromDatabase)----->SimpleExecutor(doQuery)---->PreparedStatementHandler(parameterize)---->DefaultParameterHandler(setParameters)---->TypeHandler(setParameter)---->PreparedStatementHandler(query)---->执行PreparedStatement(execute)---->DefaultResultSetHandler(handleResultSets)

插件

⼀般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者⾃⾏拓展。这样的好处是显⽽易⻅ 的,⼀是增加了框架的灵活性。⼆是开发者可以结合实际需求,对框架进⾏拓展,使其能够更好的⼯ 作。

以MyBatis为例,我们可基于MyBati s插件机制实现分⻚、分表,监控等功能。由于插件和业务 ⽆关,业务也⽆法感知插件的存在。
因此可以⽆感植⼊插件,在⽆形中增强功能

插件原理

Mybati s作为⼀个应⽤⼴泛的优秀的ORM开源框架,这个框架具有强⼤的灵活性,在四⼤组件
(Executor、StatementHandler、ParameterHandler、ResultSetHandler)处提供了简单易⽤的插 件扩展机制。

Mybatis对持久层的操作就是借助于四⼤核⼼对象。

MyBatis⽀持⽤插件对四⼤核⼼对象进 ⾏拦截,对mybatis来说插件就是拦截器,⽤来增强核⼼对象的功能,增强功能本质上是借助于底层的 动态代理实现的,换句话说,MyBatis中的四⼤对象都是代理对象

image-20230103101457109

MyBatis所允许拦截的⽅法如下:

  • 执⾏器Executor (update、query、commit、rollback等⽅法);
  • SQL语法构建器StatementHandler (prepare、parameterize、batch、updates query等⽅ 法);
  • 参数处理器ParameterHandler (getParameterObject、setParameters⽅法);
  • 结果集处理器ResultSetHandler (handleResultSets、handleOutputParameters等⽅法);


在四⼤对象创建的时候

  • 1、每个创建出来的对象不是直接返回的,⽽是interceptorChain.pluginAll(parameterHandler);
  • 2、获取到所有的Interceptor (拦截器)(插件需要实现的接⼝);调⽤ interceptor.plugin(target);返回 target 包
  • 装后的对象
  • 3、插件机制,我们可以使⽤插件为⽬标对象创建⼀个代理对象;AOP (⾯向切⾯)我们的插件可 以为四⼤对象
  • 创建出代理对象,代理对象就可以拦截到四⼤对象的每⼀个执⾏;

拦截

插件具体是如何拦截并附加额外的功能的呢?以ParameterHandler来说

public ParameterHandler newParameterHandler(MappedStatement mappedStatement,
Object object, BoundSql sql, InterceptorChain interceptorChain){
  ParameterHandler parameterHandler =
  mappedStatement.getLang().createParameterHandler(mappedStatement,object,sql);
  parameterHandler = (ParameterHandler)
  interceptorChain.pluginAll(parameterHandler);
  return parameterHandler;
}


public Object pluginAll(Object target) {
  for (Interceptor interceptor : interceptors) {
  	target = interceptor.plugin(target);
  }
  return target;
}

interceptorChain保存了所有的拦截器(interceptors),是mybatis初始化的时候创建的。调⽤拦截器链 中的拦截器
依次的对⽬标进⾏拦截或增强。interceptor.plugin(target)中的target就可以理解为mybatis 中的四⼤对象。返回
的target是被重重代理后的对象
如果我们想要拦截Executor的query⽅法,那么可以这样定义插件:

@Intercepts({
@Signature(
  type = Executor.class,
  method = "query",
  args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}
)
})
public class ExeunplePlugin implements Interceptor {
	//省略逻辑
}

在mybatis-config.xml中,将MyPlugin配置到plugins标签中

<configuration>
  ....
    
    <plugins>
        <plugin interceptor="com.wp.persist.plugin.ExeunplePlugin">
            <property name="address" value="nanjing"/>
        </plugin>
    </plugins>
</configuration>

⾃定义插件

插件接⼝
Mybatis 插件接⼝-Interceptor
• Intercept⽅法,插件的核⼼⽅法
• plugin⽅法,⽣成target的代理对象
• setProperties⽅法,传递插件所需参数

Intercepts ({//注意看这个⼤花括号,也就这说这⾥可以定义多个@Signature对多个地⽅拦截,都⽤这个拦截器
@Signature (type = StatementHandler .class , //这是指拦截哪个接⼝
	method = "prepare",//这个接⼝内的哪个⽅法名,不要拼错了
	args = { Connection.class, Integer .class}),//// 这是拦截的⽅法的⼊参,按顺序写到
这,不要多也不要少,如果⽅法重载,可是要通过⽅法名和⼊参来确定唯⼀的
})
public class MyPlugin implements Interceptor {
  private final Logger logger = LoggerFactory.getLogger(this.getClass());
  
  
  // //这⾥是每次执⾏操作的时候,都会进⾏这个拦截器的⽅法内
  Override
  public Object intercept(Invocation invocation) throws Throwable {
    //增强逻辑
    	System.out.println("对⽅法进⾏了增强....");
    	return invocation.proceed(); //执⾏原⽅法
  }
  
  
  /**
  * //主要是为了把这个拦截器⽣成⼀个代理放到拦截器链中
  * ^Description包装⽬标对象 为⽬标对象创建代理对象
  * @Param target为要拦截的对象
  * @Return代理对象
  */
  Override
  public Object plugin(Object target) {
  	System.out.println("将要包装的⽬标对象:"+target);
  	return Plugin.wrap(target,this);
  }
  
  
  /**获取配置⽂件的属性**/
  //插件初始化的时候调⽤,也只调⽤⼀次,插件配置的属性从这⾥设置进来
  Override
  public void setProperties(Properties properties) {
  	System.out.println("插件配置的初始化参数:"+properties );
  }
}

在mybatis-config.xml中,将MyPlugin配置到plugins标签中

<configuration>
  ....
    
    <plugins>
        <plugin interceptor="com.wp.persist.plugin.MyPlugin">
            <property name="address" value="nanjing"/>
        </plugin>
    </plugins>
</configuration>

源码分析

Plugin实现了 InvocationHandler接⼝,因此它的invoke⽅法会拦截所有的⽅法调⽤。

invoke⽅法会 对所拦截的⽅法进⾏检测,以决定是否执⾏插件逻辑。该⽅法的逻辑如下:

// -Plugin
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
      /*
      *获取被拦截⽅法列表,⽐如:
      * signatureMap.get(Executor.class), 可能返回 [query, update, commit]
      */
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      //检测⽅法列表是否包含被拦截的⽅法
      if (methods != null && methods.contains(method)) {
      //执⾏插件逻辑
      return interceptor.intercept(new Invocation(target, method, args));
      //执⾏被拦截的⽅法
      return method.invoke(target, args);
  } catch(Exception e){
  
  }
  
}

invoke⽅法的代码⽐较少,逻辑不难理解。

⾸先,invoke⽅法会检测被拦截⽅法是否配置在插件的 @Signature注解中,若是,则执⾏插件逻辑,否则执⾏被拦截⽅法。

插件逻辑封装在intercept中,该 ⽅法的参数类型为Invocationo Invocation主要⽤于存储⽬标类,⽅法以及⽅法参数列表。下⾯简单看 ⼀下该类的定义

public class Invocation {
    private final Object target;
    private final Method method;
    private final Object[] args;
    
    public Invocation(Object targetf Method method, Object[] args) {
    this.target = target;
    this.method = method;
    
    
    //省略部分代码
    public Object proceed() throws InvocationTargetException, IllegalAccessException { //调
    ⽤被拦截的⽅法
    >> —

}

PageHelper

参考文档:https://pagehelper.github.io/docs/howtouse/

分页插件—拦截器

分页插件,本质是一个拦截器。

新版拦截器是 com.github.pagehelper.PageInterceptor。 com.github.pagehelper.PageHelper 现在是一个特殊的 dialect 实现类,是分页插件的默认实现类,提供了和以前相同的用法。

分页插件参数介绍

helperDialect:分页插件会自动检测当前的数据库链接,自动选择合适的分页方式。

你可以配置helperDialect属性来指定分页插件使用哪种方言。

配置时,可以使用下面的缩写值:oracle,mysql,mariadb,sqlite,hsqldb,postgresql,db2,sqlserver,informix,h2,sqlserver2012,derby
特别注意:使用 SqlServer2012 数据库时,需要手动指定为 sqlserver2012,否则会使用 SqlServer2005 的方式进行分页。
你也可以实现 AbstractHelperDialect,然后配置该属性为实现类的全限定名称即可使用自定义的实现方法。


pageSizeZero:默认值为 false,当该参数设置为 true 时,如果 pageSize=0 或者 RowBounds.limit = 0 就会查询出全部的结果(相当于没有执行分页查询,但是返回结果仍然是 Page 类型)。


reasonable:分页合理化参数,默认值为false。当该参数设置为 true 时,pageNum<=0 时会查询第一页, pageNum>pages(超过总数时),会查询最后一页。默认false 时,直接根据参数进行查询。


params:为了支持startPage(Object params)方法,增加了该参数来配置参数映射,用于从对象中根据属性名取值, 可以配置 pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值, 默认值为pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero。


supportMethodsArguments:支持通过 Mapper 接口参数来传递分页参数,默认值false,分页插件会从查询方法的参数值中,自动根据上面 params 配置的字段中取值,查找到合适的值时就会自动分页。 使用方法可以参考测试代码中的 com.github.pagehelper.test.basic 包下的 ArgumentsMapTest 和 ArgumentsObjTest。

如何在代码中使用


//第二种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.startPage(1, 10);
List<Country> list = countryMapper.selectIf(1);

//第三种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.offsetPage(1, 10);
List<Country> list = countryMapper.selectIf(1);

//第四种,参数方法调用
//存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数
public interface CountryMapper {
    List<Country> selectByPageNumSize(
            @Param("user") User user,
            @Param("pageNum") int pageNum,
            @Param("pageSize") int pageSize);
}
//配置supportMethodsArguments=true
//在代码中直接调用:
List<Country> list = countryMapper.selectByPageNumSize(user, 1, 10);

//第五种,参数对象
//如果 pageNum 和 pageSize 存在于 User 对象中,只要参数有值,也会被分页
//有如下 User 对象
public class User {
    //其他fields
    //下面两个参数名和 params 配置的名字一致
    private Integer pageNum;
    private Integer pageSize;
}
//存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数
public interface CountryMapper {
    List<Country> selectByPageNumSize(User user);
}
//当 user 中的 pageNum!= null && pageSize!= null 时,会自动分页
List<Country> list = countryMapper.selectByPageNumSize(user);

PageHelper.startPage 静态方法调用

除了 PageHelper.startPage 方法外,还提供了类似用法的 PageHelper.offsetPage 方法。

在你需要进行分页的 MyBatis 查询方法前调用 PageHelper.startPage 静态方法即可,紧跟在这个方法后的第一个MyBatis 查询方法会被进行分页。

例一:
//获取第1页,10条内容,默认查询总数count
PageHelper.startPage(1, 10);
//紧跟着的第一个select方法会被分页
List list = countryMapper.selectIf(1);
assertEquals(2, list.get(0).getId());
assertEquals(10, list.size());
//分页时,实际返回的结果list类型是Page,如果想取出分页信息,需要强制转换为Page
assertEquals(182, ((Page) list).getTotal());



例二:
//request: url?pageNum=1&pageSize=10
//支持 ServletRequest,Map,POJO 对象,需要配合 params 参数
PageHelper.startPage(request);
//紧跟着的第一个select方法会被分页
List list = countryMapper.selectIf(1);

//后面的不会被分页,除非再次调用PageHelper.startPage
List list2 = countryMapper.selectIf(null);
//list1
assertEquals(2, list.get(0).getId());
assertEquals(10, list.size());
//分页时,实际返回的结果list类型是Page,如果想取出分页信息,需要强制转换为Page
//或者使用PageInfo类(下面的例子有介绍)
assertEquals(182, ((Page) list).getTotal());
//list2
assertEquals(1, list2.get(0).getId());
assertEquals(182, list2.size());



例三,使用PageInfo的用法:
//获取第1页,10条内容,默认查询总数count
PageHelper.startPage(1, 10);
List list = countryMapper.selectAll();
//用PageInfo对结果进行包装
PageInfo page = new PageInfo(list);
//测试PageInfo全部属性
//PageInfo包含了非常全面的分页属性
assertEquals(1, page.getPageNum());
assertEquals(10, page.getPageSize());
assertEquals(1, page.getStartRow());
assertEquals(10, page.getEndRow());
assertEquals(183, page.getTotal());
assertEquals(19, page.getPages());
assertEquals(1, page.getFirstPage());
assertEquals(8, page.getLastPage());
assertEquals(true, page.isFirstPage());
assertEquals(false, page.isLastPage());
assertEquals(false, page.isHasPreviousPage());
assertEquals(true, page.isHasNextPage());



3). 使用参数方式
想要使用参数方式,需要配置 supportMethodsArguments 参数为 true,同时要配置 params 参数。 例如下面的配置:

在 MyBatis 方法中:

List selectByPageNumSize(
@Param(“user”) User user,
@Param(“pageNumKey”) int pageNum,
@Param(“pageSizeKey”) int pageSize);
当调用这个方法时,由于同时发现了 pageNumKey 和 pageSizeKey 参数,这个方法就会被分页。params 提供的几个参数都可以这样使用。



除了上面这种方式外,如果 User 对象中包含这两个参数值,也可以有下面的方法:

List selectByPageNumSize(User user);
当从 User 中同时发现了 pageNumKey 和 pageSizeKey 参数,这个方法就会被分页。

注意:pageNum 和 pageSize 两个属性同时存在才会触发分页操作,在这个前提下,其他的分页参数才会生效。

分页查询中total是怎么处理的?PageInfo和 Page的关系是什么?

查询sql语句执行,获取到totalCount,将这个totalCount放到ThreadLocal中的page的total属性中,
然后将返回的list封装成page对象,这个page 是 extends ArrayList的


PageInfo是一个对象,能获取到的数据比Page多;
即PageInfo对page对象的信息,进行了重新封装。PageInfo中有很多属性,其中protected List list; 这个属性,就是指向page对象的。


封装后的格式更方便与前端进行交互

DTO BO PO转换,引起分页参数中的total不正确

当我们想要查询用户列表,因为用户过多,往往会有分页操作。当我们使用pagehelper,执行分页查询时,首先执行select sql,得到的一个List,这个List不是一个简单的List,而是一个Page对象。Page extends ArrayList,page对象除了具有List的功能,还额外持有了分页信息,包括pageSize,pageNum,total等。



数据库返回的是PO对象的List,如果我们想要将PO转换成BO,那么得到的BO对象的List是不含分页参数的,也即不含pageSize,pageNum,total等。因为前端页面,是需要返回分页信息,特别是需要返回total值。



所以,我们需要做额外的分页参数处理,如下:

  public static <E, T> List<T> getPageList(List<E> originList, List<T> targetList) {
        if (originList instanceof Page) {
            Page<T> result = new Page<>();
            Page originPage = (Page) originList;
            result.setPageSize(originPage.getPageSize());
            result.setPageNum(originPage.getPageNum());
            result.setTotal(originPage.getTotal());
            result.addAll(targetList);
            return result;
        } else {
            return targetList;
        }
    }

调用代码如下:

  List<DashboardComponentConfigPO> dashboardComponentConfigPOs = dashboardComponentConfigMapper.selectComponentConfigs(dashboardComponentConfigPO);
        if (CollectionUtils.isNotEmpty(dashboardComponentConfigPOs)) {
            List<DashboardComponentConfigBO> configBOList = dashboardComponentConfigPOs.stream().map(DashboardComponentConfigBO::po2Bo).collect(Collectors.toList());
            return PageUtils.getPageList(dashboardComponentConfigPOs, configBOList);
        } else {
            return new ArrayList<>();
        }

PageHelper原理

PageHelper 方法使用了静态的 ThreadLocal 参数,分页参数和线程是绑定的。

只要你可以保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,这就是安全的。因为 PageHelper 在 finally 代码段中自动清除了 ThreadLocal 存储的对象。

如果代码在进入 Executor 前发生异常,就会导致线程不可用,这属于人为的 Bug(例如接口方法和 XML 中的不匹配,导致找不到 MappedStatement 时), 这种情况由于线程不可用,也不会导致 ThreadLocal 参数被错误的使用。

但是如果你写出下面这样的代码,就是不安全的用法:

PageHelper.startPage(1, 10);
List<Country> list;
if(param1 != null){
    list = countryMapper.selectIf(param1);
} else {
    list = new ArrayList<Country>();
} 
  

这种情况下由于 param1 存在 null 的情况,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。

上面这个代码,应该写成下面这个样子:

List<Country> list;
if(param1 != null){
    PageHelper.startPage(1, 10);
    list = countryMapper.selectIf(param1);
} else {
    list = new ArrayList<Country>();
}

这种写法就能保证安全。

如果你对此不放心,你可以手动清理 ThreadLocal 存储的分页参数,可以像下面这样使用:

List<Country> list;
if(param1 != null){
    PageHelper.startPage(1, 10);
    try{
        list = countryMapper.selectAll();
    } finally {
        PageHelper.clearPage();
    }
} else {
    list = new ArrayList<Country>();
}

这么写很不好看,而且没有必要。

源码分析

// com.github.pagehelper.page.PageMethod#startPage(int, int, boolean, java.lang.Boolean, java.lang.Boolean)  
    /**
     * 开始分页
     *
     * @param pageNum      页码
     * @param pageSize     每页显示数量
     * @param count        是否进行count查询
     * @param reasonable   分页合理化,null时用默认配置
     * @param pageSizeZero true且pageSize=0时返回全部结果,false时分页,null时用默认配置
     */
    public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
        Page<E> page = new Page<E>(pageNum, pageSize, count);
        page.setReasonable(reasonable);
        page.setPageSizeZero(pageSizeZero);
        //当已经执行过orderBy的时候
        Page<E> oldPage = getLocalPage();
        if (oldPage != null && oldPage.isOrderByOnly()) {
            page.setOrderBy(oldPage.getOrderBy());
        }
        setLocalPage(page); // 这里是关键代码,将分页参数,保存到ThreadLocal中
        return page;
    }  

我们继续追踪,看下这个setLocalPage方法

public abstract class PageMethod {
    protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
    protected static boolean DEFAULT_COUNT = true;

    /**
     * 设置 Page 参数
     *
     * @param page
     */
    protected static void setLocalPage(Page page) {
        LOCAL_PAGE.set(page);   // 这个地方,就是将page,保存到ThreadLocal中
    }

  // 省略代码...
  
}
  

上面说了,我们将分页参数,保存到ThreadLocal中。下面,我们看下,什么时候使用了这个分页参数了呢?
关键就在于PageInterceptor

@Intercepts(
    {
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
    }
)
public class PageInterceptor implements Interceptor {
    private static final Log log = LogFactory.getLog(PageInterceptor.class);
  	
   // com.github.pagehelper.PageInterceptor#intercept
  	@Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement) args[0];
            Object parameter = args[1];
            RowBounds rowBounds = (RowBounds) args[2];
            ResultHandler resultHandler = (ResultHandler) args[3];
            Executor executor = (Executor) invocation.getTarget();
            CacheKey cacheKey;
            BoundSql boundSql;
            //由于逻辑关系,只会进入一次
            if (args.length == 4) {
                //4 个参数时
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
                //6 个参数时
                cacheKey = (CacheKey) args[4];
                boundSql = (BoundSql) args[5];
            }
            checkDialectExists();
            //对 boundSql 的拦截处理
            if (dialect instanceof BoundSqlInterceptor.Chain) {
  
  				// 这一步,是关键代码,在这一步中,我们调用了拦截器,是获取threadLocal中的分页参数,生成一个带分页功能的boundSql
                boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey);
            }
            List resultList;
  
  		// 省略代码...
  
  }
  

上面,我们找到了关键代码,下面,我们看下,具体的调用栈

com.github.pagehelper.PageInterceptor#intercept
  
  先进行 count 查询
  ---->com.github.pagehelper.PageInterceptor#count
  ---->com.github.pagehelper.util.ExecutorUtil#executeAutoCount
  
  接下来,再进行分页查询,这里,先获取分页参数
   ---->com.github.pagehelper.util.ExecutorUtil#pageQuery  
  ----->com.github.pagehelper.PageHelper#processParameterObject
  ---->com.github.pagehelper.dialect.AbstractHelperDialect#processParameterObject
  ---->com.github.pagehelper.dialect.helper.MySqlDialect#processPageParameter
  
  
  接下来,再拼接limit参数
  ---->com.github.pagehelper.PageHelper#getPageSql
  ---->com.github.pagehelper.dialect.AbstractHelperDialect#getPageSql
  ---->com.github.pagehelper.dialect.helper.MySqlDialect#getPageSql
  
  最后,再讲分页参数 和 sql 语句,封装成一个BoundSql对象,去执行sql查询


下面,看下, 获取分页参数的代码:

  
// com.github.pagehelper.dialect.AbstractHelperDialect#processParameterObject
  
public Object processParameterObject(MappedStatement ms, Object parameterObject, BoundSql boundSql, CacheKey pageKey) {
        //处理参数
        Page page = getLocalPage();
  
  // 省略代码...
  
   return processPageParameter(ms, paramMap, page, boundSql, pageKey);
}
  
  
  
  
// com.github.pagehelper.dialect.helper.MySqlDialect#processPageParameter  
@Override
    public Object processPageParameter(MappedStatement ms, Map<String, Object> paramMap, Page page, BoundSql boundSql, CacheKey pageKey) {
        paramMap.put(PAGEPARAMETER_FIRST, page.getStartRow());
        paramMap.put(PAGEPARAMETER_SECOND, page.getPageSize());
        //处理pageKey
        pageKey.update(page.getStartRow());
        pageKey.update(page.getPageSize());
        //处理参数配置
        if (boundSql.getParameterMappings() != null) {
            List<ParameterMapping> newParameterMappings = new ArrayList<ParameterMapping>(boundSql.getParameterMappings());
            if (page.getStartRow() == 0) {
                newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_SECOND, int.class).build());
            } else {
                newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_FIRST, long.class).build());
                newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_SECOND, int.class).build());
            }
            MetaObject metaObject = MetaObjectUtil.forObject(boundSql);
            metaObject.setValue("parameterMappings", newParameterMappings);
        }
        return paramMap;
    }  


最后,当分页查询,执行完成后,会清除,保存在ThreadLocal中的page参数

// com.github.pagehelper.PageHelper#afterAll  
public void afterAll() {
        //这个方法即使不分页也会被执行,所以要判断 null
        AbstractHelperDialect delegate = autoDialect.getDelegate();
        if (delegate != null) {
            delegate.afterAll();
            autoDialect.clearDelegate();
        }
        clearPage();
    }
  
    /**
     * 移除本地变量
     */
    public static void clearPage() {
        LOCAL_PAGE.remove();
    }  

LocalDateTime日期处理,以及对应的typeHandler

java中,定义一个属性的类型,是LocalDateTime,而在数据库中,定义这个字段是DATETIME。

mybatis,是如何完成,这2个类型的映射的呢?

首先,我们看下,最基本的prepareStatement的设值方法

  

        Connection con;
        PreparedStatement pstmt;
        try {
            con = ds.getConnection(username, password);
            con.setAutoCommit(false);
            pstmt = con.prepareStatement("UPDATE COFFEES " +
                        "SET PRICE = ? " +
                        "WHERE COF_NAME = ?");
            pstmt.setFloat(1, price);
            pstmt.setString(2, cofName);
            pstmt.executeUpdate();

            con.commit();
            pstmt.close();

        } finally {
            if (con != null) con.close();
        }
  

上面中设值方法中,有setFloat setString,但是,其实prepareStatement,还定义了一个setObject方法

// java.sql.PreparedStatement#setObject(int, java.lang.Object, java.sql.SQLType, int)
  
default void setObject(int parameterIndex, Object x, SQLType targetSqlType,
             int scaleOrLength) throws SQLException {
        throw new SQLFeatureNotSupportedException("setObject not implemented");
    }  

**这个java.sql.PreparedStatement的setObject方法很重要 **


这里的SQLType,是一个接口,jdk的rt.jar中,提供了一个实现类,就是JDBCType。而mysql-connector-java-8,也提供了一个实现类,就是MysqlType。

我们这里,要使用的就是 MysqlType

public enum JDBCType implements SQLType {
  
public enum MysqlType implements SQLType {  

也就是说,我们在调用jdbc的prepareStatement的setObject方法时,需要指定某个值对应的数据库类型。

那么,mybatis是在哪里调用这个 prepareStatement的setObject方法的呢?另外,mybatis是怎么知道,应该传入哪一种数据库类型的呢?

1. 保存每种java类型,以及对应的类型处理器到TypeHandlerRegistry

首先,mybatis有一个TypeHandlerRegistry,这个类的构造方法中,我们指定了,每一个java类型,以及所使用的的TypeHandler

  
// org.apache.ibatis.type.TypeHandlerRegistry#TypeHandlerRegistry(org.apache.ibatis.session.Configuration)
  
public TypeHandlerRegistry(Configuration configuration) {
    this.unknownTypeHandler = new UnknownTypeHandler(configuration);

    register(Boolean.class, new BooleanTypeHandler());
    register(boolean.class, new BooleanTypeHandler());
    register(JdbcType.BOOLEAN, new BooleanTypeHandler());
    register(JdbcType.BIT, new BooleanTypeHandler());

    register(Byte.class, new ByteTypeHandler());
    register(byte.class, new ByteTypeHandler());
    register(JdbcType.TINYINT, new ByteTypeHandler());

    
    register(Integer.class, new IntegerTypeHandler());
    register(int.class, new IntegerTypeHandler());
    register(JdbcType.INTEGER, new IntegerTypeHandler());

    register(Long.class, new LongTypeHandler());
    register(long.class, new LongTypeHandler());

    register(Float.class, new FloatTypeHandler());
    register(float.class, new FloatTypeHandler());
    register(JdbcType.FLOAT, new FloatTypeHandler());

    register(Double.class, new DoubleTypeHandler());
    register(double.class, new DoubleTypeHandler());
    register(JdbcType.DOUBLE, new DoubleTypeHandler());

 
    register(String.class, new StringTypeHandler());
    register(String.class, JdbcType.CHAR, new StringTypeHandler());
    register(String.class, JdbcType.CLOB, new ClobTypeHandler());
    register(String.class, JdbcType.VARCHAR, new StringTypeHandler());
    register(String.class, JdbcType.LONGVARCHAR, new StringTypeHandler());
    register(String.class, JdbcType.NVARCHAR, new NStringTypeHandler());
    register(String.class, JdbcType.NCHAR, new NStringTypeHandler());
    register(String.class, JdbcType.NCLOB, new NClobTypeHandler());
    register(JdbcType.CHAR, new StringTypeHandler());
    register(JdbcType.VARCHAR, new StringTypeHandler());
    register(JdbcType.CLOB, new ClobTypeHandler());
    register(JdbcType.LONGVARCHAR, new StringTypeHandler());
   
    register(Date.class, new DateTypeHandler());
    register(Date.class, JdbcType.DATE, new DateOnlyTypeHandler());
    register(Date.class, JdbcType.TIME, new TimeOnlyTypeHandler());
    register(JdbcType.TIMESTAMP, new DateTypeHandler());
    register(JdbcType.DATE, new DateOnlyTypeHandler());
    register(JdbcType.TIME, new TimeOnlyTypeHandler());

    register(java.sql.Date.class, new SqlDateTypeHandler());
    register(java.sql.Time.class, new SqlTimeTypeHandler());
    register(java.sql.Timestamp.class, new SqlTimestampTypeHandler());

    register(String.class, JdbcType.SQLXML, new SqlxmlTypeHandler());

  	// 省略代码
  
    register(Instant.class, new InstantTypeHandler());
  
  
   // 这个是 关键代码
    register(LocalDateTime.class, new LocalDateTimeTypeHandler());
  
  
    register(LocalDate.class, new LocalDateTypeHandler());
    register(LocalTime.class, new LocalTimeTypeHandler());
 
	// 省略代码
  
  }  

上面的构造方法,最终,会调用下面这个register方法

private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
    if (javaType != null) {
      Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
      if (map == null || map == NULL_TYPE_HANDLER_MAP) {
        map = new HashMap<>();
      }
      map.put(jdbcType, handler);
      typeHandlerMap.put(javaType, map);
    }
    allTypeHandlersMap.put(handler.getClass(), handler);
  }  

也就是说,会将javaType jdbcType 以及对应的typeHandler对象,保存到map和 typeHandlerMap中。

比如,会将javaType为LocalDateTime,以及对应的LocalDateTimeTypeHandler,保存到这个typeHandlerMap中。

2. 解析xml中编写sql语句时,根据java类型,找到对应的typeHandler,保存到parameterMapping中

  // org.apache.ibatis.builder.SqlSourceBuilder.ParameterMappingTokenHandler#handleToken
  
 public String handleToken(String content) {
      parameterMappings.add(buildParameterMapping(content));
      return "?";
    } 

上面的ParameterMappingTokenHandler,就是解析xml中编写的sql语句,得到真正可用的prepareStatement。调用了buildParameterMapping方法

private ParameterMapping buildParameterMapping(String content) {
      Map<String, String> propertiesMap = parseParameterMapping(content);
      String property = propertiesMap.get("property");
      Class<?> propertyType;
      if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params
        propertyType = metaParameters.getGetterType(property);
      } else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
        propertyType = parameterType;
      } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
        propertyType = java.sql.ResultSet.class;
      } else if (property == null || Map.class.isAssignableFrom(parameterType)) {
        propertyType = Object.class;
      } else {
        MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
        if (metaClass.hasGetter(property)) {
          propertyType = metaClass.getGetterType(property);
        } else {
          propertyType = Object.class;
        }
      }
      ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
      Class<?> javaType = propertyType;
      String typeHandlerAlias = null;
      for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
        String name = entry.getKey();
        String value = entry.getValue();
        if ("javaType".equals(name)) {
          javaType = resolveClass(value);
          builder.javaType(javaType);
        } else if ("jdbcType".equals(name)) {
  		
  // 这个地方,如果写了jdbcType,那么就根据这个jdbcType,找到对应的typeHandler
  
          builder.jdbcType(resolveJdbcType(value));
        } else if ("mode".equals(name)) {
          builder.mode(resolveParameterMode(value));
        } else if ("numericScale".equals(name)) {
          builder.numericScale(Integer.valueOf(value));
        } else if ("resultMap".equals(name)) {
          builder.resultMapId(value);
        } else if ("typeHandler".equals(name)) {
          typeHandlerAlias = value;
        } else if ("jdbcTypeName".equals(name)) {
          builder.jdbcTypeName(value);
        } else if ("property".equals(name)) {
          // Do Nothing
        } else if ("expression".equals(name)) {
          throw new BuilderException("Expression based parameters are not supported yet");
        } else {
          throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}.  Valid properties are " + PARAMETER_PROPERTIES);
        }
      }
      if (typeHandlerAlias != null) {
        builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
      }
  
  // 正常情况下,都是在这一步,根据javaType,找到对应的typeHandler
      return builder.build();
    }  

上面的builder.build(),调用到了下面的方法

  
// org.apache.ibatis.mapping.ParameterMapping.Builder#build
  
public ParameterMapping build() {
      resolveTypeHandler();
      validate();
      return parameterMapping;
    }  
  

上面的resolveTypeHandler方法,如下:


  // org.apache.ibatis.mapping.ParameterMapping.Builder#resolveTypeHandler
 private void resolveTypeHandler() {
      if (parameterMapping.typeHandler == null && parameterMapping.javaType != null) {
        Configuration configuration = parameterMapping.configuration;
        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
  
  // 这一步是关键,可以看到,就是根据javaType,去typeHandlerRegistry中找到对应的typeHandler,并且将结果,保存到parameterMapping中。
        parameterMapping.typeHandler = typeHandlerRegistry.getTypeHandler(parameterMapping.javaType, parameterMapping.jdbcType);
      }
    } 

3.根据parameterMapping,调用jdbc的prepareStatement的setObject方法,进行设值

上面,我们已经找到LocalDateTime对应的typeHandler—LocalDateTimeTypeHandler,并且将结果,保存到了parameterMapping中了。

接下来,我们来看下,是怎么调用jdbc的prepareStatement的setObject方法,进行设值的

我们看下DefaultParameterHandler#setParameters方法:

  
// org.apache.ibatis.scripting.defaults.DefaultParameterHandler#setParameters
  
@Override
  public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else {
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
                                                   
                                                   
          // 这一步是关键,拿到   parameterMapping中的typeHandler,然后调用typeHandler.setParameter                                      
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }  

这个 typeHandler.setParameter,会调用到LocalDateTimeTypeHandler的setNonNullParameter方法:

                                                   
  // org.apache.ibatis.type.LocalDateTimeTypeHandler#setNonNullParameter
                                                   
 @Override
  public void setNonNullParameter(PreparedStatement ps, int i, LocalDateTime parameter, JdbcType jdbcType)
          throws SQLException {
    ps.setObject(i, parameter);
  }                                                   
                                                   

这个ps.setObject,会调用到ClientPreparedStatement的setObject方法,而这个ClientPreparedStatement其实就是对jdbc的prepareStatement方法的实现。

public class ClientPreparedStatement extends com.mysql.cj.jdbc.StatementImpl implements JdbcPreparedStatement {
                                                   
@Override
    public void setObject(int parameterIndex, Object parameterObj) throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            ((PreparedQuery) this.query).getQueryBindings().setObject(getCoreParameterIndex(parameterIndex), parameterObj);
        }
    }
                                                   
}                                                   

看到这里,应该要告一段落了。总结下,就是,最终会使用LocalDateTimeTypeHandler的setNonNullParameter,进而调用jdbc的prepareStatement的setObject方法。



这里,再多说一些题外话。上面,我们知道ClientPreparedStatement是jdbc的prepareStatement的实现类,这里,我们看下,具体是怎么实现的呢?

ClientPreparedStatement的setObject,会调用到NativeQueryBindings的setObject方法:

 // com.mysql.cj.NativeQueryBindings#setObject(int, java.lang.Object)                                                  
                                                   
public void setObject(int parameterIndex, Object parameterObj) {
        if (parameterObj == null) {
            setNull(parameterIndex);
            return;
        }
                                                   
         // 这一步是关键,这个DEFAULT_MYSQL_TYPES ,保存了,所有javaType,以及对应的jdbcType                                  
        MysqlType defaultMysqlType = DEFAULT_MYSQL_TYPES.get(parameterObj.getClass());
        if (defaultMysqlType == null) {
            Optional<MysqlType> mysqlType = DEFAULT_MYSQL_TYPES.entrySet().stream().filter(m -> m.getKey().isAssignableFrom(parameterObj.getClass()))
                    .map(m -> m.getValue()).findFirst();
            if (mysqlType.isPresent()) {
                defaultMysqlType = mysqlType.get();
            }
        }
 // 找到了jdbcType,然后会调用setObject方法,将jdbcType设置进去
        setObject(parameterIndex, parameterObj, defaultMysqlType, -1);
    }                                                   

后面的代码,我们暂时就不看了。我们可以看到的是,com.mysql.cj.NativeQueryBindings#DEFAULT_MYSQL_TYPES,保存了所有javaType,以及对应的jdbcType。 如下:

// com.mysql.cj.NativeQueryBindings#DEFAULT_MYSQL_TYPES
  
static Map<Class<?>, MysqlType> DEFAULT_MYSQL_TYPES = new HashMap<>();
    static {
        DEFAULT_MYSQL_TYPES.put(BigDecimal.class, MysqlType.DECIMAL);
        DEFAULT_MYSQL_TYPES.put(BigInteger.class, MysqlType.BIGINT);
        DEFAULT_MYSQL_TYPES.put(Blob.class, MysqlType.BLOB);
        DEFAULT_MYSQL_TYPES.put(Boolean.class, MysqlType.BOOLEAN);
        DEFAULT_MYSQL_TYPES.put(Byte.class, MysqlType.TINYINT);
        DEFAULT_MYSQL_TYPES.put(byte[].class, MysqlType.BINARY);
        DEFAULT_MYSQL_TYPES.put(Calendar.class, MysqlType.TIMESTAMP);
        DEFAULT_MYSQL_TYPES.put(Clob.class, MysqlType.TEXT);
        DEFAULT_MYSQL_TYPES.put(Date.class, MysqlType.DATE);
        DEFAULT_MYSQL_TYPES.put(java.util.Date.class, MysqlType.TIMESTAMP);
        DEFAULT_MYSQL_TYPES.put(Double.class, MysqlType.DOUBLE);
        DEFAULT_MYSQL_TYPES.put(Duration.class, MysqlType.TIME);
        DEFAULT_MYSQL_TYPES.put(Float.class, MysqlType.FLOAT);
        DEFAULT_MYSQL_TYPES.put(InputStream.class, MysqlType.BLOB);
        DEFAULT_MYSQL_TYPES.put(Instant.class, MysqlType.TIMESTAMP);
        DEFAULT_MYSQL_TYPES.put(Integer.class, MysqlType.INT);
        DEFAULT_MYSQL_TYPES.put(LocalDate.class, MysqlType.DATE);
  
  		// 这个就是,我们切入话题的LocalDateTime
        DEFAULT_MYSQL_TYPES.put(LocalDateTime.class, MysqlType.DATETIME); // default JDBC mapping is TIMESTAMP, see B-4
  
  
        DEFAULT_MYSQL_TYPES.put(LocalTime.class, MysqlType.TIME);
        DEFAULT_MYSQL_TYPES.put(Long.class, MysqlType.BIGINT);
        DEFAULT_MYSQL_TYPES.put(OffsetDateTime.class, MysqlType.TIMESTAMP); // default JDBC mapping is TIMESTAMP_WITH_TIMEZONE, see B-4
        DEFAULT_MYSQL_TYPES.put(OffsetTime.class, MysqlType.TIME); // default JDBC mapping is TIME_WITH_TIMEZONE, see B-4
        DEFAULT_MYSQL_TYPES.put(Reader.class, MysqlType.TEXT);
        DEFAULT_MYSQL_TYPES.put(Short.class, MysqlType.SMALLINT);
        DEFAULT_MYSQL_TYPES.put(String.class, MysqlType.VARCHAR);
        DEFAULT_MYSQL_TYPES.put(Time.class, MysqlType.TIME);
        DEFAULT_MYSQL_TYPES.put(Timestamp.class, MysqlType.TIMESTAMP);
        DEFAULT_MYSQL_TYPES.put(ZonedDateTime.class, MysqlType.TIMESTAMP); // no JDBC mapping is defined
    }  
  


java.util.Date日期处理 以及对应的typeHandler

上面,我们已经看了LocalDateTime日期的处理,下面,我们再看看 java.util.Date日期。

我们首先,明确一点: java.util.Date 包含日期和时间 ,而 java.sql.Date只包含日期,不包含time

所以,单纯的从理论上来说, java.util.Date 变成 mysql中的datetime是没有逻辑上的问题,他们都包含日期 和 time



我们,先看下TypeHandlerRegistry的构造方法:


 register(Date.class, new DateTypeHandler());
  

也就是说,由DateTypeHandler来处理Date类型的属性

DateTypeHandler的setNonNullParameter如下:

public class DateTypeHandler extends BaseTypeHandler<Date> {

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, Date parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setTimestamp(i, new Timestamp(parameter.getTime()));
  }  

这里,可以看到,是调用了prepareStatement的setTimestamp方法,并且将Date转为了java.sql.Timestamp,最后设值的。

后面的调用代码,暂时就不看了,反正最后,设置的SQLType类型,是 MysqlType.TIMESTAMP


评论