爲了便於SEO搜索到,首先把報錯內容貼出來吧 html
不一樣版本的Oracle驅動會報不一樣的錯 java
1 <dependency> 2 <groupId>com.oracle</groupId> 3 <artifactId>ojdbc6</artifactId> 4 <version>1.0</version> 5 </dependency>
報錯以下:mysql
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>
報錯以下:sql
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調試追蹤吧。避免囉嗦,就用ojdbc6調試吧;由於ojbc6與mybatis的最新版本搭配更穩定。express
至於爲何不穩定能夠看看個人這篇博客:MyBatis+Oracle時出現的錯誤: Method oracle/jdbc/driver/OracleResultSetImpl.isClosed()Z is abstractapache
便於源碼分析,仍是先上Demo吧。數組
mybatis-oracle-config.xml mybatis
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()源碼
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驅動開發的人才能明白了。
這個地方的設計好奇怪啊;
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添加了一些本身的內容。
雖然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,我以爲本身太不要臉嘍)