暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Mysql插入超过长度字符串会发生什么

程序员升级之路 2021-07-04
1510


一、问题说明

一朋友线上用的mysql5.6.17,sql_mode配的STRICT_TRANS_TABLES,这个配置的具体含义就不在这里说明了,这个是比较严格的模式;


有一天发生一个奇怪的问题,为了简化说明,用以下表结构进行模拟:


    CREATE TABLE `user1` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `name` varchar(10) DEFAULT NULL,
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;


    复制

    上面有个表user1有个name字段,定义长度只有10。


    具体执行的就是下面2 SQL,其中第一个是失败的,但第二个是成功的

      INSERT INTO `user1`(name) VALUES('123451234512')


      INSERT INTO `user1`(name) VALUES('1234512345 ')
      复制

      其中第一条sql语句长度超过10了,并且没有多余的空格;

      第2条则特殊一些,总长度超过10,并且尾部是空格,即去掉空格后总长度不超过10。


       先手动执行下看下结果:


      可以看到sql1失败报太长了,sql2执行成功,但只有一个警告。


      二、源码分析

      在mysql_insert函数上打断点:

         while ((values= its++))
        {
        if (fields.elements || !value_count)
        {
        restore_record(table,s->default_values); // Get empty record


        /*
        Check whether default values of the fields not specified in column list
        are correct or not.
        */
        if (validate_default_values_of_unset_fields(thd, table))
        {
        error= 1;
        break;
        }


        if (fill_record_n_invoke_before_triggers(thd, fields, *values, 0,
        table->triggers,
        TRG_EVENT_INSERT))
        {
        if (values_list.elements != 1 && ! thd->is_error())
        {
        info.stats.records++;
        continue;
        }
        /*
        TODO: set thd->abort_on_warning if values_list.elements == 1
        and check that all items return warning in case of problem with
        storing field.
        */
        error=1;
        break;
        }
        }
        复制


        比较关键的是函数fill_record_n_invoke_before_triggers,跟进去一直到Field_varstring类的store函数;


        mysql对于每种数据类型抽象一个类,varchar对应的是Field_varstring:

          type_conversion_status Field_varstring::store(const char *from,uint length,
          const CHARSET_INFO *cs)
          {
          ASSERT_COLUMN_MARKED_FOR_WRITE;
          uint copy_length;
          const char *well_formed_error_pos;
          const char *cannot_convert_error_pos;
          const char *from_end_pos;


          copy_length= well_formed_copy_nchars(field_charset,
          (char*) ptr + length_bytes,
          field_length,
          cs, from, length,
          field_length field_charset->mbmaxlen,
          &well_formed_error_pos,
          &cannot_convert_error_pos,
                                                 &from_end_pos);
          if (length_bytes == 1)
          *ptr= (uchar) copy_length;
          else
          int2store(ptr, copy_length);


          return check_string_copy_error(well_formed_error_pos,
          cannot_convert_error_pos, from_end_pos,
          from + length, true, cs);
          }
          复制

          这里可以看from就是我们要插入的内容:


          因为类型是varchar(10),所以只拷贝10个字符,重点看函数check_string_copy_error:

            type_conversion_status
            Field_longstr::check_string_copy_error(const char *well_formed_error_pos,
            const char *cannot_convert_error_pos,
            const char *from_end_pos,
            const char *end,
            bool count_spaces,
            const CHARSET_INFO *cs) const
            {
            const char *pos;
            char tmp[32];
            THD *thd= table->in_use;


            if (!(pos= well_formed_error_pos) &&
            !(pos= cannot_convert_error_pos))
            return report_if_important_data(from_end_pos, end, count_spaces);


            convert_to_printable(tmp, sizeof(tmp), pos, (end - pos), cs, 6);


            push_warning_printf(thd,
            Sql_condition::WARN_LEVEL_WARN,
            ER_TRUNCATED_WRONG_VALUE_FOR_FIELD,
            ER(ER_TRUNCATED_WRONG_VALUE_FOR_FIELD),
            "string", tmp, field_name,
            thd->get_stmt_da()->current_row_for_warning());
            return TYPE_WARN_TRUNCATED;
            }
            复制


            再跟进report_if_important_data函数:

              type_conversion_status
              Field_longstr::report_if_important_data(const char *pstr, const char *end,
              bool count_spaces) const
              {
              if ((pstr < end) && table->in_use->count_cuted_fields)
              {
              if (test_if_important_data(field_charset, pstr, end))
              {
              if (table->in_use->abort_on_warning)
              set_warning(Sql_condition::WARN_LEVEL_WARN, ER_DATA_TOO_LONG, 1);
              else
              set_warning(Sql_condition::WARN_LEVEL_WARN, WARN_DATA_TRUNCATED, 1);
              return TYPE_WARN_TRUNCATED;
              }
              else if (count_spaces)
              { /* If we lost only spaces then produce a NOTE, not a WARNING */
              set_warning(Sql_condition::WARN_LEVEL_NOTE, WARN_DATA_TRUNCATED, 1);
              return TYPE_NOTE_TRUNCATED;
              }
              }
              return TYPE_OK;
              }
              复制

              这里pstr是<end,因为前面讲了只拷贝10个字符,再看test_if_important_data函数:

                static bool
                test_if_important_data(const CHARSET_INFO *cs, const char *str,
                const char *strend)
                {
                if (cs != &my_charset_bin)
                str+= cs->cset->scan(cs, str, strend, MY_SEQ_SPACES);
                return (str < strend);
                }
                复制


                这里scan最终对应的是my_scan_8bit函数:

                  size_t my_scan_8bit(const CHARSET_INFO *cs, const char *str, const char *end,
                  int sq)
                  {
                  const char *str0= str;
                  switch (sq)
                  {
                  case MY_SEQ_INTTAIL:
                  if (*str == '.')
                  {
                  for(str++ ; str != end && *str == '0' ; str++);
                  return (size_t) (str - str0);
                  }
                  return 0;


                  //进入这个逻辑
                  case MY_SEQ_SPACES:
                  for ( ; str < end ; str++)
                  {
                  if (!my_isspace(cs,*str))
                  break;
                  }
                  return (size_t) (str - str0);
                  default:
                  return 0;
                  }
                  }
                  复制

                  因为传递的是MY_SEQ_SPACES,所以这里会判断my_isspace是否空格,如果是由跳过,因此尾部是空格由会跳过,即认为不会超过长度。


                  因此test_if_important_data会返回失败,设置相应告警,因sql_mode不同从而导致两者的错误码不一样:

                     if (table->in_use->abort_on_warning)
                    set_warning(Sql_condition::WARN_LEVEL_WARN, ER_DATA_TOO_LONG, 1);
                    else
                    set_warning(Sql_condition::WARN_LEVEL_WARN, WARN_DATA_TRUNCATED, 1);
                    return TYPE_WARN_TRUNCATED;
                    复制


                    为什么这么设计,应该也是从数据正确性来看的,删除空格不影响最终数据的,但删除非空格的数据真的是丢数据了。


                    三、总结

                    1、varchar字段mysql内部用Field_varstring表示,插入时mysql会调用字段的store方法进行数据复制;


                    2、Field_varstring继承Field_longstr

                    并调用report_if_important_data来检查数据长度;


                    3、report_if_important_data调用test_if_important_data来检查是否超过长度,后者会根据每种字符集来做处理,本例是会略过相应空格。




                    文章转载自程序员升级之路,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                    评论