• 如何自定义一个Calcite对Tablesaw查询的适配器


    Tablesaw是Java领域中一个比较好用的数据分析工具,可以对数据集进行清洗、转换、统计等。如果能使用Sql对数据集进行查询,那就更完美了。使用Calcite这个开源项目完全可以做到,按照官网所说,Calcite适配了Csv、Elasticsearch、Druid等数据源,适配Tablesaw更不在话下。但是官网中没有提供Tablesaw的适配器,我们可以参考Csv适配器来简单适配Tablesaw。

    1、Calcite如何查询Sql

    使用Sql查询数据集,想必很多人都会想到Jdbc查询的那一套,其实Calcite就是基于Jdbc来实现的,也是那几个步骤:

    /**
     * 1. 加载驱动类
     * 2. 创建Connection
     * 3. 创建Statement
     * 4. 执行查询获取数据集
     * 5. 关闭资源
    */
    public static void exec(String sql) throws SQLException {
        Properties info = new Properties();
        Class.forName("org.apache.calcite.jdbc.Driver");    
        try (Connection conn = DriverManager.getConnection("jdbc:calcite:", info);  
            Statement stat = conn.createStatement()) {      
            final ResultSet resultSet = stat.executeQuery(sql);     
            ....
        }
    }
    

    Jdbc查询在查询时候会指定连接地址,但是如果对Tablesaw查询要怎么指定连接地址呢?我们可以通过设置查询属性,实现Calcite预留的接口,达到查询Tablesaw的目的。

    2、Calcite的连接属性

    Calcite的驱动由Avatica提供,可以设置的属性可以查看官网,主要有:

    属性 描述
    caseSensitive 大小写敏感
    lex 语法,默认Oracle语法
    quoting 标识符的引用语法:对特殊字段的引用,Oracle:"form"
    quotedCasing 使用了quoting的标识符,如何排序
    unquotedCasing 没有使用quoting的标识符,如何排序
    timeZone 默认JVM时区,不建议特别指定
    model 模型文件,指定如何处理数据
    schema schema名称
    schemaFactory 创建schema的工厂,model存在则不起效
    schemaType schema类型,model存在则不起效
    typeSystem 字段类型系统,model存在则不起效

    model文件的结构:

    {
      "version": "1.0",
      "defaultSchema": "foodmart",
      "schemas": [
        {
          type: 'custom',
          name: 'twissandra',
          factory: 'org.apache.calcite.adapter.cassandra.CassandraSchemaFactory',
          operand: {
            host: 'localhost',
            keyspace: 'twissandra'
          }
        }
      ]
    }
    

    schema.json的属性:

    属性 描述
    version 版本
    defaultSchema 默认schema
    schemas schema数组,可有多个schema组成
    schemas.name schema的名称
    schemas.type schema的类型,使用自定义类型需要指定factory
    schemas.factory 构造schema的工厂
    schemas.operand 构造schema的自定义参数Map
    schemas.tables schema里面可以有多个表
    schemas.tables.name table的名称
    schemas.tables.type table的类型,使用自定义类型需要指定factory
    schemas.tables.factory 构造table的工厂
    schemas.tables.operand 构造table的自定义参数Map

    可以理解为一个schema.json文件里面有多个schema,一个schema里面有多个table。
    我们也可以通过url的方式传入参数,效果和上面一样:

    jdbc:calcite:schemaFactory=org.apache.calcite.adapter.cassandra.CassandraSchemaFactory; schema.host=localhost; schema.keyspace=twissandra
    

    3、定义Table的结构

    我们需要定义Table的结构,来达到结构化查询的目的。

    public abstract class DataFrameTable extends AbstractTable {
    
        protected tech.tablesaw.api.Table table;
    
        private RelDataType rowType;
    
        DataFrameTable(tech.tablesaw.api.Table table) {
            this.table = table;
        }
    
        @Override
        public RelDataType getRowType(RelDataTypeFactory typeFactory) {
            if (rowType == null) {
                rowType = createRelDataType(typeFactory);
            }
            return rowType;
        }
    
        private RelDataType createRelDataType(RelDataTypeFactory typeFactory) {
            List<RelDataType> types = new ArrayList<>();
            List<String> names = new ArrayList<>();
            for (Column<?> column : table.columns()) {
                DataFrameFieldType type = DataFrameFieldType.of(column.type());
                RelDataType relDataType = type.toType((JavaTypeFactory) typeFactory);
                types.add(relDataType);
                names.add(column.name());
            }
            return typeFactory.createStructType(Pair.zip(names, types));
        }
    }
    

    定义Table,必不可少定义字段数据类型,需要指定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);
    
        private final Class<?> clazz;
        private final ColumnType columnType;
    
        private static final Map<ColumnType, DataFrameFieldType> MAP = new HashMap<>();
    
        static {
            for (DataFrameFieldType value : values()) {
                MAP.put(value.columnType, value);
            }
        }
    
        DataFrameFieldType(Primitive primitive, ColumnType columnType) {
            this(primitive.boxClass, columnType);
        }
    
        DataFrameFieldType(Class<?> clazz, ColumnType columnType) {
            this.clazz = clazz;
            this.columnType = columnType;
        }
    
        public RelDataType toType(JavaTypeFactory typeFactory) {
            RelDataType javaType = typeFactory.createJavaType(clazz);
            RelDataType sqlType = typeFactory.createSqlType(javaType.getSqlTypeName());
            return typeFactory.createTypeWithNullability(sqlType, true);
        }
    
        public static DataFrameFieldType of(ColumnType columnType) {
            return MAP.get(columnType);
        }
    }
    

    这样我们就定义好一个Table的结构了,我们定义的Table中,有一个tech.tablesaw.api.Table,这个是用于提供数据和字段结构的,至于怎么来的,后面细说。

    4、定义Table的数据枚举器Enumerator

    Calcite定义了3中查询Table的方式,可以参考

    • ScannableTable:根据全部数据查询
    • FilterableTable:查询底层DB时进行一部分的数据过滤,再在内存中查询
    • TranslatableTable:自定义优化规则

    我们以最简单的ScannableTable为例子查询。

    public class DataFrameScannableTable extends DataFrameTable implements ScannableTable {
    
        DataFrameScannableTable(tech.tablesaw.api.Table table) {
            super(table);
        }
    
        public Enumerable<Object[]> scan(DataContext root) {
            return new AbstractEnumerable<Object[]>() {
                public Enumerator<Object[]> enumerator() {
                    return new DataFrameEnumerator(table);
                }
            };
        }
    }
    

    ScannableTable的scan方法定义了如何获取Enumerator,而Enumerator是操作数据的关键

    展开查看
    class DataFrameEnumerator implements Enumerator<Object[]> {
    
        private Enumerator<Object[]> enumerator;
    
        DataFrameEnumerator(tech.tablesaw.api.Table table) {
            List<Object[]> objs = new ArrayList<>();
            for (int row = 0; row < table.rowCount(); row++) {
                Object[] rows = new Object[table.columnCount()];
                for (int col = 0; col < table.columnCount(); col++) {
                    Column<?> column = table.column(col);
                    rows[col] = convertToEnumeratorObject(column, row);
                }
                objs.add(rows);
            }
            this.enumerator = Linq4j.enumerator(objs);
        }
    
        public Object[] current() {
            return enumerator.current();
        }
    
        public boolean moveNext() {
            return enumerator.moveNext();
        }
    
        public void reset() {
            enumerator.reset();
        }
    
        public void close() {
            enumerator.close();
        }
    
        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);
            }
        }
    }
    

    5、使用其他方法代替model.json传入参数

    至此,我们只需让Calcite使用我们定义的Table来查询数据就行了,前面说到可以通过传入model.json来实现。我们通过自定义的TableFactory来创建Table:

    public class DataFrameTableFactory implements TableFactory<DataFrameTable> {
    
        private tech.tablesaw.api.Table table;
    
        public DataFrameTableFactory(tech.tablesaw.api.Table table) {
            this.table = table;
        }
    
        @Override
        public DataFrameTable create(SchemaPlus schema,
                                     String name,
                                     Map<String, Object> operand,
                                     RelDataType rowType) {
            return new DataFrameScannableTable(table);
        }
    }
    

    但是我们看到tech.tablesaw.api.Table参数,这个该怎么传入?如果在有数据的前提下,我们很难通过model.json的operand参数来传入,model.json适合根据operand参数到数据源获取数据后再操作。对此,我们使用另外一个方法:

        private static void setTableModel(Connection connection, tech.tablesaw.api.Table table) {
            SchemaPlus rootSchema = ((CalciteConnection) connection).getRootSchema();
            TableFactory<?> tableFactory = new DataFrameTableFactory(table);
            org.apache.calcite.schema.Table t = tableFactory
                    .create(rootSchema, table.name(), ImmutableMap.of(), null);
            rootSchema.add(table.name(), t);
        }
    

    该方法主要是获取CalciteConnection的SchemaPlus,来传入我们定义的Table。

    6、测试

    到此,我们应该能用Sql查询Tablesaw了。我们来测试一下:

    展开查看
        @Test
        public void test() throws SQLException {
            tech.tablesaw.api.Table table = tech.tablesaw.api.Table.create("test");
    
            StringColumn stringColumn = StringColumn.create("A");
            stringColumn.append("bbbbb");
            table.addColumns(stringColumn);
    
            DateColumn dateColumn = DateColumn.create("B");
            dateColumn.append(LocalDate.now());
            table.addColumns(dateColumn);
    
            DateTimeColumn dateTimeColumn = DateTimeColumn.create("C");
            dateTimeColumn.append(LocalDateTime.now());
            table.addColumns(dateTimeColumn);
    
            TimeColumn timeColumn = TimeColumn.create("D");
            timeColumn.append(LocalTime.now());
            table.addColumns(timeColumn);
    
            Table t = DataFrameQueryUtils.exec(table, "SELECT * FROM "test"");
            System.out.println(t);
        }
    
    使用各种Calcite内置的函数也是没问题的,但是如果使用了额外的函数,可能需要额外定义函数的计算方式。

    7、总结

    这个例子只用了Table,并没有使用Schema,其实Schema的原理也差不多,就是定义Table的集合,有需要可以参考官方来自己实现。Calcite做Tablesaw的适配器也不在话下,用二维数组代替Tablesaw也是可以的。虽然如此,但是还是要注意关于时间类型的一些坑,是关于时间类型转换和时区的一些问题,这个有空再总结。

  • 相关阅读:
    HDU 2509 nim博弈
    HDU 1907 nim博弈变形
    HDU 1568 double 快速幂
    HDU 5950 矩阵快速幂
    HDU 1796 容斥原理
    Linux raid信息 查看
    Linux Ubuntu 内核升级
    Ubuntu 14.04 为 root 帐号开启 SSH 登录
    Google 分布式关系型数据库 F1
    分布式事务实现-Spanner
  • 原文地址:https://www.cnblogs.com/ginponson/p/14120531.html
Copyright © 2020-2023  润新知