MyBatis中Executor源码解析之BatchExecutor搞不懂
为了便于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搞不懂
这个地方的stmt是指向oraclepreparedstatementwrapper.java这个类的;
看来这个是oracle驱动提供的类,继承了jdbc的statement接口
同时这个handler是指向routingstatementhandler类
第88行代码是开始进行sql参数进行设置的方法。我们追踪进去看看是如何实现的。
直接去preparedstatementhandler类吧;因为routingstatmenthandler继承自preparedstatmenthandler类。
继续看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 }
上面代码去除了干扰的代码,添加了注释,继续向下追踪
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()方法源码
继续去t4cpreparedstatement中查看setnull()源码
继续追踪setnullcritical()源码
继续追踪到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 }
执行结果:
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 }
运行结果:
这里没有上面那个“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();
能够正确的插入数据,说明上面源码分析中的sqltype推断是正确的了。
由此可以推断出mybatis在空值处理这一块是有兼容性问题的。
虽然mybatis在oracle数据库时,遇到未赋值的空值会报错,但是mysql数据库却不会报错,
简单的对mysql中对于空值处理做一个源码分析吧
mybatis对于空值处理的部分都是一样的,不一样的是mysql驱动和oracle驱动对空值处理方式不一样。
这个预编译对象指向mysql驱动的clientpreparedstatement类。
后面就代码是msyql对于空值的处理了;将会进入mysql驱动源码的分析了。
setnull()源码
截图中红框注释看到没有: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()方法吧。
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类
这个ps就是我们要看的预编译器对象。对象字段实在太多只能分多个截图了。
bindvalues就是我们刚才源码分析看到的值
上图显示的内容是不是和我们分析的源码完全一致。
从图中可以看出这两个参数是以两个对象的方式存放在预编译器中,传递给mysql数据库,供mysql数据库进行解析。
利用mybatis插入空值给数据库;mysql能够正常执行,而oracle却抛出异常;
这两种截然不同的表现给程序员造成了困扰,那么这个抛异常的锅到底应该是谁来背呢?
当然是mybatis来背锅喽。oracle和mysql都根据jdbc接口来提供了自己的实现方法,
而mybatis作为一个封装了jdbc的框架,没有封装到位,出现了相同的方法在不同数据库的兼容问题。
(ps:免费的框架天天用,大把大把的钞票每月每月的领,还这样埋怨mybatis,我觉得自己太不要脸喽)
上一篇: 软件工程第三次作业