欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

MyBatis中Executor源码解析之BatchExecutor搞不懂

程序员文章站 2022-03-25 16:25:07
为了便于SEO搜索到,首先把报错内容贴出来吧 不同版本的Oracle驱动会报不同的错 1 2 com.oracle 3 ojdbc6 4 1.0 5 ......

为了便于seo搜索到,首先把报错内容贴出来吧 

不同版本的oracle驱动会报不同的错 

1 <dependency>
2     <groupid>com.oracle</groupid>
3     <artifactid>ojdbc6</artifactid>
4     <version>1.0</version>
5 </dependency>

 报错如下:

error updating database.  cause: org.apache.ibatis.type.typeexception: could not set parameters for mapping: parametermapping{property='name', mode=in, javatype=class java.lang.string, jdbctype=null, numericscale=null, resultmapid='null', jdbctypename='null', expression='null'}. cause: org.apache.ibatis.type.typeexception: error setting null for parameter #1 with jdbctype other . try setting a different jdbctype for this parameter or a different jdbctypefornull configuration property. cause: java.sql.sqlexception: 无效的列类型: 1111

<dependency>
    <groupid>com.oracle</groupid>
    <artifactid>ojdbc4</artifactid>
    <version>1.0</version>
</dependency>

报错如下:

error updating database.  cause: org.apache.ibatis.type.typeexception: could not set parameters for mapping: parametermapping{property='name', mode=in, javatype=class java.lang.string, jdbctype=null, numericscale=null, resultmapid='null', jdbctypename='null', expression='null'}. cause: org.apache.ibatis.type.typeexception: error setting null for parameter #1 with jdbctype other . try setting a different jdbctype for this parameter or a different jdbctypefornull configuration property. cause: java.sql.sqlexception: 无效的列类型

如果不想看下面裹脚布版的源码分析,直接看我这篇博客寻找解决办法吧:mybatis+oracle在执行insert时空值报错之从源码寻找解决办法

有异常那就一点一点的对着mybatis调试追踪吧。避免啰嗦,就用ojdbc6调试吧;因为ojbc6与mybatis的最新版本搭配更稳定。

至于为什么不稳定可以看看我的这篇博客:mybatis+oracle时出现的错误: method oracle/jdbc/driver/oracleresultsetimpl.isclosed()z is abstract

便于源码分析,还是先上demo吧。

mybatis-oracle-config.xml 

 1 <?xml version="1.0" encoding="utf-8"?>
 2 <!doctype configuration public "-//mybatis.org//dtd config 3.0//en"
 3         "http://mybatis.org/dtd/mybatis-3-config.dtd">
 4 
 5 <configuration>
 6     <properties>
 7         <property name="driver" value="oracle.jdbc.driver.oracledriver"/>
 8         <property name="url" value="jdbc:oracle:thin:@127.0.0.1:1521/orcl"/>
 9     </properties>    
