為了便於SEO搜索到,首先把報錯內容貼出來吧 不同版本的Oracle驅動會報不同的錯 1 <dependency> 2 <groupId>com.oracle</groupId> 3 <artifactId>ojdbc6</artifactId> 4 <version>1.0</version> 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搞不懂
這個地方的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()源碼
View Code
繼續追蹤到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,我覺得自己太不要臉嘍)