Calcite 中文编码问题

2019-05-13  本文已影响0人  ni_d58f

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时间也不长,以上内容难免有错误与不准确之处,还望各位读者不吝指正,相互学习。

测试代码在这里

上一篇下一篇

猜你喜欢

热点阅读