• Calcite数据源适配器对时间字段的操作问题


    之前的文章中,说了如何通过Calcite构建一个Tablesaw的适配器,这篇来说说处理时间类型字段遇到的两个问题:

    • 时间转换问题
    • 时间不正确问题

    1、时间转换问题

    在定义Tablesaw对Calcite的类型映射的时候,就定义了相应的类型关系:

    enum DataFrameFieldType {
        STRING(String.class, ColumnType.STRING),
        TEXT(String.class, ColumnType.TEXT),
        BOOLEAN(Primitive.BOOLEAN, ColumnType.BOOLEAN),
        SHORT(Primitive.SHORT, ColumnType.SHORT),
        INT(Primitive.INT, ColumnType.INTEGER),
        LONG(Primitive.LONG, ColumnType.LONG),
        FLOAT(Primitive.FLOAT, ColumnType.FLOAT),
        DOUBLE(Primitive.DOUBLE, ColumnType.DOUBLE),
        DATE(java.sql.Date.class, ColumnType.LOCAL_DATE),
        TIME(java.sql.Time.class, ColumnType.LOCAL_TIME),
        TIMESTAMP(java.sql.Timestamp.class, ColumnType.LOCAL_DATE_TIME);
    }
    

    由上面的枚举可以看出,java.sql.Date对应Table的LOCAL_DATE字段,那是否在Enumerator获取数据的时候,可以直接将LocalDate转为java.sql.Date呢?答案是不行的,你很快收到一个类型错误:

    展开查看
    java.lang.ClassCastException: java.time.LocalDate cannot be cast to java.lang.Number
    	at org.apache.calcite.avatica.util.AbstractCursor$NumberAccessor.getNumber(AbstractCursor.java:722)
    	at org.apache.calcite.avatica.util.AbstractCursor$DateFromNumberAccessor.getDate(AbstractCursor.java:911)
    	at org.apache.calcite.avatica.AvaticaResultSet.getDate(AvaticaResultSet.java:281)
    

    从错误信息来看,是DateFromNumberAccessor.getDate报的错误,是强制转换失败。

    // DateFromNumberAccessor.java
    @Override public Date getDate(Calendar calendar) throws SQLException {
        final Number v = getNumber();
        if (v == null) {
        return null;
        }
        return longToDate(v.longValue() * DateTimeUtils.MILLIS_PER_DAY, calendar);
    }
    

    那为啥要使用这个DateFromNumberAccessor?不是定义了java.sql.Date了吗?应该使用DateAccessor啊。想知道为啥使用DateFromNumberAccessor,那只能看看在哪里创建的Accessor。

    追溯到AvaticaResultSet的execute()方法,accessorList在这时候创建:

     // AvaticaResultSet.java
      protected AvaticaResultSet execute() throws SQLException {
        final Iterable<Object> iterable1 =
            statement.connection.meta.createIterable(statement.handle, state, signature,
                Collections.<TypedValue>emptyList(), firstFrame);
        this.cursor = MetaImpl.createCursor(signature.cursorFactory, iterable1);
        this.accessorList =
            cursor.createAccessors(columnMetaDataList, localCalendar, this);
        this.row = 0;
        this.beforeFirst = true;
        this.afterLast = false;
        return this;
      }
    

    继续追踪,可以发现AbstractCursor的createAccessor创建Accessor,由columnMetaData.type.id来控制和columnMetaData.type.rep来控制。

    // AbstractCursor.java
        ...
        case Types.DATE:
          switch (columnMetaData.type.rep) {
          case PRIMITIVE_INT:
          case INTEGER:
          case NUMBER:
            return new DateFromNumberAccessor(getter, localCalendar);
          case JAVA_SQL_DATE:
            return new DateAccessor(getter);
          default:
            throw new AssertionError("bad " + columnMetaData.type.rep);
          }
        ...
    

    所以,由此可以知道,是由字段的元数据columnMetaData影响着Accessor的创建方式。所以要继续找出创建columnMetaData的方法。从AvaticaResultSet的构造方法可以知道columnMetaData是由Meta.Signature创建的,下一步是要找Meta.Signature的创建方法。

    从上面时序图可以知道,CalciteSignature由CalcitePrepareImpl的prepare2_方法中创建,继续追踪avaticaType方法,这里创建了columnMetaData.type,决定了之后如何创建Accessor。看JavaTypeFactoryImpl的getJavaClass方法,这里是决定使用DateFromNumberAccessor的关键:

    展开查看
    // JavaTypeFactoryImpl.java
    public Type getJavaClass(RelDataType type) {
        if (type instanceof JavaType) {
          JavaType javaType = (JavaType) type;
          return javaType.getJavaClass();
        }
        if (type instanceof BasicSqlType || type instanceof IntervalSqlType) {
          switch (type.getSqlTypeName()) {
          case VARCHAR:
          case CHAR:
            return String.class;
          case DATE:
          case TIME:
          case TIME_WITH_LOCAL_TIME_ZONE:
          case INTEGER:
          case INTERVAL_YEAR:
          case INTERVAL_YEAR_MONTH:
          case INTERVAL_MONTH:
            return type.isNullable() ? Integer.class : int.class;
          case TIMESTAMP:
          case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
          case BIGINT:
          case INTERVAL_DAY:
          case INTERVAL_DAY_HOUR:
          case INTERVAL_DAY_MINUTE:
          case INTERVAL_DAY_SECOND:
          case INTERVAL_HOUR:
          case INTERVAL_HOUR_MINUTE:
          case INTERVAL_HOUR_SECOND:
          case INTERVAL_MINUTE:
          case INTERVAL_MINUTE_SECOND:
          case INTERVAL_SECOND:
            return type.isNullable() ? Long.class : long.class;
          case SMALLINT:
            return type.isNullable() ? Short.class : short.class;
          case TINYINT:
            return type.isNullable() ? Byte.class : byte.class;
          case DECIMAL:
            return BigDecimal.class;
          case BOOLEAN:
            return type.isNullable() ? Boolean.class : boolean.class;
          case DOUBLE:
          case FLOAT: // sic
            return type.isNullable() ? Double.class : double.class;
          case REAL:
            return type.isNullable() ? Float.class : float.class;
          case BINARY:
          case VARBINARY:
            return ByteString.class;
          case GEOMETRY:
            return GeoFunctions.Geom.class;
          case SYMBOL:
            return Enum.class;
          case ANY:
            return Object.class;
          case NULL:
            return Void.class;
          }
        }
        switch (type.getSqlTypeName()) {
        case ROW:
          assert type instanceof RelRecordType;
          if (type instanceof JavaRecordType) {
            return ((JavaRecordType) type).clazz;
          } else {
            return createSyntheticType((RelRecordType) type);
          }
        case MAP:
          return Map.class;
        case ARRAY:
        case MULTISET:
          return List.class;
        }
        return null;
      }
    

    这里可以看到如果是JavaType的话,返回我们指定的Java类型,如果是BasicSqlType的话,时间类型会转为int类型。
    这里归咎到底是字段类型设置的问题,如果我们之前的类型使用的是SqlType:

    // DataFrameFieldType.java
        public RelDataType toType(JavaTypeFactory typeFactory) {
            RelDataType javaType = typeFactory.createJavaType(clazz);
            RelDataType sqlType = typeFactory.createSqlType(javaType.getSqlTypeName());
            return typeFactory.createTypeWithNullability(sqlType, true);
        }
    

    Enumerator获取的Date要转为EpochDay:

    // DataFrameEnumerator.java
        private Object convertToEnumeratorObject(Column<?> column, int row) {
            final TimeZone gmt = TimeZone.getTimeZone("GMT");
            if (column instanceof DateColumn) {
                return ((DateColumn) column).get(row).toEpochDay();
            } else if (column instanceof TimeColumn) {
                return Date.from(
                        ((TimeColumn) column).get(row)
                                .atDate(LocalDate.ofEpochDay(0))
                                .atZone(gmt.toZoneId())
                                .toInstant()
                ).getTime();
            } else if (column instanceof DateTimeColumn) {
                return Date.from(
                        ((DateTimeColumn) column).get(row)
                                .atZone(gmt.toZoneId())
                                .toInstant()
                ).getTime();
            } else {
                return column.get(row);
            }
        }
    

    如果不想转int的话,直接使用java.sql.Date类型的话,对应Enumerator转为java.sql.Date:

    // DataFrameFieldType.java
        public RelDataType toType(JavaTypeFactory typeFactory) {
            RelDataType javaType = typeFactory.createJavaType(clazz);
            return typeFactory.createTypeWithNullability(sqlType, true);
        }
    

    顺便提一句,如果是直接使用LocalDate也是可以的,但是不能使用对应的时间函数,Jdbc识别不出字段类型。

    2、时间不正确问题

    最常见的就是相差8个小时的问题。查看DateAccessor的getDate(Calendar calendar)方法:

    // DateAccessor.java
        @Override public Date getDate(Calendar calendar) throws SQLException {
          java.sql.Date date = (Date) getObject();
          if (date == null) {
            return null;
          }
          if (calendar != null) {
            long v = date.getTime();
            v -= calendar.getTimeZone().getOffset(v);
            date = new Date(v);
          }
          return date;
        }
    

    v -= calendar.getTimeZone().getOffset(v);,这里结果时间会减去calendar的时区偏移量,从AvaticaResultSet的构造方法看出,这个偏移量由timeZone来构建,在没有指定timeZone参数的情况下,默认使用JVM所在的时区。

    // AvaticaConnection.java
      public TimeZone getTimeZone() {
        final String timeZoneName = config().timeZone();
        return timeZoneName == null
            ? TimeZone.getDefault()
            : TimeZone.getTimeZone(timeZoneName);
      }
    

    所以,如果结果时间是GMT+8的时间,那么结果时间就会减去东8时区的偏移量,比实际结果慢8个小时。
    解决方法有两个:

    1. 连接属性设置TimeZone为gmt,Enumerator的时间是GMT+8的时间
    2. 连接属性使用Jvm的TimeZone,Enumerator的时间是GMT的时间
  • 相关阅读:
    离散时间基本信号1
    连续时间信号的基本运算2
    连续时间信号的基本运算1
    循环冗余校验码
    奇偶校验
    CAD编辑器哪个好用?如何使用CAD编辑器
    CAD转DXF怎么转换?教你三种转换方法
    CAD转PDF的软件哪个比较好用?用这两个很方便
    CAD简易口诀,保你一天就记住!零基础也能轻松学!CAD制图宝典!
    怎么将CAD转PNG格式?这两种方法值得收藏
  • 原文地址:https://www.cnblogs.com/ginponson/p/14120519.html
Copyright © 2020-2023  润新知