美文网首页
Calcite 中文编码问题

Calcite 中文编码问题

作者: ni_d58f | 来源:发表于2019-05-13 15:11 被阅读0次

    1. 问题

    前一阵子做有关Calcite的项目时出了这样的问题

     select id from user_behavior where rlike(text, '.*中国.*')
    

    执行的时候一直报错, 提示用'中国' 无法用'ISO-8859-1'编码

    Caused by: org.apache.calcite.runtime.CalciteException: Failed to encode '.*中国.*' in character set 'ISO-8859-1'
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
        at org.apache.calcite.runtime.Resources$ExInstWithCause.ex(Resources.java:463)
        at org.apache.calcite.runtime.Resources$ExInst.ex(Resources.java:572)
        at org.apache.calcite.util.NlsString.<init>(NlsString.java:139)
        at org.apache.calcite.util.NlsString.<init>(NlsString.java:112)
        at org.apache.calcite.rex.RexBuilder.makeLiteral(RexBuilder.java:888)
        at org.apache.calcite.rex.RexBuilder.makeCharLiteral(RexBuilder.java:1108)
        at org.apache.calcite.sql2rel.SqlNodeToRexConverterImpl.convertLiteral(SqlNodeToRexConverterImpl.java:118)
        at org.apache.calcite.sql2rel.SqlToRelConverter$Blackboard.visit(SqlToRelConverter.java:4695)
        at org.apache.calcite.sql2rel.SqlToRelConverter$Blackboard.visit(SqlToRelConverter.java:4013)
        at org.apache.calcite.sql.SqlLiteral.accept(SqlLiteral.java:534)
        at org.apache.calcite.sql2rel.SqlToRelConverter$Blackboard.convertExpression(SqlToRelConverter.java:4577)
        at org.apache.calcite.sql2rel.StandardConvertletTable.convertExpressionList(StandardConvertletTable.java:790)
        at org.apache.calcite.sql2rel.StandardConvertletTable.convertFunction(StandardConvertletTable.java:647)
        ... 18 more
    

    2. 分析原因

    通过代码追踪,字符串的编码最终在此处获取

      public RexLiteral makeCharLiteral(NlsString str) {
        assert str != null;
        //此处获取字符串的编码
        RelDataType type = SqlUtil.createNlsStringType(typeFactory, str);
        return makeLiteral(str, type, SqlTypeName.CHAR);
      }
      //SqlUtil.java
     public static RelDataType createNlsStringType(
          RelDataTypeFactory typeFactory,
          NlsString str) {
        Charset charset = str.getCharset();
        if (null == charset) {
          charset = typeFactory.getDefaultCharset();
        }
       ...
        return type;
      }
    

    可以看到如果NlsString的编码为null的话,就会采用RelDataTypeFactory的默认编码,否则直接采用NlsString的编码。那么NlsString的编码是如何设置呢?找到Calcite parser中关于String常量提取的地方:

        //带编码的字符串
        <PREFIXED_STRING_LITERAL>
            //获取ChartSet
            { charSet = SqlParserUtil.getCharacterSet(token.image); }
        |   <QUOTED_STRING>
        |   <UNICODE_STRING_LITERAL> {
                // TODO jvs 2-Feb-2009:  support the explicit specification of
                // a character set for Unicode string literals, per SQL:2003
                unicodeEscapeChar = BACKSLASH;
                charSet = "UTF16";
            }
        )
        {
            p = SqlParserUtil.parseString(token.image);
            SqlCharStringLiteral literal;
            try {
                literal = SqlLiteral.createCharString(p, charSet, getPos());
            } catch (java.nio.charset.UnsupportedCharsetException e) {
                throw SqlUtil.newContextException(getPos(),
                    RESOURCE.unknownCharacterSet(charSet));
            }
            frags = startList(literal);
            nfrags++;
        }
    ...
    //编码字符串的内容
    < PREFIXED_STRING_LITERAL: ("_" <CHARSETNAME> | "N") <QUOTED_STRING> >
    

    从以上代码不难理解,可以直接设置字符串常量的编码,格式为 _UTF8 ''中国" 这种形式,即上述SQL 可以写成

     select id from user_behavior where rlike(text, _UTF8 '.*中国.*')
    

    那么支持哪些编码呢?Calcite 支持UTF8、UTF16、ISO-8859-1等,关于这几个编码的区别,请自行Google。
    回到问题,那么select id from user_behavior where rlike(text, '.*中国.*')
    为什么会报错呢? 根据代码逻辑,如果没有显示的指定字符集的话,就使用RelDataTypeFactory 的默认字符集, RelDataTypeFactory的默认字符集在

    //RelDataTypeFactoryImpl.java
      public Charset getDefaultCharset() {
        return Util.getDefaultCharset();
      }
    //Util.java
      public static Charset getDefaultCharset() {
        return DEFAULT_CHARSET;
      }
      private static final Charset DEFAULT_CHARSET =
          Charset.forName(CalciteSystemProperty.DEFAULT_CHARSET.value());
    
    //CalciteSystemProperty.java
      public static final CalciteSystemProperty<String> DEFAULT_CHARSET =
          stringProperty("calcite.default.charset", "ISO-8859-1");
    

    原因总结如下: 如果没有显示指定String常量的编码时,采用TypeFactory的编码,而TypeFactory的默认编码是'ISO-8859-1', 这是一种单字节编码,中文会出现乱码情况,所以Calcite会报错

    3. 解决方式

    主要有两种方式解决字符串编码问题

    3.1 简单方式

    所谓简单的方式就是直接显示指字符串编码,比如说指定用'UTF8'、'UTF16'等支持中文的编码,即

     select id from user_behavior where rlike(text, _UTF16 '.*中国.*')
    

    3.2 修改TypeFactory的默认编码

    可以直接覆写RelDataTypeFactoryImplgetDefaultCharset()方法,如

    @Override
    public Charset getDefaultCharset() {
          return Charset.forName("UTF8");
    }
    

    4. 总结

    在3提到了两种方试修改字符串编码,那么在项目中采用哪种更为优雅,个人的观点,没有哪一种更好,但是可以根据不同的常见来使用

    1. 如果SQL中只有极少数中文符,而且SQL使用法也乐于修改SQL,我的建议是采用显示指定字符串的字符串,因为采用'UTF8'、'UTF16'等编码会在大多数情况下有空间浪费,特比是英文字占多的情况下
    2. 如果涉及字符串基本都是中文或者SQL业务方是小白用户(不愿意修改SQL以指定编码), 建议使用修改TypeFactory的默认编码方式

    以上为个人在使用Calcite中遇到的问题,由于笔者使用和探索Calcite时间也不长,以上内容难免有错误与不准确之处,还望各位读者不吝指正,相互学习。

    测试代码在这里

    相关文章

      网友评论

          本文标题:Calcite 中文编码问题

          本文链接:https://www.haomeiwen.com/subject/jxilaqtx.html