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的默认编码
可以直接覆写RelDataTypeFactoryImpl
的getDefaultCharset()
方法,如
@Override
public Charset getDefaultCharset() {
return Charset.forName("UTF8");
}
4. 总结
在3提到了两种方试修改字符串编码,那么在项目中采用哪种更为优雅,个人的观点,没有哪一种更好,但是可以根据不同的常见来使用
- 如果SQL中只有极少数中文符,而且SQL使用法也乐于修改SQL,我的建议是采用显示指定字符串的字符串,因为采用'UTF8'、'UTF16'等编码会在大多数情况下有空间浪费,特比是英文字占多的情况下
- 如果涉及字符串基本都是中文或者SQL业务方是小白用户(不愿意修改SQL以指定编码), 建议使用修改TypeFactory的默认编码方式
以上为个人在使用Calcite中遇到的问题,由于笔者使用和探索Calcite时间也不长,以上内容难免有错误与不准确之处,还望各位读者不吝指正,相互学习。
测试代码在这里