zoukankan      html  css  js  c++  java
  • Mybatis+Oracle搭配insert空值报错之myBatis+mysql驱动+oracle驱动的源码分析

    为了便于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,我觉得自己太不要脸喽)

     

  • 相关阅读:
    Jquery里live事件移除原因
    js委托事件-addEventListeners(冒泡方向)
    后端同同不肯给我算好的时间差给我,只好自己写了:
    js滚动到顶部底部代码
    浏览器地址栏运行HTML代码(谷歌)
    黑马vue---56-58、vue组件创建的三种方式
    黑马vue---46、vue使用过渡类名实现动画
    黑马vue---28、vue中全局过滤器的基本使用
    vue devtools无法使用
    红心点赞效果的实现
  • 原文地址:https://www.cnblogs.com/guoyansi19900907/p/12696023.html
Copyright © 2011-2022 走看看