10 
11     <environments default="dev">
12         <environment id="dev">   
13             <datasource type="pooled">
14                 <property name="driver" value="${driver}"></property>
15                 <property name="url" value="${url}"></property>
16                 <property name="username" value="gys"></property>
17                 <property name="password" value="gys"></property>
18             </datasource>
19         </environment>
20 
21     </environments>
22     <mappers>       
23         <mapper resource="mapper/oracle/user.xml"></mapper>
24     </mappers>
25 </configuration>

 user.xml

 1 <?xml version="1.0" encoding="utf-8"?>
 2 <!doctype mapper public "-//mybatis.org//dtd mapper 3.0//en"
 3         "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 4 <mapper namespace="dao.oracle.iusermapper">
 5     <insert id="insertuser" parametertype="model.oracle.user">
 6         insert into users
 7         (name,age)
 8         values
 9         (#{name},#{age})
10     </insert>
11 </mapper>

main方法入口: 

 1  public static void main(string[] args) throws exception{
 2         sqlsessionfactorybuilder builder=new sqlsessionfactorybuilder();
 3         sqlsessionfactory sqlsessionfactory=builder.build(resources.getresourceasstream("mybatis-oracle-config.xml"),"dev");
 4         sqlsession sqlsession=sqlsessionfactory.opensession(true);
 5         iusermapper usermapper=sqlsession.getmapper(iusermapper.class);
 6         user user=new user();
 7    //此处不设置,故意插入null数据
 8         //user.setname("gggg");
 9         user.setage(20);
10         int count=usermapper.insertuser(user);
11         system.out.println(count == 1 ? "插入成功" : "插入失败");        
12         sqlsession.close();
13     }

 运行结果就是上面的报错内容了。

我们直接从simpleexecutor.java执行器开始分析吧。

不了解执行器的可以看看我的这篇博客:mybatis中executor源码解析之batchexecutor搞不懂

MyBatis中Executor源码解析之BatchExecutor搞不懂

 这个地方的stmt是指向oraclepreparedstatementwrapper.java这个类的;

看来这个是oracle驱动提供的类,继承了jdbc的statement接口

同时这个handler是指向routingstatementhandler类

第88行代码是开始进行sql参数进行设置的方法。我们追踪进去看看是如何实现的。

MyBatis中Executor源码解析之BatchExecutor搞不懂

直接去preparedstatementhandler类吧;因为routingstatmenthandler继承自preparedstatmenthandler类。

MyBatis中Executor源码解析之BatchExecutor搞不懂

 

 继续看setparameters()源码:

 1 @override
 2 public void setparameters(preparedstatement ps) {
 3 //获取该sql中所有的参数映射对象
 4  list<parametermapping> parametermappings = boundsql.getparametermappings();
 5   if (parametermappings != null) {
 6    for (int i = 0; i < parametermappings.size(); i++) {
 7       parametermapping parametermapping = parametermappings.get(i);    
 8       //如果不是出参
 9       if (parametermapping.getmode() != parametermode.out) {
10        object value;
11       //获取参数的属性名,比如name,age
12        string propertyname = parametermapping.getproperty();       
13          metaobject metaobject = configuration.newmetaobject(parameterobject);
14          //获取参数的预设值,比如name=5,这里value就是5
15          value = metaobject.getvalue(propertyname);         
16          //根据参数获取类型转换器
17        typehandler typehandler = parametermapping.gettypehandler();
18          //获取jdbc类型,这里是枚举;如果是空着,返回other枚举值,并且枚举的code属性值是1111
19        jdbctype jdbctype = parametermapping.getjdbctype();
20        //这行条件基本不会执行,因为jdbctype在build时候,始终都会有值,空值的话默认是other枚举
21        if (value == null && jdbctype == null) {
22          jdbctype = configuration.getjdbctypefornull();
23        }
24        //参数设置开始交给类型转换器进行赋值
25        typehandler.setparameter(ps, i + 1, value, jdbctype);
26      }
27    }
28  }
29 }   

上面代码去除了干扰的代码,添加了注释,继续向下追踪

MyBatis中Executor源码解析之BatchExecutor搞不懂

 

 typehandler指向stringtypehandler类,这里面没有separameter()方法,直接去父级basetypehandler类中找吧。

setparameter()源码

下面代码去除多余干扰的代码

 1  @override
 2   public void setparameter(preparedstatement ps, int i, t parameter, jdbctype jdbctype) throws sqlexception {
 3       //参数值为空
 4     if (parameter == null) {
 5         //jdbctype为空,这里不可能为空,最起码是默认枚举other
 6       if (jdbctype == null) {
 7         throw new typeexception("jdbc requires that the jdbctype must be specified for all nullable parameters.");
 8       }
 9       try {
10           /**
11           i是参数位置,第一个参数这里就是1
12         jdbctype.type_code是枚举的编码值,这里空值是1111·    
13           **/
14         ps.setnull(i, jdbctype.type_code);
15       } catch (sqlexception e) {
16           //这里的异常内容是不是很熟悉,就是我们在控制台看到的内容。看来异常就是上面setnull方法抛出的了
17         throw new typeexception("error setting null for parameter #" + i + " with jdbctype " + jdbctype + " . "
18               + "try setting a different jdbctype for this parameter or a different jdbctypefornull configuration property. "
19               + "cause: " + e, e);
20       }
21     }
    //如果不是空值,就直接走这里了
    else{
      setnonnullparameter(ps, i, parameter, jdbctype);
    }
22 }

我不明白为什么要把jdbctype为空是,编码设置成1111;这个值是有什么特殊的含义么?有知道的,麻烦告知一下

继续查看setnull()方法

setnull()方法源码

MyBatis中Executor源码解析之BatchExecutor搞不懂

  继续去t4cpreparedstatement中查看setnull()源码

MyBatis中Executor源码解析之BatchExecutor搞不懂

  继续追踪setnullcritical()源码

MyBatis中Executor源码解析之BatchExecutor搞不懂

 

 继续追踪到getinternaltype()源码

获取oracle内部的字段类型

  1 int getinternaltype(int var1) throws sqlexception {
  2         boolean var2 = false;
  3         short var4;
  4         switch(var1) {
  5         case -104:
  6             var4 = 183;
  7             break;
  8         case -103:
  9             var4 = 182;
 10             break;
 11         case -102:
 12             var4 = 231;
 13             break;
 14         case -101:
 15             var4 = 181;
 16             break;
 17         case -100:
 18         case 93:
 19             var4 = 180;
 20             break;
 21         case -16:
 22         case -1:
 23             var4 = 8;
 24             break;
 25         case -15:
 26         case -9:
 27         case 12:
 28             var4 = 1;
 29             break;
 30         case -14:
 31             var4 = 998;
 32             break;
 33         case -13:
 34             var4 = 114;
 35             break;
 36         case -10:
 37             var4 = 102;
 38             break;
 39         case -8:
 40             var4 = 104;
 41             break;
 42         case -7:
 43         case -6:
 44         case -5:
 45         case 2:
 46         case 3:
 47         case 4:
 48         case 5:
 49         case 6:
 50         case 7:
 51         case 8:
 52             var4 = 6;
 53             break;
 54         case -4:
 55             var4 = 24;
 56             break;
 57         case -3:
 58         case -2:
 59             var4 = 23;
 60             break;
 61         case 0:
 62             var4 = 995;
 63             break;
 64         case 1:
 65             var4 = 96;
 66             break;
 67         case 70:
 68             var4 = 1;
 69             break;
 70         case 91:
 71         case 92:
 72             var4 = 12;
 73             break;
 74         case 100:
 75             var4 = 100;
 76             break;
 77         case 101:
 78             var4 = 101;
 79             break;
 80         case 999:
 81             var4 = 999;
 82             break;
 83         case 2002:
 84         case 2003:
 85         case 2007:
 86         case 2008:
 87         case 2009:
 88             var4 = 109;
 89             break;
 90         case 2004:
 91             var4 = 113;
 92             break;
 93         case 2005:
 94         case 2011:
 95             var4 = 112;
 96             break;
 97         case 2006:
 98             var4 = 111;
 99             break;
100         default:
101             sqlexception var3 = databaseerror.createsqlexception(this.getconnectionduringexceptionhandling(), 4, integer.tostring(var1));
102             var3.fillinstacktrace();
103             throw var3;
104         }
105 
106         return var4;
107     }

因为case中没有1111匹配项,所以只能进入default中了。

default中定义了一个异常类,并在最后义无反顾的throw掉了。一个空值的赋值处理总算告一段落了。

这个地方不是太明白什么意思,这些case 后面的数值都代表什么意思,我看只有oracle驱动开发的人才能明白了。

这个地方的设计好奇怪啊;

上面setnullcritical()源码中的case数值,大致可以推断字符串类型空值的编号是1,8,96,995,那么getinternaltype()中的case数值推断sqltype=70,-8,1,0;

等会下面jdbc例子中,将刚才我们推断的sqltype值设置到空值里面取,试试能否成功。

mybatis+ojbc6对于传入空值抛出的异常是:" cause: java.sql.sqlexception: 无效的列类型: 1111"

这里的1111是mybatis中对于不明确的jdbctype参数给出的编号。和oracle驱动是没有半毛钱关系的。

到这位置从mybatis到ojdbc6驱动的源码分析算是结束了。

 

那么java能否向oracle中发送一条带有未经赋值的sql语句呢?

mybatis是对jdbc的封装,我们踢掉mybatis,直接用jdbc+oracle驱动来验证上面的观点。

 1 public static void main(string[] args) throws exception{     
 2     string sql="insert into users(name,age) values(?,?)";
 3     class.forname("oracle.jdbc.driver.oracledriver");
 4     connection connection=drivermanager.getconnection("jdbc:oracle:thin:@127.0.0.1:1521/orcl","gys","gys");
 5     preparedstatement ps=connection.preparestatement(sql);
 6     ps.setint(2,30);
 7     //这里故意不对第一个参数进行设置
 8     //ps.setstring(1,null);
 9     parametermetadata metadata=ps.getparametermetadata();
10     system.out.println(metadata.getparametercount());//打印参数个数
11     int count=ps.executeupdate();
12     system.out.println(count == 1 ? "插入成功" : "插入失败");
13     connection.close();
14 }

执行结果:

MyBatis中Executor源码解析之BatchExecutor搞不懂

  jdbc也不能向oracle中插入一个未经赋值的sql语句;但是如果将第8行代码注释放开,又可以进行正确的操作了。

疑问来了,为什么mybatis+oracle和jdbc+oracle都没有对参数赋值,为什么出现的报错内容不一样?

因为mybatis对空值做了判断,如果为空了直接交给ojdbc6的预编译对象的setnull()方法处理了;

异常是在参数处理阶段抛出的异常,还没有到数据库执行的这一步;而jdbc是报错是在数据库执行sql的时候报错的;属于sql语法错误了。

我们可以把上面的jdbc代码做一个修改,也会出现和mybatis一样的异常错误

 1 public static void main(string[] args) throws exception{     
 2     string sql="insert into users(name,age) values(?,?)";
 3     class.forname("oracle.jdbc.driver.oracledriver");
 4     connection connection=drivermanager.getconnection("jdbc:oracle:thin:@127.0.0.1:1521/orcl","gys","gys");
 5     preparedstatement ps=connection.preparestatement(sql);
 6     ps.setint(2,30);
 7     //这里故意不对第一个参数进行设置
 8     //ps.setstring(1,null);
 9     ps.setnull(1,1111);
10     parametermetadata metadata=ps.getparametermetadata();
11     system.out.println(metadata.getparametercount());
12     int count=ps.executeupdate();
13     system.out.println(count == 1 ? "插入成功" : "插入失败");
14     connection.close();
15 }

运行结果:

MyBatis中Executor源码解析之BatchExecutor搞不懂

这里没有上面那个“cause: org.apache.ibatis.type.typeexception.......”之类的关键词是因为ojdbc6抛出的异常被mybatis捕获了,mybatis添加了一些自己的内容。

继续修改上面jdbc中的源码,测试一遍

 1  string sql="insert into users(name,age) values(?,?)";
 2         class.forname("oracle.jdbc.driver.oracledriver");
 3         connection connection=drivermanager.getconnection("jdbc:oracle:thin:@127.0.0.1:1521/orcl","gys","gys");
 4         preparedstatement ps=connection.preparestatement(sql);
 5         ps.setint(2,30);
 6         //ps.setstring(1,null);
 7         ps.setnull(1,70);
 8         parametermetadata metadata=ps.getparametermetadata();
 9         system.out.println("参数个数:"+metadata.getparametercount());
10         int count=ps.executeupdate();
11         system.out.println(count == 1 ? "插入成功" : "插入失败");
12         connection.close();

MyBatis中Executor源码解析之BatchExecutor搞不懂

 

能够正确的插入数据,说明上面源码分析中的sqltype推断是正确的了。

由此可以推断出mybatis在空值处理这一块是有兼容性问题的。 

虽然mybatis在oracle数据库时,遇到未赋值的空值会报错,但是mysql数据库却不会报错,

简单的对mysql中对于空值处理做一个源码分析吧

mybatis对于空值处理的部分都是一样的,不一样的是mysql驱动和oracle驱动对空值处理方式不一样。

MyBatis中Executor源码解析之BatchExecutor搞不懂

  这个预编译对象指向mysql驱动的clientpreparedstatement类。

后面就代码是msyql对于空值的处理了;将会进入mysql驱动源码的分析了。

setnull()源码

MyBatis中Executor源码解析之BatchExecutor搞不懂

  截图中红框注释看到没有:mysql忽略sqltype。所以mybatis中给sqltype赋值成1111,对mysq来说解析空值完全没有影响。

getcoreparameterindex()源码

1  protected final int getcoreparameterindex(int paramindex) throws sqlexception {
2         int parameterindexoffset = getparameterindexoffset();
3         checkbounds(paramindex, parameterindexoffset);//这里是对参数进行校验,而且是值传递,并不会对这两个值有任何修改的顾虑,就不进去看了
4         return paramindex - 1 + parameterindexoffset;//1-1+0
5     }

getparameterindexoffset()源码

1 //就是返回0,这是指定mysql参数解析的索引起始位置
2 protected int getparameterindexoffset() {
3         return 0;
4     }

所以上面截图的第1650行代码中调用是下面这样子的

 ((preparedquery<?>) this.query).getquerybindings().setnull(0); // mysql ignores sqltype

这里的0就是参数在mysql中的索引位置。

这里从setnull()的调用方式来看,基本可以推断出getquerybindings()返回的是一个参数的对象,里面包含了该参数的各种信息,提供给mysql数据库进行解析参数使用;

这个对象也只有mysql数据库能够知道里面各个字段的意思(这个mysql驱动也是mysql数据库提供的)

算了,还是继续分析上面的setnull()方法吧。

 MyBatis中Executor源码解析之BatchExecutor搞不懂

 bindvalues是一个数组,存放的是各个参数对象;

582行代码就是调用第一个参数对象的setnull()方法;设置是否是空值。

至于setvalue()我们继续往下看。

setvalue()源码

1  public synchronized final void setvalue(int paramindex, string val, mysqltype type) {
2 //将参数值转化成字节数组
3         byte[] parameterasbytes = stringutils.getbytes(val, this.charencoding);
4         setvalue(paramindex, parameterasbytes, type);
5     }

这里还有一个setvalue()方法

public synchronized final void setvalue(int paramindex, byte[] val, mysqltype type) {
//参数对象设置字节数组,实际上参数值就是以字节数组的方式传递给数据库的,并不是我们想象的1.2或者张三,李四
        this.bindvalues[paramindex].setbytevalue(val);
//设置参数在mysql数据库中数据类型,例如:varchar,int...
        this.bindvalues[paramindex].setmysqltype(type);
    }

到这位置从mybatis到mysql驱动的源码分析总算是结束了。 

我很好奇在执行数据库操作之前,mysql提供的预编译器对象是个什么样子。

直接找到mybatis源码的preparedstatementhandler类

MyBatis中Executor源码解析之BatchExecutor搞不懂

  这个ps就是我们要看的预编译器对象。对象字段实在太多只能分多个截图了。

MyBatis中Executor源码解析之BatchExecutor搞不懂

 MyBatis中Executor源码解析之BatchExecutor搞不懂

  bindvalues就是我们刚才源码分析看到的值

MyBatis中Executor源码解析之BatchExecutor搞不懂

 上图显示的内容是不是和我们分析的源码完全一致。

从图中可以看出这两个参数是以两个对象的方式存放在预编译器中,传递给mysql数据库,供mysql数据库进行解析。

 

利用mybatis插入空值给数据库;mysql能够正常执行,而oracle却抛出异常;

这两种截然不同的表现给程序员造成了困扰,那么这个抛异常的锅到底应该是谁来背呢?

当然是mybatis来背锅喽。oracle和mysql都根据jdbc接口来提供了自己的实现方法,

而mybatis作为一个封装了jdbc的框架,没有封装到位,出现了相同的方法在不同数据库的兼容问题。

(ps:免费的框架天天用,大把大把的钞票每月每月的领,还这样埋怨mybatis,我觉得自己太不要脸喽)